This version makes a big jump forward:

🌐 Multi-Server Support – view & manage multiple Fail2Ban servers from one dashboard
🔐 Authentication & Roles – viewer (read-only) and admin (ban/unban, blocklist mgmt.)
📂 Per-server archives & blocklists for cleaner organization
 Performance improvements + refined marker system (repeat bans & ban increases)

It’s still shell + PHP (no DB, no frameworks), designed for small to medium setups, and works great on Raspberry Pi or VPS.
This commit is contained in:
SubleXBle
2025-08-29 23:16:42 +02:00
committed by GitHub
71 changed files with 3071 additions and 1195 deletions

View File

@@ -0,0 +1,67 @@
#!/bin/bash
# This is the Logfile-Reader for the local installation - so you will have to edit the OUTPUT_JSON_DIR to fit your Webserver Installation
#
LOGFILE="/var/log/fail2ban.log"
OUTPUT_JSON_DIR="/var/www/html/Fail2Ban-Report/archive/<SERVERNAME>/fail2ban"
# <SERVERNAME> is the Name of your local Server Folder in archive/
TODAY=$(date +"%Y-%m-%d")
OUTPUT_JSON_FILE="$OUTPUT_JSON_DIR/fail2ban-events-$(date +"%Y%m%d").json"
mkdir -p "$OUTPUT_JSON_DIR"
echo "[" > "$OUTPUT_JSON_FILE"
# Grep all relevant Events
grep -E "(Ban|Unban)" "$LOGFILE" | awk -v today="$TODAY" '
{
timestamp = $1 " " $2;
if (index(timestamp, today) != 1) next;
action = "";
ip = "";
if ($0 ~ /Increase Ban/) {
action = "Increase Ban";
match($0, /Increase Ban ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/, m);
if (m[1]) ip = m[1];
} else if ($0 ~ /Ban/) {
action = "Ban";
match($0, /Ban ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/, m);
if (m[1]) ip = m[1];
} else if ($0 ~ /Unban/) {
action = "Unban";
match($0, /Unban ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/, m);
if (m[1]) ip = m[1];
}
# Extract jail from first non-numeric bracketed section
text = $0;
c = 0;
delete arr;
while (match(text, /\[[^]]+\]/)) {
content = substr(text, RSTART+1, RLENGTH-2);
c++;
arr[c] = content;
text = substr(text, RSTART + RLENGTH);
}
jail = "unknown";
for(i=1; i<=c; i++) {
if (arr[i] !~ /^[0-9]+$/) {
jail = arr[i];
break;
}
}
if (ip != "") {
printf " {\n \"timestamp\": \"%s\",\n \"action\": \"%s\",\n \"ip\": \"%s\",\n \"jail\": \"%s\"\n },\n", timestamp, action, ip, jail;
}
}
' >> "$OUTPUT_JSON_FILE"
# Remove last comma
if [ -s "$OUTPUT_JSON_FILE" ]; then
sed -i '$ s/},/}/' "$OUTPUT_JSON_FILE"
fi
echo "]" >> "$OUTPUT_JSON_FILE"
echo "✅ JSON created: $OUTPUT_JSON_FILE"

View File

@@ -1,11 +1,13 @@
#!/bin/bash
#
# This is the Firewall-Update Script for the local installation - you you would have to edit the BLOCKLIST_DIR to fit your Installation
#
set -euo pipefail
# --- Configuration ---
BLOCKLIST_DIR="/var/www/vhosts/suble.org/xbkupx/Fail2Ban-Report/archive"
LOGFILE="/opt/Fail2Ban-Report/Firewall-Report.log"
LOGGING=true # Set to true to enable logging
BLOCKLIST_DIR="/var/www/html/Fail2Ban-Report/archive/<SERVERNAME>/blocklists" # Or Webarchive when local
LOGFILE="/opt/Fail2Ban-Report/Firewall.log"
LOGGING=false # Set to true to enable logging
# --- Set PATH ---
export PATH="/usr/sbin:/usr/bin:/sbin:/bin"
@@ -49,37 +51,50 @@ for FILE in "$BLOCKLIST_DIR"/*.blocklist.json; do
fi
# Extract active and inactive IPs
active_ips=$(jq -r '.[] | select(.active != false) | .ip' "$FILE")
inactive_ips=$(jq -r '.[] | select(.active == false) | .ip' "$FILE")
mapfile -t active_ips < <(jq -r '.[] | select(.active != false) | .ip' "$FILE")
mapfile -t inactive_ips < <(jq -r '.[] | select(.active == false) | .ip' "$FILE")
# Block new IPs and update pending flag
for ip in $active_ips; do
blocked_success=()
# --- BLOCK: Collect all new IPs and block them ---
for ip in "${active_ips[@]}"; do
if ! grep -qw "$ip" "$TMP_BLOCKED"; then
log "Blocking IP: $ip"
if ufw deny from "$ip"; then
log "Blocked $ip successfully, updating pending flag"
tmp_file=$(mktemp)
jq --arg ip "$ip" 'map(if .ip == $ip then .pending = false else . end)' "$FILE" > "$tmp_file" \
&& mv "$tmp_file" "$FILE"
blocked_success+=("$ip")
else
log "Failed to block $ip via ufw"
fi
fi
done
# Remove UFW rules for inactive IPs
for ip in $inactive_ips; do
# Reload UFW once after all block actions
if ((${#blocked_success[@]} > 0)); then
log "Reloading UFW after block actions"
ufw reload
fi
# --- UNBLOCK: Process each inactive IP individually ---
for ip in "${inactive_ips[@]}"; do
mapfile -t rules < <(ufw status numbered | grep "$ip" | grep "DENY IN" | tac)
for rule in "${rules[@]}"; do
rule_number=$(echo "$rule" | awk -F'[][]' '{print $2}')
log "Removing UFW rule #$rule_number for IP: $ip"
ufw --force delete "$rule_number"
if [[ -n "$rule_number" ]]; then
log "Removing UFW rule #$rule_number for IP: $ip"
ufw --force delete "$rule_number"
fi
done
done
# Clean up JSON by removing inactive entries
# --- JSON Update: pending=false for blocked_success, remove inactive entries ---
tmp_file=$(mktemp)
jq 'map(select(.active != false))' "$FILE" > "$tmp_file" && mv "$tmp_file" "$FILE"
BLOCK_JSON=$(printf '%s\n' "${blocked_success[@]:-}" | jq -R . | jq -s .)
jq --argjson ips "$BLOCK_JSON" '
map(
if (.ip as $ip | $ips | index($ip)) then .pending = false else . end
)
| map(select(.active != false))
' "$FILE" > "$tmp_file" && mv "$tmp_file" "$FILE"
# Set ownership and permissions
chown www-data:www-data "$FILE"

View File

@@ -0,0 +1,31 @@
#!/bin/bash
# Fail2Ban-Report-cronscript.sh
# change dir for cronjobs
cd /opt/Fail2Ban-Report/Backend
LOGFILE="/opt/Fail2Ban-Report/cronjobs.log"
echo "----- cronrun start ------ $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOGFILE"
# Step 1: JSON generation
echo "📝 Step 1: Generating JSON from Fail2Ban logs..." >> "$LOGFILE"
./fail2ban_log2json.sh >> "$LOGFILE" 2>&1
sleep 1
# Step 2: Check for updates
echo "🔎 Step 2: Checking for updates..." >> "$LOGFILE"
./download-checker.sh >> "$LOGFILE" 2>&1
DOWNLOAD_STATUS=$?
# Step 3: Run firewall & sync only if updates available
if [ $DOWNLOAD_STATUS -eq 0 ]; then
echo "✅ Updates found, running sync cycle..." >> "$LOGFILE"
./firewall-update.sh >> "$LOGFILE" 2>&1
./syncback.sh >> "$LOGFILE" 2>&1
else
echo " No Updates, firewall & syncback skipped" >> "$LOGFILE"
fi
# Step 4: Always mark end of cronrun
echo "----- cronrun done! ------ $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOGFILE"

17
Backend/multi/config.env Normal file
View File

@@ -0,0 +1,17 @@
# === Shared Fail2Ban Report Client Config ===
# Authentication
CLIENT_USER="MyClientName"
CLIENT_PASS="MyPassword"
CLIENT_UUID="MyUUID"
# Server URLs
ENDPOINT_URL="https://my.server.tld/Fail2Ban-Report/endpoint/index.php"
UPDATE_URL="https://my.server.tld/Fail2Ban-Report/endpoint/update.php"
DOWNLOAD_URL="https://my.server.tld/Fail2Ban-Report/endpoint/download.php"
BACKSYNC_URL="https://my.server.tld/Fail2Ban-Report/endpoint/backsync.php"
# Local Paths
OUTPUT_JSON_DIR="/opt/Fail2Ban-Report/archive/fail2ban"
BLOCKLIST_DIR="/opt/Fail2Ban-Report/archive/blocklists"
CLIENT_LOG="/var/log/fail2ban-report-client.log"

View File

@@ -0,0 +1,44 @@
#!/bin/bash
set -euo pipefail
# === Config laden ===
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/config.env"
mkdir -p "$BLOCKLIST_DIR"
# --- Update-Check ---
response=$(curl -s -X POST "$UPDATE_URL" \
-F "username=$CLIENT_USER" \
-F "password=$CLIENT_PASS" \
-F "uuid=$CLIENT_UUID")
echo "Server Response:"
echo "$response"
updates=$(echo "$response" | jq -r '.updates | length')
if [ "$updates" -eq 0 ]; then
echo " No updates available."
exit 1
fi
echo "✅ Updates available: $updates blocklist(s)."
# --- download Blocklists ---
for FILE in $(echo "$response" | jq -r '.updates[]'); do
echo "⬇️ Downloading $FILE ..."
curl -s -X POST "$DOWNLOAD_URL?file=$FILE" \
-d "username=$CLIENT_USER" \
-d "password=$CLIENT_PASS" \
-d "uuid=$CLIENT_UUID" \
-o "$BLOCKLIST_DIR/$FILE"
if [ $? -eq 0 ] && [ -s "$BLOCKLIST_DIR/$FILE" ]; then
echo "$FILE downloaded successfully."
else
echo "❌ Failed to download $FILE"
fi
done
echo "🎉 All blocklists processed."

View File

@@ -0,0 +1,109 @@
#!/bin/bash
# fail2ban_log2json.sh
set -euo pipefail
# === Config laden ===
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/config.env"
# === Lokale Variablen ===
LOGFILE="/var/log/fail2ban.log"
TODAY=$(date +"%Y-%m-%d")
TODAY_SHORT=$(date +"%Y%m%d")
OUTPUT_JSON_FILE="$OUTPUT_JSON_DIR/fail2ban-events-$TODAY_SHORT.json"
mkdir -p "$OUTPUT_JSON_DIR"
echo "[" > "$OUTPUT_JSON_FILE"
# Grep all relevant Events
grep -E "(Ban|Unban)" "$LOGFILE" | awk -v today="$TODAY" '
{
timestamp = $1 " " $2;
if (index(timestamp, today) != 1) next;
action = "";
ip = "";
if ($0 ~ /Increase Ban/) {
action = "Increase Ban";
match($0, /Increase Ban ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/, m);
if (m[1]) ip = m[1];
} else if ($0 ~ /Ban/) {
action = "Ban";
match($0, /Ban ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/, m);
if (m[1]) ip = m[1];
} else if ($0 ~ /Unban/) {
action = "Unban";
match($0, /Unban ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/, m);
if (m[1]) ip = m[1];
}
# Extract jail from first non-numeric bracketed section
text = $0;
c = 0;
delete arr;
while (match(text, /\[[^]]+\]/)) {
content = substr(text, RSTART+1, RLENGTH-2);
c++;
arr[c] = content;
text = substr(text, RSTART + RLENGTH);
}
jail = "unknown";
for(i=1; i<=c; i++) {
if (arr[i] !~ /^[0-9]+$/) {
jail = arr[i];
break;
}
}
if (ip != "") {
printf " {\n \"timestamp\": \"%s\",\n \"action\": \"%s\",\n \"ip\": \"%s\",\n \"jail\": \"%s\"\n },\n", timestamp, action, ip, jail;
}
}
' >> "$OUTPUT_JSON_FILE"
if [ -s "$OUTPUT_JSON_FILE" ]; then
sed -i '$ s/},/}/' "$OUTPUT_JSON_FILE"
fi
echo "]" >> "$OUTPUT_JSON_FILE"
echo "✅ JSON created: $OUTPUT_JSON_FILE"
# === Upload JSON to Server ===
upload_file() {
local file=$1
echo "🔄 Uploading $file ..."
response=$(curl -s -w "\n%{http_code}" -X POST "$ENDPOINT_URL" \
-F "username=$CLIENT_USER" \
-F "password=$CLIENT_PASS" \
-F "uuid=$CLIENT_UUID" \
-F "file=@$file" || true)
http_code=$(tail -n1 <<< "$response")
body=$(sed '$d' <<< "$response")
if [ "$http_code" -eq 0 ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') ❌ Connection failed to $ENDPOINT_URL" | tee -a "$CLIENT_LOG"
return 1
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') HTTP Status: $http_code" | tee -a "$CLIENT_LOG"
echo "$(date '+%Y-%m-%d %H:%M:%S') Response Body: $body" | tee -a "$CLIENT_LOG"
if [ "$http_code" -ne 200 ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') ❌ Upload failed (HTTP $http_code)" | tee -a "$CLIENT_LOG"
return 1
fi
success=$(echo "$body" | jq -r '.success // empty')
if [ "$success" != "true" ]; then
message=$(echo "$body" | jq -r '.message // empty')
echo "$(date '+%Y-%m-%d %H:%M:%S') ❌ Endpoint rejected the file: $message" | tee -a "$CLIENT_LOG"
return 1
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') ✅ Upload succeeded for $file" | tee -a "$CLIENT_LOG"
}
upload_file "$OUTPUT_JSON_FILE"
echo "✅ Upload completed."

View File

@@ -3,8 +3,8 @@
set -euo pipefail
# --- Configuration ---
BLOCKLIST_DIR="/path/to/web/archive"
LOGFILE="/var/log/Fail2Ban-Report.log"
BLOCKLIST_DIR="/opt/Fail2Ban-Report/archive/blocklists" # Or Webarchive when local
LOGFILE="/opt/Fail2Ban-Report/Firewall.log"
LOGGING=true # Set to true to enable logging
# --- Set PATH ---

64
Backend/multi/syncback.sh Normal file
View File

@@ -0,0 +1,64 @@
#!/bin/bash
# syncback.sh
set -euo pipefail
# === Config laden ===
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/config.env"
# === Logging function ===
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $*" | tee -a "$CLIENT_LOG"
}
# === Collect all blocklist files ===
mapfile -t files < <(find "$BLOCKLIST_DIR" -maxdepth 1 -type f -name '*.blocklist.json')
if [ "${#files[@]}" -eq 0 ]; then
log "No blocklist files found in $BLOCKLIST_DIR"
exit 0
fi
# === Prepare curl args for multiple files ===
curl_args=()
for f in "${files[@]}"; do
curl_args+=("-F" "file[]=@$f")
done
# Add auth fields
curl_args+=(
"-F" "username=$CLIENT_USER"
"-F" "password=$CLIENT_PASS"
"-F" "uuid=$CLIENT_UUID"
)
log "Uploading ${#files[@]} blocklist file(s) to backsync.php ..."
# === Upload to server ===
response=$(curl -s -w "\n%{http_code}" "${curl_args[@]}" "$BACKSYNC_URL" || true)
http_code=$(tail -n1 <<< "$response")
body=$(sed '$d' <<< "$response")
log "HTTP Status: $http_code"
log "Server Response: $body"
if [ "$http_code" -ne 200 ]; then
log "❌ Upload failed (HTTP $http_code)"
exit 1
fi
success=$(echo "$body" | jq -r '.success // empty')
if [ "$success" != "true" ]; then
message=$(echo "$body" | jq -r '.message // empty')
log "❌ Endpoint rejected the files: $message"
exit 1
fi
uploaded_files=$(echo "$body" | jq -r '.files[]?')
if [ -n "$uploaded_files" ]; then
log "✅ Successfully uploaded files: $uploaded_files"
else
log "⚠️ No files were uploaded according to server response"
fi
log "All blocklists processed and upload attempt finished."

View File

@@ -3,14 +3,17 @@ report=false
report_types=abuseipdb,ipinfo
[AbuseIPDB API Key]
abuseipdb_key=YOUR_ABUSEIPDB_API_KEY_HERE
abuseipdb_key=YOUR-API-KEY
[IPInfo API Key]
ipinfo_key=YOUR_IPINFO_API_KEY_HERE
[IP-Info API Key]
ipinfo_key=YOUR-API-KEY
[Fail2Ban-Daily-List-Settings]
max_display_days=7
[Warnings]
enabled=true
threshold=10:50
threshold=5:20
[Default Server]
defaultserver=

159
Docs/Adding-Clients.md Normal file
View File

@@ -0,0 +1,159 @@
# Manual Sync-Client Installation Fail2Ban-Report v0.5.0
> ### Username of the Client and Displayed Servername in Web-UI are the same.
> Wanna know more about ![Chain of Trust](chain-of-trust.md) or ![Syncronisation Concept](Sync-Concept.md) ?
# On the new Sync-Client
## Preparation
- Check Fail2Ban → must be running
- Check UFW → must be running
## Install required packages
```
apt update -qq && apt install jq gawk curl -y -qq
```
Structure of Fail2Ban-Report Sync-Client (!***Case-Sensitive***!)
```
/opt/Fail2Ban-Report/
Backend/
Helper-Scripts/
Settings
archive/
fail2ban/
blocklists/
```
## Helper scripts
in `/opt/Fail2Ban-Report/Helper-Scripts/` create:
- `create-client-uuid.sh`
Make it executable:
```
chmod +x /opt/Fail2Ban-Report/Helper-Scripts/create-client-uuid.sh
```
Run it once to generate client-uuid.json inside
/opt/Fail2Ban-Report/Settings/
```
./create-client-uuid.sh
```
The client UUID will be displayed
You can later see the created UUID with
```
cat /opt/Fail2Ban-Report/Settings/client-uuid.json
```
---
## Backend scripts
> Note: Credentials:
> Username and Servername are basically the same thing.
> The Client will show up on the Server by it's Username
> So if you have your testserver (testing.yourodmain.tld) as Client and you are using the "UserName" "TestServer" it will show up in the UI as TestServer (Case-Sensitive)
In /opt/Fail2Ban-Report/Backend/ create or copy the following scripts:
- config.env
- fail2ban_log2json.sh
- download-checker.sh
- firewall-update.sh
- syncback.sh
- Fail2Ban-Report-cronscript.sh
> **Note:** you can set logging in ***every script*** to your desired logging destination and logfile name.
Make all .sh files executable:
```
chmod +x /opt/Fail2Ban-Report/Backend/*.sh
```
edit config.env
```
# === Shared Fail2Ban Report Client Config ===
# Authentication
CLIENT_USER="MyClientName"
CLIENT_PASS="MyPassword"
CLIENT_UUID="MyUUID"
# Server URLs
ENDPOINT_URL="https://my.server.tld/Fail2Ban-Report/endpoint/index.php"
UPDATE_URL="https://my.server.tld/Fail2Ban-Report/endpoint/update.php"
DOWNLOAD_URL="https://my.server.tld/Fail2Ban-Report/endpoint/download.php"
BACKSYNC_URL="https://my.server.tld/Fail2Ban-Report/endpoint/backsync.php"
# Local Paths
OUTPUT_JSON_DIR="/opt/Fail2Ban-Report/archive/fail2ban"
BLOCKLIST_DIR="/opt/Fail2Ban-Report/archive/blocklists"
CLIENT_LOG="/var/log/fail2ban-report-client.log"
```
You will have to edit:
Authentication Part
`CLIENT_USER` this is your Username on the Server and also the Name that is Displayed in Web-UI for this Server
`CLIENT_PASS` the Passwort for your Client
`CLIENT_UUID` the UUID that was created with `create-client-uuid.sh`
Server URLs
change `my.server.tld/` to fit your envoirement
---
> If you are curious why there are 4 Sync URLs read the [Syncronisation-Concept](Sync-Concept.md)
---
## Cron jobs
you set the Fail2Ban-Report-cronscript.sh as the "Master Cronscript"
```
crontab -e
```
then
```
*/5 * * * * /opt/Fail2Ban-Report/Backend/Fail2Ban-Report-cronscript.sh >> /opt/Fail2Ban-Report/Backend/Fail2ban-Report-cronscript.sh
```
This will add a cronjob running the script every 5 minutes
change `*/5` to `*/10` or `*/15` to run every 10 or 15 minutes
The Script will call the other Backend Scripts:
- upload of new fail2ban-log json file
- check if new update of blocklists is available
- if yes download blocklist
- change firewall
- upload changed blocklist
- script ends
---
# On the UI-Server
## Go to Helper-Scripts:
```cd /opt/Fail2Ban-Report/Helper-Scripts/```
```./manage-clients.sh```
Enter data for the new client.
## WebUI configuration
Adjust `.htaccess` in `/var/www/html/Fail2Ban-Report/` (or where your Installation exists) and `/endpoint/` to let the new IPs access it
Add your IP (use Require Any)
## After first sync of Fail2Ban events from the new Client
```cd /opt/Fail2Ban-Report/Helper-Scripts/```
```./folder-watchdog.sh```

View File

@@ -0,0 +1,40 @@
## 🔑 Authentication in Fail2Ban-Report
This version introduces a **user-based authentication system** to secure access to the Fail2Ban-Report web interface.
### ✨ Key Features
1. **Session-based Login**
- Users authenticate with username and password.
- Passwords are securely stored using **bcrypt hashing** (`password_hash` / `password_verify`).
- On successful login, a secure PHP session is established.
2. **Secure Session Management**
- Session cookies are set with `HttpOnly`, `Secure`, and `SameSite=Strict`.
- Inactivity timeout: **30 minutes**.
- Absolute session lifetime: **2 hours**.
- Session ID is automatically regenerated every **15 minutes**.
- Session is bound to the client using a **fingerprint** (browser user-agent + IP subnet).
3. **Role-based Access Control**
- Default role: `viewer`.
- Extendable with roles like `admin` (configured in `users.json`).
- Access checks are handled via the `is_admin()` function.
4. **Login / Logout Mechanism**
- Logout reliably destroys the session and clears the session cookie.
- Failed login attempts are logged via `error_log` → compatible with **Fail2Ban** monitoring.
5. **User Data**
- User accounts are stored in a local JSON file (`users.json`).
- Structure: `username`, `password` (hash), `role`.
- File should be protected with strict filesystem permissions.
---
### Protection-Level
- Protects the Fail2Ban-Report Blocklists from unauthorized access.
- Hardened against session hijacking and fixation attacks.
- Provides a foundation for future improvements (e.g., CSRF protection [_Login Page_], additional roles, additional admin features, Fail2Ban Support by logging failed login attempts).
---

252
Docs/Setup-Instructions.md Normal file
View File

@@ -0,0 +1,252 @@
# 🔧 Fail2Ban-Report V 0.5.0 Manual Setup Instructions
This guide explains how to install **Fail2Ban-Report v0.5.0** from scratch.
It covers the Web-UI setup, security hardening, backend configuration, and user management.
> 🚨 This guide assumes that you have downloaded the project to `/home/USER/` and that the project folder is named `Fail2Ban-Report-latest`.
> Also that you want to install the Web-UI in `/var/www/html/` to get the URL `https://yourdomain.tld/Fail2Ban-Report/`
> Therefore, any copy commands or paths in this guide are based on this assumption. Please edit Paths to fit your Setup.
> Folder Names are Case Sensitive - please use them as descibed to avoid conflicts
Wanna know more about ![Chain of Trust](chain-of-trust.md) or ![Syncronisation Concept](Sync-Concept.md) ?
---
## ✅ Requirements
- A Linux system (tested with debian only) with the following installed:
- `fail2ban`
- `jq`
- `awk` (gawk)
- `curl`
- `ufw` (only UFW is supported at this time)
- A PHP-enabled web server (e.g. Apache with PHP 7.4+)
- The web server user (e.g. `www-data`) must have write access to the `/archive/` directory
---
## Step 1 Install Web-UI on the Server
1. Create the target directory on your web server, e.g.:
```bash
mkdir -p /var/www/html/Fail2Ban-Report
```
2. Copy ***the content*** of the `Web-UI` folder into it:
```bash
cp -r /home/USER/Fail2Ban-Report-latest/Web-UI/* /var/www/html/Fail2Ban-Report/
```
3. Adjust file ownership so your web server user can access them:
```bash
chown -R -P www-data:www-data /var/www/html/Fail2Ban-Report
```
---
## Step 2 Secure the Web-UI
Edit the included **`.htaccess`** file and apply the following settings:
### 1. Enforce HTTPS
Go to the section **“Basic HTTPS Headers”** and make sure the Web-UI is only available via **HTTPS**.
All unencrypted HTTP requests should be redirected to HTTPS.
### 2. Authentication
Fail2Ban-Report does not include a native base login.
Use the `.htaccess` file to secure access with either **Basic Authentication** or **IP-based restrictions**.
---
### Option A: Password Protection
Enable Basic Auth in `.htaccess`:
```apache
AuthType Basic
AuthName "Restricted Area"
AuthUserFile /etc/apache2/htpasswd/.htpasswd
<RequireAny>
Require valid-user
</RequireAny>
```
* Create the `.htpasswd` file outside of the web root, e.g.:
```bash
htpasswd -c -B /etc/apache2/htpasswd/.htpasswd admin
```
* Store only **bcrypt-hashed passwords**, never plain text.
You can use the built-in Apache `htpasswd` tool (strongly recommended) or an external generator (e.g. my tool: [bcrypt generator](https://suble.net/htpasswd.php)).
> Note, that you need your Sync Clients to allow access via IP also to get over password restrictions
---
### Option B: IP Restriction
```apache
<RequireAny>
Require ip <YOUR_IP_ADDRESS>
</RequireAny>
```
If you want to allow your Sync Clients as well, list them explicitly:
> Note that this will be needed, if you have sync clients and use password protection to allow sync-clients to get over password restriction by using ip address
```apache
<RequireAny>
Require ip <YOUR_IP_ADDRESS>
Require ip <Sync-Client-1>
Require ip <Sync-Client-2>
</RequireAny>
```
⚠️ **Important:** Never allow unauthenticated access to your Web-UI! Use either password protection, IP restriction, or both.
---
## Step 3 Backend Setup
Create the following directory structure (case-sensitive!):
```
/opt/Fail2Ban-Report
├── archive
│ ├── fail2ban
│ └── blocklists
├── Helper-Scripts
├── Settings
└── Backend
```
---
### Helper-Scripts
Copy helper scripts:
```bash
cp -r /home/USER/Fail2Ban-Report-latest/Helper-Scripts/* /opt/Fail2Ban-Report/Helper-Scripts/
```
Adjust paths in `folder-watchdog.sh`:
* Set `BASE_PATH` to the directory of your Web-UI `archive` folder.
---
### Config File
Copy the config file:
```bash
cp -r /home/USER/Fail2Ban-Report/Conf/fail2ban-report.config /opt/Fail2Ban-Report/Settings/
```
Example configuration:
```ini
[reports]
report=false
report_types=abuseipdb,ipinfo
[Fail2Ban-Daily-List-Settings]
max_display_days=7
[Warnings]
enabled=true
threshold=5:20
[Default Server]
defaultserver=
```
* **Reports**: If enabled (`report=true`), you need API keys for [AbuseIPDB](https://www.abuseipdb.com/) and [IPInfo](https://ipinfo.io/).
* **report_types**: can be configured with `report_types=` `abuseipdb`,`ipinfo` (you can set none, one or both - seperate them with `,`)
* **Warnings**: Threshold `5:20` means “Warning” at 5 bans/minute per jail, “Critical” at 20.
* **Default Server**: Define the default server name (folder name) shown in the Web-UI.
---
### Backend Scripts
Copy backend scripts:
```bash
cp -r /home/USER/Fail2Ban-Report/Backend/fail2ban_log2json.sh /opt/Fail2Ban-Report/Backend/
cp -r /home/USER/Fail2Ban-Report/Backend/firewall-update.sh /opt/Fail2Ban-Report/Backend/
```
Adjust script paths:
* `fail2ban_log2json.sh` → set `OUTPUT_JSON_DIR` to `/opt/Fail2Ban-Report/archive/<SERVERNAME>/fail2ban/`
* `firewall-update.sh` → set blocklist path to `/var/www/html/Fail2Ban-Report/archive/<SERVERNAME>/blocklists`
Make scripts executable:
```bash
chmod +x /opt/Fail2Ban-Report/Backend/*.sh
```
---
### Cronjobs
Edit your crontab:
```bash
crontab -e
```
Example (run every 5 minutes):
```bash
*/5 * * * * /opt/Fail2Ban-Report/Backend/fail2ban_log2json.sh
*/5 * * * * /opt/Fail2Ban-Report/Backend/firewall-update.sh
```
* `*/5` = every 5 minutes
* `*/15` = every 15 minutes
* `*/30` = every 30 minutes
Logging is recommended for easier debugging.
---
## Step 4 Create Admin User
To ensure only authorized users can manipulate blocklists, **authentication is required** since v0.5.0.
Go to the helper scripts directory:
```bash
cd /opt/Fail2Ban-Report/Helper-Scripts/
./manage-users.sh
```
* Enter username and password (stored as bcrypt hash only).
* Assign a role:
* `Admin` → can manipulate blocklists
* `Viewer` → read-only access
---
## Done!
👉 Fail2Ban-Report is now running on your server.
To add more clients, follow the ![**“Adding-Clients”** guide](Adding-Clients.md).
> Username of the Client and Displayed Servername in Web-UI are the same.

72
Docs/Sync-Concept.md Normal file
View File

@@ -0,0 +1,72 @@
# Sync Concept of Fail2Ban-Report
> Username of the Client and Displayed Servername in Web-UI are the same.
> Please also read the [Chain of Trust Document](chain-of-trust.md) to understand when Data-Ownership is transfered between Server and Client.
> There is a poibility that set blocks in blocklists will be set back in WebUI because of Client Syncronisation. (Last Truth from Client) Future Versions will prevent this
## Web UI
When a block or unblock action is triggered via the Web UI (example: block):
1. The IP is sent to the blocklist of the respective jail from the correct server, containing:
- IP address
- Timestamp
- `active=true`
- `pending=true`
2. An entry is created in `/archive/update.json` with server name, updated blocklist, and `true`.
---
## On the Client
When the client synchronizes its firewall, it processes the blocklist and applies it to the firewall.
If a block was set:
- `active=true` remains
- `pending` is set to `false`
---
## After Sync on the Server
Once the blocklist is synced back to the server, the entry is no longer shown as `pending` but instead as `active`.
---
## Endpoints
### index.php
1. Client authenticates using server name, password, UUID, and IP (validated via `client-list.json`).
2. `index.php` accepts `fail2ban-event.json` from the client and overwrites the server version.
### update.php
1. Client authenticates with server name, password, UUID, and IP (validated via `client-list.json`).
2. Client queries `update.php` to check if an update is available (`update.json` is checked).
3. Client receives a JSON response with a list of updated blocklists.
4. `update.php` copies the corresponding blocklists into a protected download directory.
5. `update.php` sets the entry for the copied blocklist in `update.json` to `false`.
### download.php
1. Client authenticates (same as with `update.php`).
2. Upon successful authentication, the client receives its blocklists (no direct downloads allowed).
3. After delivery, blocklists are removed from the download directory.
### syncback.php
1. Client authenticates (same as above).
2. Client uploads blocklists to `syncback.php`.
3. `syncback.php` saves the blocklists in a temp directory, locks the server-side blocklist, and overwrites it with the clients latest valid version.
- **Note:** This can cause intermediate changes (between download and sync-back) to be lost. However, it guarantees that server and client are fully consistent afterward.
4. After overwriting, `syncback.php` removes the corresponding blocklist from `update.json` and releases it again.
---
## Resulting Behavior
- Data authority is with the **server** until the client downloads the blocklist.
- Data authority shifts to the **client** until the blocklist is synced back.
- Once synced back, data authority returns to the **server**.
---
## Security
- The server only communicates with authenticated clients.
- No direct access to `.json` files is possible.
- No direct download of blocklists is allowed.
- Although application-level authentication (server name, password, UUID) is sufficient, it is strongly recommended to also restrict client IP addresses for additional security.
- For web access, an additional .htaccess protection (or comparable server-level restriction) is highly recommended.
- An additional **AllowList** in `.htaccess` is highly recommended.

70
Docs/chain-of-trust.md Normal file
View File

@@ -0,0 +1,70 @@
# Chain of Trust Authentication and Data Ownership
> ### Please read carefully
> This is the first Version of the **Fail2Ban-Report** Endpoint - upcoming releases will make changes to ensure a smoother workflow.
The **backend shell scripts** run with `sudo` or root privileges to read Fail2Ban logs and modify firewall rules.
This ensures that processing only occurs at a trusted system level, as long as the server itself is properly secured.
---
## 1) Creation of Daily JSON Files (Fail2Ban Events)
During data transfer from client to server, the following information is verified:
* Username
* Password
* UUID
* IP address (optional but recommended)
Uploads are performed via a PHP script that additionally validates file names and structure.
This ensures that the JSON data comes from a **trusted source**.
### ➡️ The ownership of Fail2Ban event data is transferred from the client to the server.
---
## 2) Manipulation of Blocklists (Web Interface)
* Fail2Ban events are displayed in the Web UI.
* Changes (Block/Unblock) are performed **exclusively through the web interface**.
* The Web UI ensures that browsers and JavaScript cannot directly access the `.json` files.
* Only users with the **Admin** role in Fail2Ban-Report (Application Accounts) can make changes.
* Authentication is handled via a **session-based system**, and passwords are stored only in encrypted form (bcrypt) on the backend.
### ➡️ Data ownership resides with the server, which manages the blocklists and provides them via the UI.
### ➡️ The chain of trust remains intact because only verified accounts can make modifications.
---
## 3) Data Synchronization (Client ↔ Server)
* **The server never actively pushes data.**
* Clients actively check the server for updates.
* Clients re-authenticate with username, password, UUID, and optionally IP address.
* Updates are only provided after successful authentication.
Flow:
1. Client requests update → Server verifies authentication.
2. If an update exists, the server places the blocklist in an endpoint directory.
### ➡️ Data ownership transfers to the client.
3. Client downloads the blocklist; the file is then deleted from the server.
4. Client processes the data locally (root shell script, stores it in `archive`).
5. Client uploads the updated blocklist back to the server (re-authentication required).
6. Server overwrites its blocklist with the client's version.
### ➡️ Data ownership returns to the server.
---
## Conclusion: Chain of Trust
* **Root/sudo shell scripts** perform local processing (trusted environment).
* **Authenticated communication** ensures that only authorized clients and admins can manipulate or transfer blocklists.
* **Data ownership** switches transparently between server and client, but always remains within a verified trust chain.
### ➡️ Result: A complete **Chain of Trust** that guarantees data integrity and security in blocklist management and synchronization.

View File

@@ -0,0 +1,106 @@
# Updating existing Installation of Fail2Ban-Report
> Allready collected Daily Fail2Ban -Event Data (up from V 0.1.0) is fully compatible to V 0.5.0
> Blocklists (up from 0.3.3 in *.blocklist.json format) are fully compatible with V 0.5.0
## Get archive/ to the new Structure
right now in your archive/ folder you can find all sorts of .json files
you will have to create a Folder inside of archive/ for your local Installation. This Folder is the "Name" of your Local Server. So if you create a Folder named "Webserver" your local Server will show up in UI as "Webserver"
in your new folder `archive/<SERVERNAME>/` you will have to create 2 additional Folders:
- fail2ban
- blocklists
Change permissions of the new Created Folders and hand it over to your Webserver-User. (chown)
Moove all Daily Event-Lists to `archive/<SERVERNAME>/fail2ban/`
Moove all Blocklists to `archive/<SERVERNAME>/blocklists/`
Your archive/ is set and ready for V 0.5.0
## get /opt/Fail2Ban-Report/ to it's new Structure
Go to /opt/Fail2Ban-Report/
If this Folder does not exist, create it, as the new Version will need this Folder and Its config.
create following Folders: (! ***Case-Sensitive*** !)
opt/Fail2Ban-Report/Settings/
opt/Fail2Ban-Report/Backend/
opt/Fail2Ban-Report/Helper-Scripts/
copy new Versions of `fail2ban_log2json.sh` and `firewall-update.sh` to /opt/Fail2Ban-Report/Backend
make them executeable
```
chmod +x /opt/Fail2Ban-Report/Backend/*.sh
```
if you are using Cronjobs to run your Backend, please update the Cronjob to the new Path of the Backend-Files
copy the new Config to opt/Fail2Ban-Report/Settings/
Contents of the new `fail2ban-report.config`
```
[reports]
report=false
report_types=abuseipdb,ipinfo
[AbuseIPDB API Key]
abuseipdb_key=YOUR-API-KEY
[IP-Info API Key]
ipinfo_key=YOUR-API-KEY
[Fail2Ban-Daily-List-Settings]
max_display_days=7
[Warnings]
enabled=true
threshold=5:20
[Default Server]
defaultserver=
```
copy following Helper-Scripts from Sources to /opt/Fail2Ban-Report/Helper-Scripts/
- folder-watchdog.sh
- manage-clients.sh
- manage-users.sh
make them executeable
chmod +x /opt/Fail2Ban-Report/Helper-Scripts/*.sh
run the manage-users script to create your admin user
```
./manage-users.sh
```
> - choose 1 to create a user
> - Enter your Username
> - Enter Password
> - Enter "admin" to get admin role (needed to manipulate blocklists)
### Your Backend is now ready for V 0.5.0
---
## Updating Web-UI
> save your `.htaccess` eventually to not have to rework everything or edit new version
Overwrite all your Files in your Web-UI Directory (eg.: `/var/www/html/Fail2Ban-Report/`) with the Files in Web-UI Folder in this Project
you are done - you have upgraded Fail2Ban-Report to it's new version
> if you want to use a checklist, you can use the document for new installation.

View File

@@ -0,0 +1,22 @@
#!/bin/bash
# generate-client-uuid.sh
# This will create a UUID for this Client that will get saved under the path: /opt/Fail2Ban-Report/Settings/client-uuid.json
SETTINGS_DIR="/opt/Fail2Ban-Report/Settings"
UUID_FILE="$SETTINGS_DIR/client-uuid.json"
# create Settings directory if not exist
mkdir -p "$SETTINGS_DIR"
# generate UUID
UUID=$(cat /proc/sys/kernel/random/uuid)
# write JSON
echo "{\"uuid\": \"$UUID\"}" > "$UUID_FILE"
# Set ownership
chown root:www-data "$UUID_FILE"
chmod 0660 "$UUID_FILE"
echo "UUID generated and saved to $UUID_FILE:"
echo "$UUID"

View File

@@ -0,0 +1,29 @@
#!/bin/bash
# once a Client did his first Sync it creates a fail2ban folder inside his directory
# this Script creates the folders for blocklists
# Set the base path here
BASE_PATH="/var/www/html/Fail2Ban-Report/archive"
# Check if the path exists
if [ ! -d "$BASE_PATH" ]; then
echo "The specified path does not exist: $BASE_PATH"
exit 1
fi
# Loop through each subdirectory in the base path
for DIR in "$BASE_PATH"/*/; do
# Check if a "fail2ban" folder exists
if [ -d "${DIR}fail2ban" ]; then
echo "Processing folder: $DIR"
# Create folders next to fail2ban, if they don't exist
mkdir -p "${DIR}blocklists"
mkdir -p "${DIR}ufw"
mkdir -p "${DIR}stats"
# Set ownership for the created folders
chown -R www-data:www-data "${DIR}blocklists" "${DIR}ufw" "${DIR}stats"
fi
done
echo "Done!"

View File

@@ -0,0 +1,95 @@
#!/bin/bash
# manage-clients.sh
# CLI-Tool to manage Fail2Ban-Report Clients for the HTTPS-Endpoint (JSON + bcrypt)
# === Configuration ===
CLIENT_FILE="/opt/Fail2Ban-Report/Settings/client-list.json"
# JSON-File does not exist → create
if [ ! -f "$CLIENT_FILE" ]; then
echo "[]" > "$CLIENT_FILE"
fi
# === Helperfunction ===
function read_password() {
read -sp "Password: " password
echo
read -sp "Confirm Password: " password_confirm
echo
if [ "$password" != "$password_confirm" ]; then
echo "Passwords do not match. Aborting."
exit 1
fi
}
function add_client() {
read -p "Username: " username
read_password
read -p "UUID: " uuid
read -p "IP (optional): " ip
# Password in bcrypt hash (PHP)
hash=$(php -r "echo password_hash('$password', PASSWORD_BCRYPT);")
# attach JSON entry
tmp=$(mktemp)
jq --arg u "$username" --arg p "$hash" --arg id "$uuid" --arg ip "$ip" \
'. += [{"username":$u,"password":$p,"uuid":$id,"ip":$ip}]' "$CLIENT_FILE" > "$tmp" && mv "$tmp" "$CLIENT_FILE"
echo "Client $username added."
}
function edit_client() {
read -p "Username to edit: " username
# Check, if Client exists
exists=$(jq --arg u "$username" 'map(select(.username==$u)) | length' "$CLIENT_FILE")
if [ "$exists" -eq 0 ]; then
echo "Client $username not found."
exit 1
fi
read_password
read -p "UUID: " uuid
read -p "IP (optional): " ip
tmp=$(mktemp)
jq --arg u "$username" --arg p "$(php -r "echo password_hash('$password', PASSWORD_BCRYPT);")" \
--arg id "$uuid" --arg ip "$ip" \
'map(if .username==$u then .password=$p | .uuid=$id | .ip=$ip else . end)' \
"$CLIENT_FILE" > "$tmp" && mv "$tmp" "$CLIENT_FILE"
echo "Client $username updated."
}
function delete_client() {
read -p "Username to delete: " username
tmp=$(mktemp)
jq --arg u "$username" 'map(select(.username != $u))' "$CLIENT_FILE" > "$tmp" && mv "$tmp" "$CLIENT_FILE"
echo "Client $username deleted (if existed)."
}
function list_clients() {
echo "Current Clients:"
jq -r '.[] | "Username: \(.username), UUID: \(.uuid), IP: \(.ip)"' "$CLIENT_FILE"
}
# === Main Menu ===
echo "Select action:"
echo "1) Add client"
echo "2) Edit client"
echo "3) Delete client"
echo "4) List clients"
read -p "Choice [1/2/3/4]: " action
case "$action" in
1) add_client ;;
2) edit_client ;;
3) delete_client ;;
4) list_clients ;;
*) echo "Invalid choice"; exit 1 ;;
esac
# === set ownership ===
chown root:www-data "$CLIENT_FILE"
chmod 0660 "$CLIENT_FILE"

View File

@@ -0,0 +1,46 @@
#!/bin/bash
# manage_users.sh
# CLI-Tool to manage Fail2Ban-Report Users (JSON + bcrypt)
USER_FILE="/opt/Fail2Ban-Report/Settings/users.json"
# JSON-Datei does not exist → create
if [ ! -f "$USER_FILE" ]; then
echo "[]" > "$USER_FILE"
fi
function add_user() {
read -p "Username: " username
read -sp "Password: " password
echo
read -p "Role (admin/viewer): " role
# Password in bcrypt hash (using PHP)
hash=$(php -r "echo password_hash('$password', PASSWORD_BCRYPT);")
# attach JSON-entry
tmp=$(mktemp)
jq --arg u "$username" --arg p "$hash" --arg r "$role" '. += [{"username":$u,"password":$p,"role":$r}]' "$USER_FILE" > "$tmp" && mv "$tmp" "$USER_FILE"
echo "User $username added."
}
function del_user() {
read -p "Username to delete: " username
tmp=$(mktemp)
jq --arg u "$username" 'map(select(.username != $u))' "$USER_FILE" > "$tmp" && mv "$tmp" "$USER_FILE"
echo "User $username deleted (if existed)."
}
echo "Select action:"
echo "1) Add user"
echo "2) Delete user"
read -p "Choice [1/2]: " action
case "$action" in
1) add_user ;;
2) del_user ;;
*) echo "Invalid choice"; exit 1 ;;
esac
chown root:www-data "$USER_FILE"
chmod 0660 "$USER_FILE"

442
README.md
View File

@@ -1,241 +1,321 @@
# Fail2Ban-Report
> Beta 4.0 | Version 0.4.0
![Fail2Ban-Report Beta](https://img.shields.io/badge/Fail2Ban--Report%20-_🕵_V0.5.0_(Beta_5)-gold?style=flat&logoColor=black)
> A simple and clean web-based dashboard to turn your daily Fail2Ban logs into searchable and filterable JSON reports — with optional IP blocklist management for UFW.
> This version brings more stability and performance, as well as improved visibility into Fail2Ban events.
A lightweight web-based "multi-server dashboard" that transforms daily Fail2Ban logs into searchable and filterable JSON reports, while also providing centralized UFW IP blocklist management across all your servers through a pull-based client-side synchronization via secure HTTPS endpoints.
**Integration**
>Designed for easy integration on a wide range of Linux systems — from small Raspberry Pis to modest business setups — though its not (yet) targeted at large-scale enterprise environments.
Flexibility comes from the two backend shell scripts, which you can adapt to your specific environment or log sources to provide the JSON data the web interface needs (daily JSON event files).
High flexibility comes from the backend shell scripts, which you can adapt to your specific environment or log sources to provide the JSON data the web interface needs (daily JSON event files).
🛡️ **Note**: This tool is a visualization and management layer — it does **not** replace proper intrusion detection or access control. Deploy it behind IP restrictions or HTTP authentication.
🔐 Security Notice
## 🛡️ **Note**: This tool is a visualization and management layer — it does **not** replace proper intrusion detection or access control. Deploy it behind IP restrictions or HTTP authentication only!
> ### ❗This version introduces **Authentication** and **Multi-Server Support** as core features.
>
>To enable these, significant parts of the application have been reworked to align with the new multi-server architecture and integrated authentication system. A new `/endpoint/` has also been added to synchronize your Fail2Ban servers with Fail2Ban-Report. If anything not working as expected, feel free to open an discussion or issue. As usual see the [changelog](changelog.md) for more detailed change information.
> For further details, see the [Syncronisation-Concept](Docs/Sync-Concept.md) and the [Chain of Trust](Docs/chain-of-trust.md) or the [Authentication System](Docs/Authentication-System.md) Documentations.
---
## 📑 Table of Contents
- [⚠️ Status of the Project](#-status-of-the-project)
- [Syncronisation-Concept](Docs/Sync-Concept.md)
- [Chain of Trust](Docs/chain-of-trust.md)
- [📚 What It Does](#-what-it-does)
- [📦 Features](#-features)
- [🖥️ Demo](#-demo)
- [🛠️ Installation](#-installation)
- [🧱 Architecture Overview](#-architecture-overview)
- [⚙️ Requirements](#-requirements)
- [🗄️ Server](#-server)
- [📡 Sync-Client](#-sync-client)
- [🆕 What's New in v0.5.0](#-whats-new-in-v050)
- [🪳 Bugfixes (History)](#-bugfixes-history)
- [👀 Outlook](#-outlook)
- [🖼️ Screenshots](#-screenshots)
- [👥 Discussions](#-discussions)
- [📄 Changelog](#-changelog)
- [⚡ Performance & Stress Test](#-performance--stress-test)
- [🛣️ Roadmap or "Things I will have to do - but I do them later"](#-roadmap-or-things-i-will-have-to-do---but-i-do-them-later)
- [🐳 Docker Version](#-Docker-Version)
- [🤝 Contributing](#-contributing)
- [📄 License](#-license)
---
## ⚠️ Status of the Project
**Current Status:**
> Fail2Ban-Report currently manages bans and unbans through **UFW**, serving as a safe **intermediate solution**.
It does **not** directly modify Fail2Ban jails or change existing fail2ban configurations.
> Fail2Ban-Report currently manages bans and unbans via UFW, providing a safe and persistent solution.
It does not modify Fail2Ban jails or existing Fail2Ban configurations directly, instead using UFW for its own "permanent jails".
> **Version 0.5.0 introduces multi-server support and role-based access:** Viewer accounts are read-only, while Admins can manage bans/unbans and blocklists across all connected servers via the dashboard.
**Future Direction:**
> A potential long-term enhancement could include **direct interaction with Fail2Ban jails** — for example, user-controlled bans and unbans per jail.
The existing structured `*.blocklist.json` format is already designed to support this, ensuring that any future manual ban management can remain "persistent", reviewable, and fully auditable.
Please read the [Installation Instructions](Setup-Instructions.md) carefully and secure your deployment with the provided `.htaccess`.
> still a little experimental feature : Use the Installer ![Installer Setup Documentation](installer-setup.md) It would be great if you tell me if the installer worked for your needs.
**Syncronisation-Concept and Chain of Trust**
> you can read about the Syncronisation Concept in this Document [Sync-Concept](Docs/Sync-Concept.md) to get a better understanding of how it works
> you can read about the "Chain of Trust" between Server and Clients in this Document: [Chain of Trust](Docs/chain-of-trust.md)
Critical backend operations (like UFW updates) are executed via root cron scripts; ensure the server running Fail2Ban-Report is fully secured.
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 📚 What It Does
Fail2Ban-Report parses your `fail2ban.log` and generates JSON-based reports viewable via a responsive web dashboard.
It provides optional tools to:
It provides optional tools to:
- 📊 Visualize **ban** and **unban** events, including per-jail statistics
- ⚡ Interact with IPs (e.g., manually block, unblock, get report from external services)
- 📂 Maintain **jail-specific** persistent blocklists (JSON) with `active` and `pending` status
- 🔄 Sync those lists with your system firewall using **ufw**
- 🚨 Show **warning indicators** when ban rates exceed configurable thresholds
- 🚨 Show **Markers** when a IP Address is present more than once in one (yellow) or more (red) jails.
- 📊 View **ban/unban events** and per-jail statistics
- 🌐 Switch between multiple servers in a single dashboard
- 🔐 Use authentication with **viewer** (read-only) and **admin** (block/unblock) roles
- 📂 Maintain **persistent blocklists** (per jail and per server) with metadata (`active`, `pending`, `source`)
- no fire & forget
- ⚡ Apply or remove firewall rules (currently via **ufw**)
- 🚨 Get configureable warnings for unusual activity (DDoS, brute-force, scans)
- 🚨 Mark IPs with 🔴 repeat bans or 🟡 ban increases
- 🔍 Optional integrations: (_Free API-KEYS_)
- [AbuseIPDB](https://www.abuseipdb.com/) for reputation lookups
- [IP-Info.io](https://ipinfo.io/) for region/provider checks
> **Note:** Direct integration with other firewalls or native Fail2Ban jail commands is not yet implemented.
---
## 🧱 Architecture Overview
- **Backend Shell Scripts**:
- Parse logs and generate daily JSON event files
- Maintain and update `*.blocklist.json`
- Apply or remove firewall rules based on blocklist entries (`ufw`)
- **Frontend Web Interface**:
- Displays event timelines, statistics, and per-jail blocklists
- Allows **multi-selection** for bulk ban/report actions
- Shows **pending status** for unprocessed manual actions
- Displays real-time warning indicators
- **JSON Blocklists**:
- Stored per jail
- Contain IP entries with metadata (`active`, `pending`, timestamps, jail name)
> **Note:** Viewer accounts are read-only. Direct integration with other firewalls or native Fail2Ban jail commands is not yet implemented.
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 📦 Features
- 🔍 **Searchable + filterable** log reports (date, jail, IP)
- 🔧 **Integrated JSON blocklist** for persistent Block-Overview
- 🧱 **Firewall sync** using UFW (planned: nftables, firewalld)
- **Lightweight setup** — no DB, no frameworks
- 🔐 **Compatible with hardened environments** (no external assets, strict headers)
- 🛠️ **Installer script** to automate setup and permissions
- 🧩 **Modular design** for easy extension
- 🪵 Optional logging of block/unblock actions (set true/false and logpath in `firewall-update.sh`)
- 🕵️ **Optional Feature :** IP reputation check via AbuseIPDB (manual lookup from web interface)
- 🔍 Searchable & filterable event reports
- 📊 Aggregated statistics (today, yesterday, 7 days, 30 days)
- 📂 Jail- and server-specific blocklists
- 🔄 Firewall sync with UFW
- 🔐 Authentication with role separation
- ⚡ Lightweight: no database, no frameworks
- 🛠️ Setup scripts for installation, permissions, and user management
- 🧩 Modular structure
- 🪵 Optional backend logging for ban/unban actions
- 🔍 Optional integrations: (_Free API-KEYS_)
- [AbuseIPDB](https://www.abuseipdb.com/) for reputation lookups
- [IP-Info.io](https://ipinfo.io/) for region/provider checks
> 🧰 Works even on small setups (Raspberry Pi, etc.)
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 🖥️ Demo
👀 Want to try out the look & feel?
There's a simple demo version available here no backend, no real data:
👉 https://demo.suble.net/ 🔗
Username and Password for Website Access is : `admin`:`admin`
Username and Password for Blocklist manipulation is `admin`:`admin`
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 🛠️ Installation
### Manual Installations:
> - [New Installation](Docs/Setup-Instructions.md)
> - [Upgrading fom 0.3.3 or above](Docs/update-existing-installation.md)
> - [Add Sync-Client](Docs/Adding-Clients.md)
> when your existing installation is older than Version 0.3.3 you can still do the upgrade installation, your Daily json Files will be fully compatible, you would have to rename you blockfile to *.blockfile.json to further use it. A fresh Install would be still recommended
### Auto-Installer
> - expected with 0.5.1
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 🧱 Architecture Overview
**Backend (Shell scripts):**
- Parse Fail2Ban logs → generate daily JSON event files
- Maintain and update jail-specific blocklists (`*.blocklist.json`)
- Sync blocklists with `ufw`
- Provide HTTPS endpoint for multi-server synchronization
**Frontend (PHP Web Interface):**
- Event timeline with filtering and search
- Per-jail blocklist view
- Multi-server dropdown
- Bulk actions (ban/unban/report)
- Pending status for actions not yet applied
- Warning/critical indicators for activity spikes
- Authentication: viewer (read-only) / admin (ban/unban)
**Blocklists (JSON):**
- Stored per jail and per server
- Include metadata: jail, status, timestamps, source
- Modified only by authenticated admins
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## ⚙️ Requirements
### 🗄️ Server
- Fail2Ban with logging enabled
- UFW (for firewall integration)
- HTTPS-capable web server (Apache)
- PHP 7.4+ with JSON support
- `jq` - (https://jqlang.org/)
- `awk` - (https://en.wikipedia.org/wiki/AWK)
- `curl` - (https://curl.se/)
### 📡 Sync-Client
- Fail2Ban with logging enabled
- UFW (for firewall integration)
- `jq` - (https://jqlang.org/)
- `awk` - (https://en.wikipedia.org/wiki/AWK)
- `curl` - (https://curl.se/)
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 🆕 What's New in v0.5.0
- 🌐 **Multi-server support** with HTTPS sync backend
- 🔐 **User authentication** with roles (Admin / Viewer)
- ⚙️ **Reorganized backend**:
- Centralized path handling, less hardcoding
- `archive/` separated per server (fail2ban / blocklists)
- `/opt/Fail2Ban-Report/` cleaned and structured
- 🌐 **Frontend updates**:
- Server selection dropdown
- Admin login + logout (session handling)
- changed Marker Feature to show: - 🔴 repeat bans |🟡(1) ban increases with the number how often it happend ([see bug reports)](#-bugfixes-history))
- 🔒 **Security updates**:
- Bcrypt password storage
- UUID and optional IP checks
- Additional `.htaccess` IP whitelist
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 🪳 Bugfixes (History)
Found a bug? → [Open an issue](https://github.com/SubleXBle/Fail2Ban-Report/issues)
> - ✅ **Date filter** now correctly limits displayed events (0.1.2)
> - ✅ **Jail filter** now correctly shows only the jails present in the displayed event list. (0.2.1)
> - ✅ **File date filtering** fix to include today's JSON logs and ensure latest files are listed correctly. (0.2.2)
> - ✅ **Blocklist Path on unblocking** fixed a possible bug that could lead to not finding the blocklist.json when unblocking from the Blocklist view. (0.2.2)
→ Hotfixed on 05.08.2025 at 13:10 (UTC+2) directly in latest (0.2.3)
> - ✅ **Installer** should now ask if you want to delete and reclone repo when allready existing (0.3.1)
> - ✅ **Added FLOCK** to lock json files to not loose data when several write processes write at the same time (0.3.2)
-**Handling of "Increase Ban" Events** : will now processed correct by backend and is also visible in frontend via markers (0.4.0)
- Thanks to 👉 **`jbd7`** 👈 for reporting and debugging `issue #21` 👍.
-**Copy to Clipboard** cannot copy the list when filtered by markers (0.5.0)
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 👀 Outlook
> next minor releases will focus on stability, security and usability, wider integration of authentication (Aka Loginpage) and smoother sync cycle, next major releases will focus more on statistics, integration of other firewalls and more fail2ban integration
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 🖼️ Screenshots
### Main List
![Main-List](screenshots/Main-List-050-3.png)
### Blocklist View
![Blocklist](screenshots/Block-List-050-3.png)
### Information Message
![Info-Message](screenshots/Info-050-2.png)
### Security Message (new)
![Admin-Message](screenshots/admin-msg-050-2.png)
### Ban Message
![Ban-Message](screenshots/Ban-Msg-050-2.png)
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 👥 Discussions
> If you want to join the conversation or have questions or ideas, visit the 💬 [Discussions page](https://github.com/SubleXBle/Fail2Ban-Report/discussions).
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 🆕 What's New in V 0.4.0
### 🧱 Firewall & JSON
- Optimized `firewall-update.sh` for faster batch processing of IPs.
- Batch blocking per jail with a single `ufw reload`.
- Safe unblocking with rule renumbering and reload after each deletion.
- JSON updates and cleanup done once per jail, not per IP.
- Core mechanisms, logging, and permissions unchanged.
> This significantly reduces both the runtime and the lock duration of the blocklists, especially during ban events.
### 🖥️ UI & Statistics
- Minor visual improvements in:
- `header.php`, `fail2ban-logstats.php`, `fail2ban-logstats.js`
- `index.php` (IP sorting)
- `style.css`
### 🟡🔴 Marker Feature
- **IP Event Markers**: Highlights repeated events per IP (yellow) and IPs in multiple jails (red).
- **Sortable & Filterable Mark Column**: New column `Mark` with dropdown filter.
- **Dynamic Filtering**: Markers update live with Action, Jail, IP, or Date filters.
- Marker column placed between Action and IP, responsive layout preserved.
### ✨ New Feature: Copy Filtered Data to Clipboard
- **Added** a new "Copy to Clipboard" button to export the currently **filtered table data**.
- **Implemented** a dedicated JavaScript file `assets/js/table-export.js` for the copy functionality.
- **Integration** with existing DataTables filtering logic to ensure only visible/filtered rows are copied.
- **Output Format**: Tab-separated values (TSV) with all HTML tags removed for clean text export.
- **User Feedback**:
- Shows a warning if theres no data to copy.
- Shows a success or error alert based on the clipboard operation result.
> This Feature will only work with enabled https for security reasons
---
### ⚠️ Upgrade Notice
If you're upgrading from an existing installation : from 0.3.3 and later
1. 📦 Replace all or changed files with the new version (overwrite).
2. 👀 List of changed files: ![changelog.md#list-of-changed-files](changelog.md#list-of-changed-files)
3. 📦 make sure new shellscripts are executable
4. 🛠️ Control needed Paths
5. 🛠️ Control .htaccess
If you're upgrading from an existing installation : pre 0.3.2 and also from 0.3.2
- ⚠️ **The new blocklist format is not compatible with the old `blocklist.json`.** and got new field `pending` is in json since 0.3.3
- 🧹 To ensure a clean transition and avoid orphaned firewall entries, follow these steps:
1. **Empty your current blocklist** via the **Unblock** buttons in the UI.
2. 🔄 Trigger a **sync** using the `firewall-update.sh` to remove all Fail2Ban-Report-related rules from the firewall.
3. 🗑️ Delete the old `blocklist.json`.
4. 📦 Replace all files with the new version (overwrite).
5. ✅ Done! The new system will now build jail-specific blocklists automatically.
- 🛠️ _Optional_ : Run the `installer.sh` again to get a fresh setup.
> This ensures no leftover blocks remain in your firewall from the previous system.
---
## 📄 Changelog
Details about all new features, improvements, and changed files can be found in the [Changelog](changelog.md).
This is especially useful if you want to manually patch or update individual files.
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 🪳 Bugfixes
> - Found a bug? → [Open an issue](https://github.com/SubleXBle/Fail2Ban-Report/issues)
## ⚡ Performance & Stress Test
-**Date filter** now correctly limits displayed events
-**Jail filter** now correctly shows only the jails present in the displayed event list.
-**File date filtering** fix to include today's JSON logs and ensure latest files are listed correctly.
-**Blocklist Path on unblocking** fixed a possible bug that could lead to not finding the blocklist.json when unblocking from the Blocklist view.
→ Hotfixed on 05.08.2025 at 13:10 (UTC+2) directly in latest
-**Installer** should now ask if you want to delete and reclone repo when allready existing
-**Added FLOCK** to lock json files to not loose data when several write processes write at the same time
Fail2Ban-Report has been tested under high-load conditions to verify stability, responsiveness, and reliable synchronization across multiple servers.
**Real Scenario:**
- **Duration: ~10 minutes**
- **Webserver events:** ~13,400 entries across several jails (mostly SSH)
- **Data per event:** date, action, marker, IP, jail
**Key Results:**
- Since each ban is triggered after 4 failed attempts, the actual number of incoming requests corresponds to roughly 53,600 login attempts over 10 minutes → about 5,360 requests per minute (≈ 89 requests per second).
- The WebUI loads all 13,480 daily JSON entries in about 1.5 seconds.
- Connected clients consistently pulled and pushed blocklists without any delay. Even when a blocklist update included 80+ new IP entries, the synchronization completed in a blink of an eye, with changes applied in both directions instantly.
- Switching between multiple servers in the dashboard remains smooth, typically under 2 seconds, even during attacks.
**Takeaway:**
Fail2Ban-Report maintains fast performance and reliable data synchronization, proving its suitability for multi-server setups and high-frequency event environments.
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 🛣️ Roadmap
### 🔧 Setup & Automation
- ✅ Automated installer script
- ✅ Optional cron setup for log parsing and firewall sync
- 🧩 More robust installer
- ⏳ Secure-by-default deployments
## 🛣️ Roadmap or "Things I will have to do - but I do them later"
### 🔐 Security
- ✅ Hardened `.htaccess` with best practices
- ✅ add security layer between json and js
- ⏳ Further improvements (ongoing goal)
> I gave up the usual Roadmap - to have more freedom with development - Things like Multiserver was never on the Roadmap but allways in my mind.
### 🔥 Active Defense
- ✅ Manual IP blocking via UI in UFW
- ✅ IP reputation lookup via AbuseIPDB (optional)
- ✅ IP GeoLoc and Provider Data with IP-Info (optional)
- ✅ Bulk blocking of multiple IPs
- ✅ Shows warnings/critical states threshold for Bans/Minute/Jail (setable in config)
- ✅ Shows warning states for Ips that are more than once on List
- ✅ Shows critical states for IPs that are in more than one Jail in List
- 🧩 Support for nftables, firewalld
- ⏳ LTG: Integration with external services (e.g. AbuseIPDB reporting)
- ⏳ LTG: Integration with fail2ban-jails directly
- ⏳ Rework Blocklist Overlay
- ⏳ Rework Stylesheet
- ⏳ Rework Info Notices
### 🌿 User Interface
- ⏳ Improve CSS and styling
> As I am using Fail2Ban-Report I think it has a lot of potential to become something nice for not just myself.
## 👀 Outlook
- 📦 Further Improvements & Security Enhancements
- Moving `archive/` to `/opt` makes little sense if `www-data` still needs access.
- Working on a solution to authorize changes made to JSON files via the web interface.
- 🐳 A Docker image is expected probably around version v0.5.x
> Suggestions and Ideas still welcome at any time (see Discussions) - When you are using Fail2Ban-Report and you think "I would need to see .. " tell me, I am happy to see your Ideas!
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 🖼️ Screenshots
![Main interface with log overview](assets/images/Main-List-040.png)
![Blocklist interface with unblock actions](assets/images/Block-List-033.png)
![Result after banning an IP](assets/images/Message-Toast-033.png)
![Result after Info](assets/images/Info-Msg-033.png)
---
## 🖥️ Demo
👀 Want to try out the look & feel?
There's a simple demo version available here no backend, no real data:
👉 https://suble.net/ 🔗
---
## ✅ What It Is
- A **read-only + action-enabled** web dashboard for Fail2Ban events
- A tool to **visualize** bans/unbans and **manually** manage blocked IPs
- A **log parser + JSON generator** that works alongside your existing Fail2Ban setup
- A way to **sync a persistent blocklist** with your firewall (currently **UFW only**)
- Designed for **sysadmins** who want quick insights without SSH-ing into the server
## ❌ What It Is Not
- ❌ A replacement for **Fail2Ban** itself (it depends on Fail2Ban)
- ❌ A real-time IDS/IPS (data updates depend on log parsing intervals)
- ❌ A universal firewall manager (no native support for iptables/nftables, etc. — yet)
- ❌ A tool for **automatic** jail management (manual actions only for now)
- ❌ A heavy analytics platform — its lightweight and log-driven by design
## 🐳 Docker Version
> Development of [Docker Version of Fail2Ban-Report](https://github.com/SubleXBle/Fail2Ban-Report-Docker) is expected with 0.5.1
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 🤝 Contributing
@@ -249,10 +329,14 @@ Pull requests, feature ideas and bug reports are very welcome!
> 💡 “Wouldnt it be cool if it could also do XYZ?”
> Absolutely — Im happy to hear your ideas.
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---
## 📄 License
This project is licensed under the **GPLv3**.
Feel free to use, modify and share — but please respect the license terms.
##### [↑ Table of Contents ↑](#-Table-of-Contents)
---

View File

@@ -1,190 +0,0 @@
# 🔧 Fail2Ban-Report Beta 2 Manual Setup Instructions
These instructions explain how to manually install and configure **Fail2Ban-Report v2** on a Linux system.
---
## ✅ Requirements
- A Linux system (tested with debian only) with the following installed:
- `fail2ban`
- `jq`
- `ufw` (only UFW is supported at this time)
- A PHP-enabled web server (e.g. Apache with PHP 7.4+)
- The web server user (e.g. `www-data`) must have write access to the `/archive/` directory
---
## 📁 Project Structure
Place the project in your desired web directory, for example:
/var/www/html/Fail2Ban-Report/
The structure should look like this:
Fail2Ban-Report/
├── assets/
│ ├── css/style.css
│ ├── images/*.png
│ └── js/*.js
├── includes/
│ ├── actions/*.php
│ ├── block-ip.php
│ ├── unblock-ip.php
│ ├── list-files.php
│ └── footer.php
├── archive/ ← must be writable by web server
├── index.php
├── .htaccess
├── README.md
├── Setup-Instructions.md
└── (Shell scripts stored outside the web root)
---
## 🔐 Permissions
Make the `/archive/` directory writable for the web server:
chown -R www-data:www-data /var/www/html/Fail2Ban-Report/
find /var/www/html/Fail2Ban-Report/ -type d -exec chmod 755 {} \;
find /var/www/html/Fail2Ban-Report/ -type f -exec chmod 644 {} \;
---
## ⚙️ Shell Scripts
The following two shell scripts **must not** be placed inside the web root.
Recommended path: `/opt/Fail2Ban-Report/`
- `fail2ban_log2json.sh`
- `firewall-update.sh`
Adjust paths in these scripts if necessary:
- `fail2ban_log2json.sh` reads the Fail2Ban log and writes JSON files to `/archive/` (archive/ is a folder placed in the Webspace of /Fail2Ban-Report/
- `firewall-update.sh` reads `blocklist.json` and syncs it with UFW (blocks/unblocks) so it also needs the path to `/archive/`
> Make sure both scripts are executable (`chmod +x`)
---
## 🕒 Cronjob Configuration
Set up two cronjobs:
1. Convert logs to JSON every 515 minutes:
*/5 * * * * root /opt/Fail2Ban-Report/fail2ban_log2json.sh
2. Sync firewall blocklist with UFW every 515 minutes:
*/5 * * * * root /opt/Fail2Ban-Report/firewall-update.sh
> Make sure both scripts are executable (`chmod +x`)
---
## 🌐 Web Interface Configuration
- No PHP configuration is required.
- All scripts in `includes/` and `includes/actions/` work without manual changes.
- The web interface displays log information and lets you:
- View ban history
- Block/unblock IPs manually
- Manage the `blocklist.json` interactively
---
## 🔒 Security Notes
The `.htaccess` file includes:
- Protection against direct access to:
- `.json`, `.sh`, `.ini`, `.log`, `.bak`, `.OLD`
- Rewrite rules for `archive/*.json` and `includes/*.php`
- Strong HTTPS headers
- (Optional) examples for basic authentication and IP restrictions (commented)
Make sure your Apache server honors `.htaccess`, and you enable `mod_rewrite`.
---
## ✅ Setup Complete
You can now access the tool at:
http(s)://yourdomain.tld/Fail2Ban-Report/
Monitor your logs, manage bans, and secure your system visually and efficiently.
---
## 🌐 Optional: AbuseIPDB Integration
Fail2Ban-Report supports an optional IP reputation check and reporting via [AbuseIPDB](https://www.abuseipdb.com/), a public threat intelligence platform.
### 🔎 What It Does
- Displays how often an IP has been reported on AbuseIPDB
- Allows you to manually report abusive IPs directly from the interface
- Helps assess the trustworthiness of IP addresses before unblocking them
### 🧰 Requirements
- A free account at [AbuseIPDB.com](https://www.abuseipdb.com/)
- A personal API key (available in your [AbuseIPDB dashboard](https://www.abuseipdb.com/account/api))
### 🛠 Configuration
1. Create a config file **outside the web root**, e.g. at:
```
/opt/Fail2Ban-Report/fail2ban-report.config
```
2. Add the following content (replace the API key placeholder):
```ini
[reports]
report=true
report_types=abuseipdb
[AbuseIPDB API Key]
abuseipdb_key=YOUR_API_KEY_HERE
```
3. Ensure the config file is **readable** by the web server (e.g. `www-data`):
```bash
chown www-data:www-data /opt/Fail2Ban-Report/fail2ban-report.config
chmod 640 /opt/Fail2Ban-Report/fail2ban-report.config
```
4. Done — if the file exists and is valid, the web interface will show AbuseIPDB info and allow reporting.
### ⚠️ Custom Path or Filename
If you choose a different location or name for the config file:
- Open this file:
```
includes/actions/reports/abuseipdb.php
```
- Look for the line that defines the path:
```php
$configPath = '/opt/Fail2Ban-Report/fail2ban-report.config';
```
- Adjust it to match your actual file path.
> ⚠️ The config file must be placed **outside** the web root for security reasons.
### 💡 Notes
- The feature is **fully optional** — Fail2Ban-Report works fine without it.
- AbuseIPDB requests are made **server-side**, your API key is not exposed in the browser.
- Their free tier currently allows **1,000 requests per day**

View File

@@ -1,8 +1,10 @@
# ----------------------------------------------------
# Fail2Ban-Report Docker
# Fail2Ban-Report
# ----------------------------------------------------
# Do not modify up here !
# Upper Section is for Basic Security of Fail2Ban-Report
# you can edit the lower Section to meet your needs
# ----------------------------------------------------
# Disable directory listing
@@ -38,6 +40,8 @@ Options -Indexes
AddDefaultCharset utf-8
RewriteEngine On
# End of default Configuration
##########################################################
##########################################################
##########################################################
@@ -45,15 +49,22 @@ RewriteEngine On
# ----------------------------------------------------
# Set your own Rules to fit your needs down here
# Example for additonal configuration
# ----------------------------------------------------
# This is mandatory for a save setup!
# Never expose this to the internet without https and restrictions (auth / ip)
# ----------------------------------------------------
# ----------------------------------------------------
# Security Headers
# ----------------------------------------------------
# Add your own security headers or overrides below
# Header always set Referrer-Policy "no-referrer-when-downgrade"
# Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline';"
# ----------------------------------------------------
# BASIC HTTPS HEADERS
# ----------------------------------------------------
# Redirect to HTTPS (also works behind reverse proxies using X-Forwarded-Proto)
# If HTTPS is not on AND X-Forwarded-Proto is not https, redirect to https URL
@@ -76,25 +87,26 @@ RewriteEngine On
# Set Basic Auth
# ----------------------------------------------------
# Enable Basic Authentication (uncomment and configure if needed)
#AuthType Basic
#AuthName "Restricted Area"
#AuthUserFile /var/www/.htpasswd
#Require valid-user
# Restrict access by IP address (adjust IP ranges accordingly)
# If you want to restrict access by IP addresses only, you can use RequireAny instead of RequireAll
# <RequireAll>
# <RequireAny>
# Require valid-user
# Require ip 192.168.1.1 # Single IP (recommended)
# Require ip 192.168.1.0/24 # Network-Range
# </RequireAll>
# </RequireAny>
# ----------------------------------------------------
# Additional Settings
# ----------------------------------------------------
# Enable Basic Authentication (uncomment and configure if needed)
# Block access to backup files (optional)
# <FilesMatch "\.(bak|old|backup)$">
# Require all denied
# </FilesMatch>
# Add your own security headers or overrides below
# Header always set Referrer-Policy "no-referrer-when-downgrade"
# Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline';"

View File

@@ -590,12 +590,67 @@ ul.toplist li {
/* Jail List Small Row */
.jaillistdiv {
display: flex;
flex-wrap: wrap;
gap: 10px;
white-space: nowrap;
animation: marquee 10s linear infinite;
}
.jaillist {
display: flex;
flex-wrap: wrap;
gap: 3px;
color: #aaa;
font-size: 0.7em;
display: inline-block;
}
.spacer1 {
height: 10px;
}
.sessionheader {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
padding: 0.5rem 1rem;
background-color: #111;
border-bottom: 0px solid #333;
}
.sessionheader h1 {
font-size: 1.8rem;
margin: 0;
}
.sessionheader h2 {
font-size: 1.2rem;
font-weight: normal;
margin: 0;
color: #aaa;
}
.sessionheader h3 {
font-size: 1.0rem;
margin: 0;
color: #d4af37;
}
.sessionheader h4 {
font-size: 1.0rem;
font-weight: normal;
margin: 0;
color: #aaa;
}
.inline-titles {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1rem;
}
.inline-titles h3,
.inline-titles h4 {
margin: 0;
}

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -35,8 +35,7 @@ function collectAndExecuteActions(ips, action, jails = []) {
const prefix = `[${action.toUpperCase()}] `;
const message = data.message || 'No message returned.';
const type = data.type || (data.success ? 'success' : 'error');
// when Notification - display 10 Seconds
const duration = (action === 'report') ? 10000 : 5000;
const duration = (action === 'report') ? 7000 : 7000;
showNotification(prefix + message, type, duration);
})
.catch(err => {

View File

@@ -1,8 +1,7 @@
// const availableFiles = <?php echo $filesJson; ?>;
const jsonProxyEndpoint = 'includes/get-json.php?file=';
let currentSort = { column: 'timestamp', direction: 'desc' }; // default newest first
let allData = []; // cached data of current JSON file
let currentFilename = ''; // current loaded filename
let currentSort = { column: 'timestamp', direction: 'desc' };
let allData = [];
let currentFilename = '';
function formatDateFromFilename(filename) {
const dateStr = filename.match(/(\d{4})(\d{2})(\d{2})/);
@@ -46,8 +45,17 @@ function renderTable() {
const ipFilter = document.getElementById('ipFilter').value.trim();
const markFilter = document.getElementById('markFilter').value;
// === Step 1: Filter by base criteria ===
const filtered = allData.filter(entry => {
// --- Step 1: Count "Increase Ban" per IP ---
const increaseBanCount = {};
allData.forEach(e => {
if (e.action === 'Increase Ban') {
increaseBanCount[e.ip] = (increaseBanCount[e.ip] || 0) + 1;
}
});
// --- Step 2: Filter only Ban/Unban events and base criteria ---
let filtered = allData.filter(e => (e.action === 'Ban' || e.action === 'Unban'));
filtered = filtered.filter(entry => {
const entryDate = entry.timestamp ? entry.timestamp.substring(0, 10) : '';
return (!selectedDate || entryDate === selectedDate) &&
(!actionFilter || entry.action === actionFilter) &&
@@ -55,7 +63,7 @@ function renderTable() {
(!ipFilter || entry.ip.includes(ipFilter));
});
// === Step 2: Prepare marker counts ===
// --- Step 3: Prepare counts for repeated events ---
const eventCounts = {};
const ipJails = {};
filtered.forEach(e => {
@@ -66,18 +74,26 @@ function renderTable() {
ipJails[e.ip].add(e.jail);
});
// === Step 3: Assign marker field ===
// --- Step 4: Assign markers ---
filtered.forEach(e => {
let marker = '';
if (eventCounts[e.ip + '|' + e.action] > 1) marker += '🟡'; // multiple same event
if (ipJails[e.ip].size > 1) marker += '🔴'; // multiple jails
if (!marker) marker = ''; // grey dot if no marker
// Red marker for repeated Ban/Unban
if (eventCounts[e.ip + '|' + e.action] > 1) marker += '🔴';
// Yellow marker if Increase Ban exists for IP
if (increaseBanCount[e.ip]) marker += '🟡';
// Append number of Increase Ban events
if (increaseBanCount[e.ip]) marker += ` (${increaseBanCount[e.ip]})`;
if (!marker) marker = '⚪';
e.marker = marker;
});
// === Step 4: Apply marker filter ===
// --- Step 5: Apply marker filter ---
const filteredWithMarker = filtered.filter(e => {
if (!markFilter) return true; // All
if (!markFilter) return true;
if (markFilter === 'yellow') return e.marker.includes('🟡') && !e.marker.includes('🔴');
if (markFilter === 'red') return e.marker.includes('🔴') && !e.marker.includes('🟡');
if (markFilter === 'yellowred') return e.marker.includes('🟡') && e.marker.includes('🔴');
@@ -85,7 +101,7 @@ function renderTable() {
return true;
});
// === Step 5: Rebuild jail dropdown ===
// --- Step 6: Rebuild jail dropdown ---
const jailSelect = document.getElementById('jailFilter');
const previousSelection = jailSelect.value;
jailSelect.innerHTML = '';
@@ -107,7 +123,7 @@ function renderTable() {
return;
}
// === Step 6: Sorting ===
// --- Step 7: Sorting ---
const sorted = [...filteredWithMarker].sort((a, b) => {
const { column, direction } = currentSort;
let valA = a[column] || '';
@@ -126,14 +142,14 @@ function renderTable() {
return 0;
});
// === Step 7: Table header sort arrows ===
// --- Step 8: Header sort arrows ---
document.querySelectorAll('#resultTable thead th[data-sort]').forEach(th => {
const col = th.getAttribute('data-sort');
const arrow = col === currentSort.column ? (currentSort.direction === 'asc' ? ' ⮝' : ' ⮟') : '';
th.textContent = th.getAttribute('data-label') + arrow;
});
// === Step 8: Render table ===
// --- Step 9: Render table ---
tbody.innerHTML = '';
sorted.forEach(entry => {
const row = document.createElement('tr');
@@ -155,14 +171,14 @@ function getSelectedDate() {
return dateMatch ? `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}` : null;
}
// === Event listeners for filters ===
// === Event listeners for filters
document.getElementById('dateSelect').addEventListener('change', e => loadDataAndRender(e.target.value));
document.getElementById('actionFilter').addEventListener('change', renderTable);
document.getElementById('jailFilter').addEventListener('change', renderTable);
document.getElementById('ipFilter').addEventListener('input', renderTable);
document.getElementById('markFilter').addEventListener('change', renderTable);
// === Event listeners for sorting ===
// === Event listeners for sorting
document.querySelectorAll('#resultTable thead th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
const column = th.getAttribute('data-sort');
@@ -176,5 +192,5 @@ document.querySelectorAll('#resultTable thead th[data-sort]').forEach(th => {
});
});
// === Initial loading ===
// Initial loading
populateDateDropdown();

20
Web-UI/endpoint/.htaccess Normal file
View File

@@ -0,0 +1,20 @@
# ================================
# Fail2Ban-Report Endpoint
# ================================
# Directory Listing
Options -Indexes
# Block all but index.php and update.php
<FilesMatch "^(?!(?:index|update|download|backsync)\.php$).*">
Require all denied
</FilesMatch>
# Optional: IP-Whitelist for Sync-Clients
# --------------------------------
#
# <RequireAny>
# Require ip 192.168.1.10 # Singe Address
# Require ip 203.0.113.0/24 # Network
# </RequireAny>

View File

@@ -0,0 +1,130 @@
<?php
// backsync.php Receives updated blocklists from the client and synchronizes them on the server
// === Configuration ===
$CLIENTS_FILE = "/opt/Fail2Ban-Report/Settings/client-list.json";
$ARCHIVE_BASE = __DIR__ . "/../archive/";
$TEMP_UPLOAD_DIR = sys_get_temp_dir() . "/backsync/";
// === Helper function for JSON responses ===
function respond($statusCode, $data) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data, JSON_PRETTY_PRINT);
exit;
}
// === 1) Load clients ===
if (!file_exists($CLIENTS_FILE)) {
respond(500, ["success" => false, "message" => "Client list not found."]);
}
$clients = json_decode(file_get_contents($CLIENTS_FILE), true);
if (!is_array($clients)) {
respond(500, ["success" => false, "message" => "Client list corrupted."]);
}
// === 2) Authenticate client ===
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$uuid = $_POST['uuid'] ?? '';
$remoteIp = $_SERVER['REMOTE_ADDR'] ?? '';
$client = null;
foreach ($clients as $c) {
if ($c['username'] === $username && $c['uuid'] === $uuid) {
$client = $c;
break;
}
}
if (!$client) respond(403, ["success" => false, "message" => "Authentication failed (user/uuid)."]);
if (!password_verify($password, $client['password'])) {
respond(403, ["success" => false, "message" => "Authentication failed (password)."]);
}
if (isset($client['ip']) && $client['ip'] !== $remoteIp) {
respond(403, ["success" => false, "message" => "Authentication failed (IP mismatch)."]);
}
// === 3) Prepare temp upload directory ===
$userTempDir = $TEMP_UPLOAD_DIR . $username . "/";
if (!is_dir($userTempDir)) mkdir($userTempDir, 0770, true);
// === 4) Receive files and store them temporarily ===
if (!isset($_FILES['file'])) {
respond(400, ["success" => false, "message" => "No files uploaded."]);
}
$uploadedFiles = [];
foreach ($_FILES['file']['name'] as $index => $name) {
$tmpName = $_FILES['file']['tmp_name'][$index];
if (!is_uploaded_file($tmpName)) continue;
if (!preg_match('/^[a-z0-9_-]+\.blocklist\.json$/i', $name)) continue;
$destPath = $userTempDir . basename($name);
if (!move_uploaded_file($tmpName, $destPath)) continue;
$uploadedFiles[] = $name;
}
// If no valid files, respond early
if (empty($uploadedFiles)) {
respond(400, ["success" => false, "message" => "No valid blocklist files uploaded."]);
}
// === 5) Process files immediately, one by one ===
$processedFiles = [];
foreach ($uploadedFiles as $file) {
$tempFile = $userTempDir . $file;
$targetDir = $ARCHIVE_BASE . $username . "/blocklists/";
if (!is_dir($targetDir)) mkdir($targetDir, 0770, true);
$targetFile = $targetDir . $file;
// Acquire lock on the target blocklist file
$blockLockFile = "/tmp/{$username}_{$file}.lock";
$blockHandle = fopen($blockLockFile, 'c');
if (!$blockHandle || !flock($blockHandle, LOCK_EX)) {
continue; // skip this file if lock cannot be acquired
}
// Replace old file with the new one
copy($tempFile, $targetFile);
// Acquire lock on update.json
$updateFile = $ARCHIVE_BASE . "update.json";
$updateLockFile = "/tmp/update.json.lock";
$updateHandle = fopen($updateLockFile, 'c');
if ($updateHandle && flock($updateHandle, LOCK_EX)) {
$updateData = [];
if (file_exists($updateFile)) {
$updateData = json_decode(file_get_contents($updateFile), true);
if (!is_array($updateData)) $updateData = [];
}
// Remove the entry for this blocklist
if (isset($updateData[$username][$file])) {
unset($updateData[$username][$file]);
file_put_contents($updateFile, json_encode($updateData, JSON_PRETTY_PRINT));
}
flock($updateHandle, LOCK_UN);
fclose($updateHandle);
}
flock($blockHandle, LOCK_UN);
fclose($blockHandle);
// Delete temp file
unlink($tempFile);
$processedFiles[] = $file;
}
// Optional: clean up temp directory if empty
if (is_dir($userTempDir)) rmdir($userTempDir);
// === 6) Respond to client after all processing ===
respond(200, [
"success" => true,
"message" => "All files processed successfully.",
"files" => $processedFiles
]);

View File

@@ -0,0 +1,70 @@
<?php
// download.php Authenticated Blocklist-Download (one File per Request)
// === Configuration ===
$CLIENTS_FILE = "/opt/Fail2Ban-Report/Settings/client-list.json";
$BLOCKLIST_BASE = __DIR__ . "/"; // Basis-Pfad zu den Blocklists
// --- Helpfunction for JSON-Answer ---
function respond($statusCode, $data) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
exit;
}
// === 1) load Clients ===
if (!file_exists($CLIENTS_FILE)) {
respond(500, ["success" => false, "message" => "Client list not found."]);
}
$clients = json_decode(file_get_contents($CLIENTS_FILE), true);
if (!is_array($clients)) {
respond(500, ["success" => false, "message" => "Client list corrupted."]);
}
// === 2) Authentication ===
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$uuid = $_POST['uuid'] ?? '';
$remoteIp = $_SERVER['REMOTE_ADDR'] ?? '';
$client = null;
foreach ($clients as $c) {
if ($c['username'] === $username && $c['uuid'] === $uuid) {
$client = $c;
break;
}
}
if (!$client) {
respond(403, ["success" => false, "message" => "Authentication failed (user/uuid)."]);
}
if (!password_verify($password, $client['password'])) {
respond(403, ["success" => false, "message" => "Authentication failed (password)."]);
}
if (isset($client['ip']) && $client['ip'] !== $remoteIp) {
respond(403, ["success" => false, "message" => "Authentication failed (ip mismatch)."]);
}
// === 3) set file ===
$fileToDownload = $_GET['file'] ?? '';
if (!$fileToDownload) {
respond(400, ["success" => false, "message" => "Missing 'file' parameter."]);
}
$userBlocklistDir = $BLOCKLIST_BASE . $username . "/blocklists/";
$fullPath = realpath($userBlocklistDir . $fileToDownload);
if (!$fullPath || strpos($fullPath, realpath($userBlocklistDir)) !== 0 || !file_exists($fullPath)) {
respond(404, ["success" => false, "message" => "Requested blocklist not found."]);
}
// === 4) deliver file ===
header('Content-Type: application/json');
header('Content-Disposition: attachment; filename="' . basename($fullPath) . '"');
header('Content-Length: ' . filesize($fullPath));
readfile($fullPath);
// === 5) delete file when downloaded ===
unlink($fullPath);
exit;

167
Web-UI/endpoint/index.php Normal file
View File

@@ -0,0 +1,167 @@
<?php
// index.php Endpoint für Fail2Ban-Report Event-Lists
// === Config ===
$CLIENTS_FILE = "/opt/Fail2Ban-Report/Settings/client-list.json";
$ARCHIVE_BASE = __DIR__ . "/archive/"; // anpassen falls nötig
header('Content-Type: application/json');
// === Helper: Response-function ===
function respond($statusCode, $data) {
http_response_code($statusCode);
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
exit;
}
// === 1) Authentcation ===
if (!file_exists($CLIENTS_FILE)) {
respond(500, ["success" => false, "message" => "Client list not found."]);
}
$clients = json_decode(file_get_contents($CLIENTS_FILE), true);
if (!is_array($clients)) {
respond(500, ["success" => false, "message" => "Client list corrupted."]);
}
// Data from Request
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$uuid = $_POST['uuid'] ?? '';
$remoteIp = $_SERVER['REMOTE_ADDR'] ?? '';
// search Client
$client = null;
foreach ($clients as $c) {
if ($c['username'] === $username && $c['uuid'] === $uuid) {
$client = $c;
break;
}
}
if (!$client) {
respond(403, ["success" => false, "message" => "Authentication failed (user/uuid)."]);
}
if (!password_verify($password, $client['password'])) {
respond(403, ["success" => false, "message" => "Authentication failed (password)."]);
}
if (isset($client['ip']) && $client['ip'] !== $remoteIp) {
respond(403, ["success" => false, "message" => "Authentication failed (ip mismatch)."]);
}
// === 2) Check files ===
if (!isset($_FILES['file'])) {
respond(400, ["success" => false, "message" => "No file uploaded."]);
}
$uploadedFile = $_FILES['file']['tmp_name'];
$originalName = $_FILES['file']['name'];
if (!is_uploaded_file($uploadedFile)) {
respond(400, ["success" => false, "message" => "Invalid upload."]);
}
// get type
$isEvents = preg_match('/^fail2ban-events-\d+\.json$/', $originalName);
$isBlocklist = preg_match('/^[a-z0-9_-]+\.blocklist\.json$/i', $originalName);
if (!$isEvents && !$isBlocklist) {
respond(400, ["success" => false, "message" => "Invalid filename: $originalName"]);
}
// === 3) prepare archive path ===
$userArchive = $ARCHIVE_BASE . $username . "/";
$targetDir = $userArchive . ($isEvents ? "fail2ban/" : "blocklists/");
if (!is_dir($targetDir) && !mkdir($targetDir, 0770, true)) {
respond(500, ["success" => false, "message" => "Failed to create target directory."]);
}
$targetFile = $targetDir . $originalName;
// === 4) Fail2Ban Events → overwrite them ===
if ($isEvents) {
if (!move_uploaded_file($uploadedFile, $targetFile)) {
respond(500, ["success" => false, "message" => "Failed to save events file."]);
}
chown($targetFile, "root");
chgrp($targetFile, "www-data");
respond(200, ["success" => true, "message" => "Events file stored."]);
}
// === 5) process eventlists ===
$newData = json_decode(file_get_contents($uploadedFile), true);
if (!is_array($newData)) {
respond(400, ["success" => false, "message" => "Invalid JSON in upload."]);
}
$lockFile = "/tmp/" . $originalName . ".lock";
$lockHandle = fopen($lockFile, 'c');
if (!$lockHandle || !flock($lockHandle, LOCK_EX)) {
respond(500, ["success" => false, "message" => "Could not acquire lock."]);
}
// load old Data
$archiveData = [];
if (file_exists($targetFile)) {
$archiveData = json_decode(file_get_contents($targetFile), true);
if (!is_array($archiveData)) {
$archiveData = [];
}
}
// Compare & Update
$changed = false;
foreach ($archiveData as $key => &$entry) {
if (!isset($entry['ip'])) continue;
$ip = $entry['ip'];
// Case 1: active=true & pending=true -> pending=false when in new List with pending=false
if ($entry['active'] === true && $entry['pending'] === true) {
foreach ($newData as $n) {
if ($n['ip'] === $ip && $n['pending'] === false) {
$entry['pending'] = false;
$entry['lastModified'] = date('c');
$changed = true;
break;
}
}
}
// Case 2: active=false & pending=true -> delete if not in List anymore
if ($entry['active'] === false && $entry['pending'] === true) {
$found = false;
foreach ($newData as $n) {
if ($n['ip'] === $ip) {
$found = true;
break;
}
}
if (!$found) {
unset($archiveData[$key]);
$changed = true;
}
}
}
unset($entry);
// Save
if ($changed) {
if (file_put_contents($targetFile, json_encode(array_values($archiveData), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) === false) {
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
respond(500, ["success" => false, "message" => "Failed to write blocklist."]);
}
} else {
// if file is new -> save it
if (!file_exists($targetFile)) {
if (!move_uploaded_file($uploadedFile, $targetFile)) {
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
respond(500, ["success" => false, "message" => "Failed to create blocklist."]);
}
}
}
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
chown($targetFile, "root");
chgrp($targetFile, "www-data");
respond(200, ["success" => true, "message" => "Blocklist updated."]);

111
Web-UI/endpoint/update.php Normal file
View File

@@ -0,0 +1,111 @@
<?php
// update.php Checks Blocklist-Updates for Sync-Client and provides Blocklists for Download
header('Content-Type: application/json');
// === Config ===
$CLIENTS_FILE = "/opt/Fail2Ban-Report/Settings/client-list.json";
$ARCHIVE_ROOT = __DIR__ . "/../archive/";
$DOWNLOAD_ROOT = __DIR__; // /endpoint/<username>/blocklists/
// === Helper: Antwortfunktion ===
function respond($statusCode, $data) {
http_response_code($statusCode);
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
exit;
}
// === 1) Authentication ===
if (!file_exists($CLIENTS_FILE)) {
respond(500, ["success" => false, "message" => "Client list not found."]);
}
$clients = json_decode(file_get_contents($CLIENTS_FILE), true);
if (!is_array($clients)) {
respond(500, ["success" => false, "message" => "Client list corrupted."]);
}
// POST-Data
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$uuid = $_POST['uuid'] ?? '';
$remoteIp = $_SERVER['REMOTE_ADDR'] ?? '';
// search Client
$client = null;
foreach ($clients as $c) {
if ($c['username'] === $username && $c['uuid'] === $uuid) {
$client = $c;
break;
}
}
if (!$client) {
respond(403, ["success" => false, "message" => "Authentication failed (user/uuid)."]);
}
if (!password_verify($password, $client['password'])) {
respond(403, ["success" => false, "message" => "Authentication failed (password)."]);
}
if (isset($client['ip']) && $client['ip'] !== $remoteIp) {
respond(403, ["success" => false, "message" => "Authentication failed (ip mismatch)."]);
}
// === 2) Check if Updates are available for this Client ===
$updateFile = $ARCHIVE_ROOT . 'update.json';
$update_data = [];
if (file_exists($updateFile)) {
$update_data = json_decode(file_get_contents($updateFile), true);
if (!is_array($update_data)) $update_data = [];
}
$clientUpdates = $update_data[$username] ?? [];
$updatesToSend = array_filter($clientUpdates, fn($v) => $v === true);
if (empty($updatesToSend)) {
respond(200, ["success" => true, "updates" => []]);
}
// === 3) Preparing temporary download path ===
$tempDir = $DOWNLOAD_ROOT . "/{$username}/blocklists/";
if (!is_dir($tempDir)) mkdir($tempDir, 0770, true);
// === 4) provide Blocklists & set Status to false ===
$sentLists = [];
foreach ($updatesToSend as $listName => $_) {
$sourceFile = $ARCHIVE_ROOT . "$username/blocklists/$listName";
$destFile = $tempDir . $listName;
// Lock Blocklist
$blockLock = "/tmp/{$listName}.lock";
$blockHandle = fopen($blockLock, 'c');
if (!$blockHandle || !flock($blockHandle, LOCK_EX)) {
continue; // logging ?
}
if (file_exists($sourceFile)) {
copy($sourceFile, $destFile);
$sentLists[] = $listName;
// Status in update.json to false → Lock only at write operations
$updateLock = "/tmp/update.json.lock";
$updateHandle = fopen($updateLock, 'c');
if ($updateHandle && flock($updateHandle, LOCK_EX)) {
$update_data[$username][$listName] = false;
file_put_contents($updateFile, json_encode($update_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
flock($updateHandle, LOCK_UN);
fclose($updateHandle);
}
}
flock($blockHandle, LOCK_UN);
fclose($blockHandle);
}
// === 5) answer to Sync-Client ===
respond(200, [
"success" => true,
"updates" => $sentLists
]);

View File

@@ -3,7 +3,7 @@
header('Content-Type: application/json; charset=utf-8');
$config = parse_ini_file('/opt/Fail2Ban-Report/fail2ban-report.config');
$config = parse_ini_file('/opt/Fail2Ban-Report/Settings/fail2ban-report.config');
$ips = $_POST['ip'] ?? null;
if (!$config['report'] || !$config['report_types'] || !$ips) {

View File

@@ -0,0 +1,84 @@
<?php
// includes/actions/reports/abuseipdb.php
require_once __DIR__ . '/../paths.php';
// Config laden
$config = parse_ini_file($PATHS['config'] . "fail2ban-report.config", true);
$apiKey = trim($config['AbuseIPDB API Key']['abuseipdb_key'] ?? '');
// IP aus POST
$ipToCheck = $_POST['ip'] ?? null;
if (!$apiKey) {
echo json_encode([
'success' => false,
'message' => 'AbuseIPDB API key not set.',
'type' => 'error'
]);
return;
}
if (!$ipToCheck) {
echo json_encode([
'success' => false,
'message' => 'No IP specified for AbuseIPDB check.',
'type' => 'error'
]);
return;
}
// API Call
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.abuseipdb.com/api/v2/check?ipAddress=$ipToCheck",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Key: $apiKey",
"Accept: application/json"
],
]);
$response = curl_exec($curl);
$curlError = curl_error($curl);
curl_close($curl);
if (!$response) {
echo json_encode([
'success' => false,
'message' => $curlError ?: 'AbuseIPDB request failed.',
'type' => 'error'
]);
return;
}
$json = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_encode([
'success' => false,
'message' => 'AbuseIPDB: Invalid JSON response.',
'type' => 'error',
'raw_response' => $response
]);
return;
}
$count = $json['data']['totalReports'] ?? null;
if ($count === null) {
echo json_encode([
'success' => false,
'message' => 'AbuseIPDB: Unexpected API response structure.',
'type' => 'error',
'raw_response' => $response
]);
return;
}
$msg = "AbuseIPDB: $ipToCheck was reported $count time(s).";
echo json_encode([
'success' => true,
'message' => $msg,
'type' => ($count >= 10) ? 'error' : (($count > 0) ? 'info' : 'success'),
'data' => $json
]);

View File

@@ -0,0 +1,70 @@
<?php
// includes/actions/reports/ipinfo.php
require_once __DIR__ . '/../paths.php';
// Config laden
$config = parse_ini_file($PATHS['config'] . "fail2ban-report.config", true);
$apiKey = trim($config['IP-Info API Key']['ipinfo_key'] ?? '');
// IP aus POST
$ipToCheck = $_POST['ip'] ?? null;
if (!$apiKey) {
echo json_encode([
'success' => false,
'message' => 'IPInfo API key not set.',
'type' => 'error'
]);
return;
}
if (!$ipToCheck) {
echo json_encode([
'success' => false,
'message' => 'No IP specified for IPInfo check.',
'type' => 'error'
]);
return;
}
// API Call
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://ipinfo.io/{$ipToCheck}/json?token={$apiKey}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Accept: application/json"],
]);
$response = curl_exec($curl);
$curlError = curl_error($curl);
curl_close($curl);
if (!$response) {
echo json_encode([
'success' => false,
'message' => $curlError ?: 'IPInfo request failed.',
'type' => 'error'
]);
return;
}
$json = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_encode([
'success' => false,
'message' => 'IPInfo: Invalid JSON response.',
'type' => 'error',
'raw_response' => $response
]);
return;
}
$msg = "IPInfo: {$json['ip'] ?? 'unknown'} - Hostname: {$json['hostname'] ?? 'N/A'}, Location: {$json['city'] ?? 'N/A'}, {$json['region'] ?? 'N/A'}, {$json['country'] ?? 'N/A'}, Org: {$json['org'] ?? 'N/A'}";
echo json_encode([
'success' => true,
'message' => $msg,
'type' => 'info',
'data' => $json
]);

109
Web-UI/includes/auth.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
// Start session (with secure cookie flags)
if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'httponly' => true,
'secure' => true,
'samesite' => 'Strict'
]);
session_start();
}
// Timeout / Lifetime / Regeneration Settings
$SESSION_TIMEOUT = 1800; // 30 Minutes inactivity
$SESSION_REGEN_INTERVAL = 900; // 15 Minutes for ID regeneration
$SESSION_MAX_LIFETIME = 7200; // 2 Hours absolute lifetime
// Check inactivity timeout
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > $SESSION_TIMEOUT)) {
session_unset();
session_destroy();
die("Session expired due to inactivity. Please log in again.");
}
$_SESSION['last_activity'] = time();
// Check absolute lifetime
if (!isset($_SESSION['created_at'])) {
$_SESSION['created_at'] = time();
} elseif (time() - $_SESSION['created_at'] > $SESSION_MAX_LIFETIME) {
session_unset();
session_destroy();
die("Session expired due to maximum lifetime. Please log in again.");
}
// Session ID regeneration
if (!isset($_SESSION['last_regeneration'])) {
$_SESSION['last_regeneration'] = time();
} elseif (time() - $_SESSION['last_regeneration'] > $SESSION_REGEN_INTERVAL) {
session_regenerate_id(true);
$_SESSION['last_regeneration'] = time();
}
// Client binding (User-Agent + partial IP)
$clientFingerprint = hash('sha256',
$_SERVER['HTTP_USER_AGENT'] .
substr($_SERVER['REMOTE_ADDR'], 0, strrpos($_SERVER['REMOTE_ADDR'], '.')) // IP ohne letztes Oktett
);
if (!isset($_SESSION['client_fingerprint'])) {
$_SESSION['client_fingerprint'] = $clientFingerprint;
} elseif ($_SESSION['client_fingerprint'] !== $clientFingerprint) {
session_unset();
session_destroy();
die("Session validation failed. Please log in again.");
}
// set Standard Role
if (!isset($_SESSION['user_role'])) {
$_SESSION['user_role'] = 'viewer';
}
// Load User-File
$USER_FILE= "/opt/Fail2Ban-Report/Settings/users.json";
$USERS = json_decode(file_get_contents($USER_FILE), true) ?: [];
// Logout
if (isset($_POST['logout'])) {
session_unset();
session_destroy();
setcookie(session_name(), '', time() - 3600, '/');
header("Location: " . $_SERVER['PHP_SELF']);
exit;
}
// sent Loginform?
if (isset($_POST['login_user']) && isset($_POST['login_pass'])) {
$user = $_POST['login_user'];
$pass = $_POST['login_pass'];
$loggedIn = false;
foreach ($USERS as $u) {
if ($u['username'] === $user && password_verify($pass, $u['password'])) {
// Login success -> Hold Session
session_regenerate_id(true);
$_SESSION['user_role'] = $u['role'];
$_SESSION['username'] = $u['username'];
$_SESSION['created_at'] = time();
$_SESSION['last_regeneration'] = time();
$_SESSION['client_fingerprint'] = hash('sha256',
$_SERVER['HTTP_USER_AGENT'] .
substr($_SERVER['REMOTE_ADDR'], 0, strrpos($_SERVER['REMOTE_ADDR'], '.'))
);
$loggedIn = true;
break;
}
}
if (!$loggedIn) {
error_log("Failed login for $user from " . $_SERVER['REMOTE_ADDR']);
die("Login failed");
}
}
// Admin-Check
function is_admin() {
return (isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'admin');
}
?>

View File

@@ -1,15 +1,18 @@
<?php
// includes/block-ip.php
/**
* Blocks one or multiple IP addresses by adding them to their jail-specific blocklist JSON files.
*
* @param string|array $ips IP address or array of IP addresses to block.
* @param string $jail Fail2Ban jail/context name (optional).
* @param string $source Who triggered the block (e.g. 'manual', 'report', etc.)
* @return array Result array or array of results with 'success', 'message' and 'type'.
*/
require_once __DIR__ . "/paths.php";
function blockIp($ips, $jail = 'unknown', $source = 'manual') {
if (!is_admin()) {
return [
'success' => false,
'message' => 'Unauthorized: Only admin can block IPs.',
'type' => 'error'
];
}
global $ARCHIVE_ROOT; // Archive-Root
$results = [];
if (!is_array($ips)) {
@@ -19,7 +22,6 @@ function blockIp($ips, $jail = 'unknown', $source = 'manual') {
foreach ($ips as $ip) {
$ip = trim($ip);
// Validate IP address format
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
$results[] = [
'ip' => $ip,
@@ -32,48 +34,42 @@ function blockIp($ips, $jail = 'unknown', $source = 'manual') {
// Sanitize jail name
$safeJail = strtolower(preg_replace('/[^a-z0-9_-]/', '', $jail));
if ($safeJail === '') {
$safeJail = 'unknown';
}
if ($safeJail === '') $safeJail = 'unknown';
$jsonFile = dirname(__DIR__, 1) . "/archive/{$safeJail}.blocklist.json";
$lockFile = "/tmp/{$safeJail}.blocklist.lock"; // same lock file name as shell script
// path to Blocklist
$jsonFile = $GLOBALS["PATHS"]["blocklists"] . $safeJail . ".blocklist.json";
$lockFile = "/tmp/{$safeJail}.blocklist.lock";
// Open lock file
$lockHandle = fopen($lockFile, 'c');
if (!$lockHandle) {
$results[] = [
'ip' => $ip,
'success' => false,
'message' => "Unable to open lock file for {$safeJail}.",
'message' => "[LOCK] Unable to open lock file for {$safeJail}.",
'type' => 'error'
];
continue;
}
// Acquire exclusive lock
if (!flock($lockHandle, LOCK_EX)) {
fclose($lockHandle);
$results[] = [
'ip' => $ip,
'success' => false,
'message' => "Could not acquire lock for {$safeJail}.",
'message' => "[LOCK] Could not acquire lock for {$safeJail}.",
'type' => 'error'
];
continue;
}
// Load existing JSON
// load used blocklist
$data = [];
if (file_exists($jsonFile)) {
$existing = file_get_contents($jsonFile);
$data = json_decode($existing, true);
if (!is_array($data)) {
$data = []; // fallback if file is corrupted
}
if (!is_array($data)) $data = [];
}
// Check if IP already exists
$found = false;
foreach ($data as &$item) {
if ($item['ip'] === $ip) {
@@ -81,23 +77,17 @@ function blockIp($ips, $jail = 'unknown', $source = 'manual') {
if (!isset($item['active']) || $item['active'] === false) {
$item['active'] = true;
$item['lastModified'] = date('c');
if (file_put_contents($jsonFile, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) === false) {
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
$results[] = [
'ip' => $ip,
'success' => false,
'message' => "Failed to write to {$safeJail}.blocklist.json.",
'type' => 'error'
];
continue 2;
}
file_put_contents($jsonFile, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
// --- Update update.json ---
updateClientJson($jsonFile, $safeJail);
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
$results[] = [
'ip' => $ip,
'success' => true,
'message' => "IP $ip was reactivated in {$safeJail}.blocklist.json.",
'message' => "IP $ip reactivated in {$safeJail}.blocklist.json.",
'type' => 'success'
];
continue 2;
@@ -107,7 +97,7 @@ function blockIp($ips, $jail = 'unknown', $source = 'manual') {
$results[] = [
'ip' => $ip,
'success' => true,
'message' => "IP $ip is already active in {$safeJail}.blocklist.json.",
'message' => "IP $ip already active in {$safeJail}.blocklist.json.",
'type' => 'info'
];
continue 2;
@@ -116,7 +106,7 @@ function blockIp($ips, $jail = 'unknown', $source = 'manual') {
}
unset($item);
// Add new entry if not found
// add new ip
if (!$found) {
$entry = [
'ip' => $ip,
@@ -132,33 +122,23 @@ function blockIp($ips, $jail = 'unknown', $source = 'manual') {
];
$data[] = $entry;
if (file_put_contents($jsonFile, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) === false) {
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
$results[] = [
'ip' => $ip,
'success' => false,
'message' => "Failed to write to {$safeJail}.blocklist.json.",
'type' => 'error'
];
continue;
}
file_put_contents($jsonFile, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
// --- Update update.json ---
updateClientJson($jsonFile, $safeJail);
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
$results[] = [
'ip' => $ip,
'success' => true,
'message' => "IP $ip was successfully added to {$safeJail}.blocklist.json.",
'message' => "IP $ip added to {$safeJail}.blocklist.json.",
'type' => 'success'
];
}
}
// Flatten result if only one entry
if (count($results) === 1) {
return $results[0];
}
if (count($results) === 1) return $results[0];
return [
'success' => true,
@@ -167,3 +147,35 @@ function blockIp($ips, $jail = 'unknown', $source = 'manual') {
'type' => 'success'
];
}
/**
* set Update-Flag in update.json in Archive-Root.
*
* @param string $blocklistFile Pfad der Blocklist, um Servernamen zu extrahieren
* @param string $jail Blocklist-Name
*/
function updateClientJson($blocklistFile, $jail) {
global $ARCHIVE_ROOT;
// Servername from path
$relativePath = str_replace($ARCHIVE_ROOT, '', $blocklistFile);
$parts = explode('/', $relativePath);
$server = $parts[0] ?? 'unknown';
$updateFile = $ARCHIVE_ROOT . 'update.json';
if (!is_dir($ARCHIVE_ROOT)) mkdir($ARCHIVE_ROOT, 0755, true);
// Load or initialize
if (file_exists($updateFile)) {
$update_data = json_decode(file_get_contents($updateFile), true);
if (!is_array($update_data)) $update_data = [];
} else {
$update_data = [];
}
if (!isset($update_data[$server])) $update_data[$server] = [];
$update_data[$server][$jail . '.blocklist.json'] = true;
file_put_contents($updateFile, json_encode($update_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}

View File

@@ -1,6 +1,9 @@
<?php
// Set correct path to your blocklist directory
$blocklistDir = dirname(__DIR__) . '/archive/';
require_once __DIR__ . "/paths.php";
$blocklistDir = $PATHS["blocklists"];
$stats = [];

View File

@@ -1,7 +1,8 @@
<?php
header('Content-Type: application/json');
$archiveDirectory = dirname(__DIR__) . '/archive/';
require_once __DIR__ . "/paths.php";
$archiveDirectory = $blocklistDir = $PATHS["fail2ban"];
$files = array_filter(scandir($archiveDirectory), function($file) {
return preg_match('/^fail2ban-events-\d{8}\.json$/', $file);
@@ -23,17 +24,17 @@ if (!$files) {
rsort($files); // newest first
// Heute = neueste Datei
// Today = newest File
$todayFile = $files[0];
// Aggregationsziel: [yesterday => 1, last_7_days => 7, last_30_days => 30]
// Aggregationtarget: [yesterday => 1, last_7_days => 7, last_30_days => 30]
$aggregationRanges = [
'yesterday' => 1,
'last_7_days' => 7,
'last_30_days' => 30,
];
// Funktion zur Verarbeitung von Einträgen
// process entrys
function processEntries($entries): array {
$banTotal = 0;
$unbanTotal = 0;
@@ -62,7 +63,7 @@ function processEntries($entries): array {
];
}
// Zähle Bans pro Jail in einem Eintrags-Array
// count bans per jail
function countBansPerJail(array $entries): array {
$bansPerJail = [];
@@ -81,20 +82,20 @@ function countBansPerJail(array $entries): array {
return $bansPerJail;
}
// Zuerst: heutige Datei verarbeiten
// first todays file
$todayPath = $archiveDirectory . '/' . $todayFile;
$todayEntries = json_decode(file_get_contents($todayPath), true);
$todayStats = processEntries($todayEntries);
// Zusätzliche Statistik: Bans pro Jail (nur heute)
// Bans per Jail (only from today)
$banCountPerJail = countBansPerJail($todayEntries);
// Dann aggregierte Werte berechnen
// count
$aggregatedStats = [];
foreach ($aggregationRanges as $label => $count) {
$aggregatedEntries = [];
// n Dateien überspringen wenn nicht genug vorhanden
// Skip n files if not enough are available
for ($i = 1; $i <= $count && isset($files[$i]); $i++) {
$filePath = $archiveDirectory . '/' . $files[$i];
$content = json_decode(file_get_contents($filePath), true);
@@ -106,7 +107,7 @@ foreach ($aggregationRanges as $label => $count) {
$aggregatedStats[$label] = processEntries($aggregatedEntries);
}
// Finales JSON-Resultat mit zusätzlichem Feld ban_count_per_jail
// Final JSON result with additional field ban_count_per_jail
echo json_encode(array_merge($todayStats, [
'aggregated' => $aggregatedStats,
'ban_count_per_jail' => $banCountPerJail,

View File

@@ -3,9 +3,9 @@
header('Content-Type: application/json');
// Directory containing blocklist files
//$archiveDir = dirname(__DIR__) . '/../archive/';
$archiveDir = realpath(__DIR__ . '/../archive');
require_once __DIR__ . "/paths.php";
$archiveDir = $blocklistDir = $PATHS["blocklists"];
if (!$archiveDir) {
http_response_code(500);
die('Archive directory not found.');
@@ -42,11 +42,6 @@ foreach ($blocklistFiles as $file) {
$allEntries = array_merge($allEntries, $data);
}
// Optional: sort entries by timestamp descending or IP ascending, etc.
// usort($allEntries, function($a, $b) {
// return strcmp($b['timestamp'], $a['timestamp']); // newest first
// });
// Output the aggregated blocklist entries as JSON
echo json_encode([
'success' => true,

View File

@@ -1,14 +1,15 @@
<?php
// includes/get-json.php
require_once __DIR__ . '/paths.php';
$filename = basename($_GET['file'] ?? '');
$filepath = realpath(__DIR__ . '/../archive/' . $filename);
$filepath = $PATHS["fail2ban"] . '/' . $filename;
// secure: it can only read json form archive
// secure: it can only read json from archive
if (
!$filename ||
!preg_match('/^fail2ban-events-\d{8}\.json$/', $filename) ||
strpos($filepath, realpath(__DIR__ . '/../archive/')) !== 0 ||
strpos(realpath($filepath), realpath($PATHS["fail2ban"])) !== 0 ||
!file_exists($filepath)
) {
http_response_code(404);

View File

@@ -1,6 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<?php require_once __DIR__ . '/auth.php'; ?>
<meta charset="UTF-8" />
<title>Fail2Ban Report</title>
<meta name="viewport" content="width=device-width, initial-scale=0.8">
@@ -21,18 +24,97 @@
<script src="assets/js/blocklist-stats.js"></script>
<script src="assets/js/warnings.js"></script>
<script src="assets/js/table-export.js"></script>
<script src="assets/js/ufw-report.js"></script>
<!-- Auth -->
<?php
if (!isset($_SESSION['user_role'])) {
die("Session not active. Please login.");
}
?>
<!-- Auth -->
</head>
<body>
<div class="inline-headlines">
<div class="sessionheader">
<div>
<h1>Fail2Ban-Report</h1>
<h2>Let's catch the bad guys!</h2>
<div><span title="Beta 4.0"><small>Version : 0.4.0</small></span></div>
<div>
<span title="Beta 5.0"><small>Version : 0.5.0</small> 🕵️</span></div>
</div>
<!-- Show User and Server -->
<!--
<div>
<?php
if (isset($_SESSION['username'])) {
echo "<h3>".$_SESSION['username']." @ ".$activeServer."</h3>";
} else {
echo "<h4>viewer @ ".$activeServer."</h4>";
}
?>
</div>
-->
<!-- Show User and Server -->
<!-- Log in/out -->
<div>
<form method="post" action="">
<small>
<label for="login_user">User:</label>
<input type="text" name="login_user" id="login_user" required>
<label for="login_pass">Password:</label>
<input type="password" name="login_pass" id="login_pass" required>
</small>
<button class="button-reset" type="submit">Login</button>
</form>
</div>
<div>
<form method="post" action="">
<button class="button-reset" type="submit" name="logout" value="1">Logout</button>
</form>
</div>
<!-- Log in/out -->
<!-- Serverselect -->
<div>
<form method="post" style="margin-bottom: 1em;">
<label for="server">
<?php
if (isset($_SESSION['username'])) {
echo "<span style='color: #d4af37; font-weight: bold;'>".$_SESSION['username']."</span> @";
} else {
echo "guest @";
}
?>
</label>
<select name="server" id="server" onchange="this.form.submit()">
<?php
foreach ($SERVERS as $key => $name) {
$selected = ($key === $activeServer) ? "selected" : "";
echo "<option value='$key' $selected>$name</option>";
}
?>
</select>
</form>
</div>
<!-- Serverselect -->
</div>
<!- Second row here -->
<div class="inline-headlines">
<div id="fail2ban-alerts-container">
<div class="headhead"><span title="Shows Jail | Events | Unique IPs">DoS/Scan/BF:</span></div>
<div id="fail2ban-warning-status" class="fail2ban-status">
@@ -50,6 +132,8 @@
<div id="fail2ban-top3-jails" class="toplist"></div>
</div>
<div id="fail2ban-stats">
<div class="headhead">Fail2Ban Today:</div>
<div>🚫 Bans: <span id="fail2ban-bans">--</span></div>
@@ -64,11 +148,13 @@
<div class="headstat">📆 30 Days: <span id="fail2ban-last30">--</span></div>
</div>
<div id="blocklist-stats">
<div class="headhead">Fail2Ban-Report Jails:</div>
<div class="headhead">Fail2Ban-Report Blocklists:</div>
<div id="blocklist-stats-container">
<!-- JS render Jails here -->
</div>
</div>
</div>

View File

@@ -3,6 +3,8 @@
// Path to the config file
$configPath = '/opt/Fail2Ban-Report/fail2ban-report.config';
require_once __DIR__ . "/paths.php";
// Read config file and parse the [Fail2Ban-Daily-List-Settings] section
$config = [];
if (file_exists($configPath)) {
@@ -15,7 +17,7 @@ if (isset($config['Fail2Ban-Daily-List-Settings']['max_display_days'])) {
$maxDays = (int)$config['Fail2Ban-Daily-List-Settings']['max_display_days'];
}
$jsonDir = dirname(__DIR__) . '/archive/';
$jsonDir = $PATHS["fail2ban"];
// Collect all matching JSON files with their dates extracted from filenames
$matchedFiles = [];

66
Web-UI/includes/paths.php Normal file
View File

@@ -0,0 +1,66 @@
<?php
// Use existing Session or start a new one
if (session_status() === PHP_SESSION_NONE) {
require_once __DIR__ . '/auth.php';
}
// Config Pfad
$CONFIG_ROOT = "/opt/Fail2Ban-Report/Settings/";
// Basispfad
$ARCHIVE_ROOT = __DIR__ . "/../archive/";
// Serverliste automatisch aus archive/ generieren
$SERVERS = [];
if (is_dir($ARCHIVE_ROOT)) {
foreach (scandir($ARCHIVE_ROOT) as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
if (is_dir($ARCHIVE_ROOT . $entry)) {
// z. B. Key = Ordnername, Value = "Schönschreibweise"
$SERVERS[$entry] = ucfirst($entry);
}
}
}
// Config einlesen
$configFile = $CONFIG_ROOT . 'fail2ban-report.config';
$config = parse_ini_file($configFile, true);
// Standardserver aus Config lesen
$configDefault = $config['Default Server']['defaultserver'] ?? null;
// Validierung Default: aus Config, sonst erster gefundener Server
if ($configDefault && array_key_exists($configDefault, $SERVERS)) {
$DEFAULT_SERVER = $configDefault;
} else {
$DEFAULT_SERVER = array_key_first($SERVERS);
}
// If choosen item -> dont forget
if (isset($_POST['server']) && array_key_exists($_POST['server'], $SERVERS)) {
$_SESSION['active_server'] = $_POST['server'];
}
// active server (Session → Default)
$activeServer = (isset($_SESSION['active_server']) && array_key_exists($_SESSION['active_server'], $SERVERS))
? $_SESSION['active_server']
: $DEFAULT_SERVER;
/**
* Pfade für den aktuell aktiven Server zurückgeben
*/
function getPaths($server) {
global $ARCHIVE_ROOT;
$base = $ARCHIVE_ROOT . $server . "/";
return [
"fail2ban" => $base . "fail2ban/",
"blocklists" => $base . "blocklists/",
"ufw" => $base . "ufw/",
];
}
// Globale PATHS-Variable setzen
$PATHS = getPaths($activeServer);
$PATHS['config'] = $CONFIG_ROOT;

View File

@@ -0,0 +1,166 @@
<?php
// includes/unblock-ip.php
require_once __DIR__ . "/paths.php";
function unblockIp($ips, $jail = 'unknown') {
require_once __DIR__ . '/auth.php';
if (!is_admin()) {
return [
'success' => false,
'message' => 'Unauthorized: Only admin can unblock IPs.',
'type' => 'error'
];
}
global $ARCHIVE_ROOT; // Pfad zum Archive-Root
$results = [];
if (!is_array($ips)) {
$ips = [$ips];
}
foreach ($ips as $ip) {
$ip = trim($ip);
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
$results[] = [
'ip' => $ip,
'success' => false,
'message' => "Invalid IP address: $ip",
'type' => 'error'
];
continue;
}
$safeJail = strtolower(preg_replace('/[^a-z0-9_-]/', '', $jail));
if ($safeJail === '') $safeJail = 'unknown';
$jsonFile = $GLOBALS["PATHS"]["blocklists"] . $safeJail . ".blocklist.json";
$lockFile = "/tmp/{$safeJail}.blocklist.lock";
if (!file_exists($jsonFile)) {
$results[] = [
'ip' => $ip,
'success' => false,
'message' => "[NOTFOUND] Blocklist file {$safeJail}.blocklist.json not found.",
'type' => 'error'
];
continue;
}
$lockHandle = fopen($lockFile, 'c');
if (!$lockHandle) {
$results[] = [
'ip' => $ip,
'success' => false,
'message' => "[LOCK] Unable to open lock file for {$safeJail}.",
'type' => 'error'
];
continue;
}
if (!flock($lockHandle, LOCK_EX)) {
fclose($lockHandle);
$results[] = [
'ip' => $ip,
'success' => false,
'message' => "[LOCK] Could not acquire lock for {$safeJail}.",
'type' => 'error'
];
continue;
}
// Load existing JSON
$data = json_decode(file_get_contents($jsonFile), true);
if (!is_array($data)) $data = [];
$found = false;
foreach ($data as &$item) {
if ($item['ip'] === $ip && (!isset($item['active']) || $item['active'] === true)) {
$item['active'] = false;
$item['pending'] = true;
$item['lastModified'] = date('c');
$found = true;
if (file_put_contents($jsonFile, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) === false) {
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
$results[] = [
'ip' => $ip,
'success' => false,
'message' => "[WRITE] Failed to update {$safeJail}.blocklist.json.",
'type' => 'error'
];
continue 2;
}
// --- Update update.json ---
updateClientJson($jsonFile, $safeJail);
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
$results[] = [
'ip' => $ip,
'success' => true,
'message' => "IP $ip successfully unblocked in {$safeJail}.blocklist.json.",
'type' => 'success'
];
continue 2;
}
}
unset($item);
flock($lockHandle, LOCK_UN);
fclose($lockHandle);
if (!$found) {
$results[] = [
'ip' => $ip,
'success' => false,
'message' => "[NOTFOUND] IP $ip not active in {$safeJail}.blocklist.json.",
'type' => 'error'
];
}
}
if (count($results) === 1) return $results[0];
return [
'success' => true,
'message' => count($results) . ' IP(s) processed.',
'details' => $results,
'type' => 'success'
];
}
/**
* Setzt den Update-Flag in update.json im Archive-Root.
*
* @param string $blocklistFile Pfad der Blocklist, um Servernamen zu extrahieren
* @param string $jail Blocklist-Name
*/
function updateClientJson($blocklistFile, $jail) {
global $ARCHIVE_ROOT;
$relativePath = str_replace($ARCHIVE_ROOT, '', $blocklistFile);
$parts = explode('/', $relativePath);
$server = $parts[0] ?? 'unknown';
$updateFile = $ARCHIVE_ROOT . 'update.json';
if (!is_dir($ARCHIVE_ROOT)) mkdir($ARCHIVE_ROOT, 0755, true);
if (file_exists($updateFile)) {
$update_data = json_decode(file_get_contents($updateFile), true);
if (!is_array($update_data)) $update_data = [];
} else {
$update_data = [];
}
if (!isset($update_data[$server])) $update_data[$server] = [];
$update_data[$server][$jail . '.blocklist.json'] = true;
file_put_contents($updateFile, json_encode($update_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}

View File

@@ -1,7 +1,9 @@
<?php
header('Content-Type: application/json');
// read config
require_once __DIR__ . "/paths.php";
// read Config
$configPath = '/opt/Fail2Ban-Report/fail2ban-report.config';
if (!file_exists($configPath)) {
echo json_encode(['status' => 'disabled', 'reason' => 'No config found']);
@@ -10,18 +12,18 @@ if (!file_exists($configPath)) {
$config = parse_ini_file($configPath, true);
// Warnings only active if enabled in the config is set to true (valid boolean check)
// Warnings only active, when in Config enabled = true steht (bool valid Check)
if (empty($config['Warnings']['enabled']) || !filter_var($config['Warnings']['enabled'], FILTER_VALIDATE_BOOLEAN)) {
echo json_encode(['status' => 'disabled', 'reason' => 'Warnings not enabled']);
exit;
}
// Read threshold values, fallback to 20:50 if not set
// Fallback 20:50 for warn and crit - warnings in header
$thresholdRaw = $config['Warnings']['threshold'] ?? '20:50';
[$warnThreshold, $criticalThreshold] = array_map('intval', explode(':', $thresholdRaw));
// Locate archive directory and latest log file
$archiveDir = dirname(__DIR__) . '/archive/';
// find newest log in archive
$archiveDir = $PATHS["fail2ban"];
$files = array_filter(scandir($archiveDir), fn($f) => preg_match('/^fail2ban-events-\d{8}\.json$/', $f));
rsort($files); // neueste zuerst
$todayFile = $files[0] ?? null;
@@ -31,14 +33,14 @@ if (!$todayFile || !file_exists($archiveDir . $todayFile)) {
exit;
}
// load JSON entrys
// load JSON Data
$entries = json_decode(file_get_contents($archiveDir . $todayFile), true);
if (!is_array($entries)) {
echo json_encode(['status' => 'error', 'reason' => 'Invalid JSON log']);
exit;
}
// Group events by jail and minute (minute as 'Y-m-d H:i')
// Group Events - Jail and Minute (Minute als 'Y-m-d H:i')
$jailMinuteEvents = [];
foreach ($entries as $entry) {
@@ -52,7 +54,7 @@ foreach ($entries as $entry) {
$jailMinuteEvents[$jail][$minute][] = $ip;
}
// Analysis: Count events and unique IPs per jail/minute, classify as warning or critical
// Analyze: count Events and unique IPs per Jail per Minute, classify as Warn or Crit
$warnings = [];
$criticals = [];
@@ -69,7 +71,7 @@ foreach ($jailMinuteEvents as $jail => $minutes) {
$criticals[$jail]['events'] += $eventCount;
$criticals[$jail]['unique_ips'] += $uniqueIPCount;
} elseif ($eventCount >= $warnThreshold) {
// Warning (only if not already classified as critical)
// Warning (only when not crit)
if (!isset($warnings[$jail])) {
$warnings[$jail] = ['events' => 0, 'unique_ips' => 0];
}
@@ -79,7 +81,7 @@ foreach ($jailMinuteEvents as $jail => $minutes) {
}
}
// craft json answer
// forge response
$response = [
'status' => 'ok',
'warning' => [

View File

@@ -1,6 +1,22 @@
<?php include ('includes/list-files.php'); ?>
<?php include 'includes/header.php'; ?>
<!-- Blockstats -->
<div class="jaillistdiv">
<div class="subheadhead">Number of Fail2Ban Blocks per Jail today:</div>
<div id="fail2ban-bans-per-jail" class="jaillist">loading list ...</div>
</div>
<!-- Future Feature -->
<!--
<div class="jaillistdiv">
<div class="subheadhead">UFW Blocks from Fail2Ban-Report Lists:</div>
<div class="jaillist" id="ufw-blocks-info">loading list ..</div>
</div>
-->
<!-- Future Feature -->
<!-- Blockstats -->
<div class="spacer1"></div>
<!-- === Filters Container === -->
<div id="filters" style="display:flex; flex-wrap:wrap; gap:0.5em; align-items:center; margin-bottom:1em;">
@@ -19,8 +35,8 @@
<label for="markFilter">Mark:</label>
<select id="markFilter">
<option value="">All</option>
<option value="yellow">🟡 Warn</option>
<option value="red">🔴 Crit</option>
<option value="yellow">🟡 Ban Increases</option>
<option value="red">🔴 Multiple Bans</option>
<option value="yellowred">🟡🔴 Both</option>
<option value="none"> None</option>
</select>
@@ -42,10 +58,16 @@
<div id="notification-container" style="position: fixed; top: 20px; right: 20px; z-index: 9999;"></div>
<!--
<div class="jaillistdiv">
<div class="headhead jaillist">Todays Blocks per Jail:</div>
<div class="subheadhead">Number of Fail2Ban Blocks per Jail Today:</div>
<div id="fail2ban-bans-per-jail" class="jaillist">loading list ...</div>
</div>
<div class="jaillistdiv">
<div class="subheadhead">UFW Blocks from Fail2Ban-Report Blocklists: </div>
<div class="jaillist" id="ufw-blocks-info">loading list ..</div>
</div>
-->
<table id="resultTable">
<thead>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -1,5 +1,220 @@
# changelog
## Timeline (since 0.3.1)
```
0.3.1 ──► 0.3.2 ──► 0.3.3 ──► 0.3.4 ──► 0.4.0 ──► 0.5.0
🔐🌐 ⚙️🌐 ⚙️🌐 ⚙️🌐 🌐🐳 ⚙️🔐🌐
│ │ │ │ │ │
│ │ │ │ │ └─ Multi-server with https endpoint & Admin Authentication
│ │ │ │ └─ Marker system, copy clipboard & Docker Version
│ │ │ └─ Warnings & pending status
│ │ └─ Jail-specific blocklists & filters
│ └─ Aggregated Fail2Ban stats
└─ Secure JSON proxy, daily logs, mobile, favicon
########## Legend #########
🔐 Security
🌐 UI
⚙️ Backend
🐳 Docker
```
# Detailed Changes for all Versions (since 0.3.1)
## Changes made for 0.5.0
### Restructuring `archive/` directory
The archive structure has been reorganized for multi-server support:
```
archive/
└── %server%/ <= Hostname or choosen Name for Host
├── fail2ban/ <= daily json files from fail2ban
├── blocklists/ <= blocklists
└── ufw/ <= future feature
```
Standardserver has to be set in config
### Centralized Path Configuration
**Dynamic Path Management**
- New `paths.php` introduced for centralized path management
- `$PATHS['config']``/opt/Fail2Ban-Report/Settings/`
- `$PATHS['blocklists']` → server-specific blocklist paths
- Usage:
```php
require_once __DIR__ . "/paths.php";
$NEEDED_PATH = $PATHS["blocklists"];
```
> Last 3 Hardcoded Paths are in:
> - `paths.php` → To avoid circular reference
> - `auth.php` → To avoid circular reference
> - `action_report-ip.php` → not integrated in multiserver logic for now (_no need to function propper_)
### UI
- new dropdown for server-list to switch between servers
## Authentication for Admin-Actions
1. **Session-based Authentication**
- Secure sessions with `session_set_cookie_params`:
- `HttpOnly = true`
- `Secure = true` (HTTPS only)
- `SameSite = Strict`
- Session timeout set to 30 minutes
- Session roles: `viewer` (default) / `admin`
2. **Login / Logout Functionality**
- Login form with username & password
- Password verification using `password_verify()` (bcrypt)
- Session regeneration after successful login (`session_regenerate_id(true)`)
- Logout destroys the session and redirects back to login page
3. **User Management via JSON File**
- File: `users.json` located outside the web root (`/opt/Fail2Ban-Report/Settings/users.json`)
- No default users users must be created via shell script
- File permissions: `chown root:www-data` + `chmod 0660`
- All admin and viewer information loaded from this file
4. **Admin-only Actions**
- `block-ip.php` and `unblock-ip.php` check `is_admin()`
- Viewers cannot execute these actions and receive clear error messages
5. **UI Adjustments**
- Role display, e.g., `Viewer` or `Admin`
- Server selection preserved correctly across sessions
- Login Form in Header
- Logout button resets role to `Viewer`
#### Roles
- `Block` and `Unblock` Features need now Admin Role to work
- Users with Roles `Viewer` or `Admin` can be added with provided .sh Script `manage-users.sh`
### fail2ban_log2json.sh
**Changes:**
- **Support for "Increase Ban" events:**
- Added parsing logic for `Increase Ban` events in addition to `Ban` and `Unban`.
- Extracts the IP from `Increase Ban` lines separately.
- Ensures only lines with valid IPs are included in the JSON output.
- **Updated grep pattern:**
- From `grep -E "(^|[^A-Za-z])(Ban|Unban) "` to `grep -E "(Ban|Unban)"` to capture `Increase Ban`.
- **Output path adjustment:**
- `OUTPUT_JSON_DIR` updated from `/opt/Fail2Ban-Report/archive/YOUR-HOSTNAME/fail2ban` to `/var/www/Fail2Ban-Report/archive`.
- **Minor cleanup:**
- Last comma removal logic kept to ensure valid JSON.
---
### jsonreader.js
**Changes:**
- **Increase Ban handling in table:**
- `Increase Ban` events are no longer rendered as separate rows to avoid flooding the table.
- Counts `Increase Ban` events per IP and marks the corresponding `Ban`/`Unban` row with a yellow marker (`🟡`).
- Appends the count of `Increase Ban` events in parentheses next to the yellow marker.
- **Marker logic updated:**
- Red marker (`🔴`) now indicates multiple `Ban`/`Unban` events per IP.
- Yellow marker (`🟡`) indicates that `Increase Ban` events exist for that IP, even if no repeated `Ban`/`Unban`.
- Combination of red and yellow markers can appear if both conditions apply.
- **Filtering remains consistent:**
- Marker filter logic updated to respect new marker assignments.
- **Step restructuring for clarity:**
- Added steps for counting `Increase Ban`, filtering, marker assignment, marker filtering, jail dropdown rebuild, sorting, and rendering.
**Behavioral impact:**
- Reduces duplicate rows caused by multiple `Increase Ban` events.
- Highlights IPs with `Increase Ban` activity on the same day.
- Table rendering and filtering continue to work as before with updated marker system.
#### index.php
changed the dropdown-list to match the new Marker assignment
### Fail2Ban-Report 0.5.0 Backend / Endpoint Updates
```
endpoint/
├── index.php # Responsible for generating and serving daily JSON logs (Fail2Ban events)
├── update.php # Endpoint for clients to request which blocklists have updates
├── download.php # Endpoint for clients to download updated blocklists for syncing
├── backsync.php # Endpoint for clients to upload updated blocklists; replaces server-side blocklists
└── .htaccess # Security: IP allowlist to restrict access to trusted clients only
```
### 1. Endpoint (`/endpoint/`)
- New HTTPS endpoint for clients to send JSON data (`fail2ban-events-*.json` and `*.blocklist.json`).
- Authentication using:
- Username
- Password (bcrypt)
- UUID
- IP address (optional whitelist via `.htaccess`)
- File type handling:
- **fail2ban-events-*.json:** overwrites existing file in `archive/<username>/fail2ban/`
- **\*.blocklist.json:** locked via `flock`; existing entries are updated (`pending=false`) or deleted depending on transmitted status
- Automatic creation of client folders in `archive/` on first upload
- Correct permissions set for web server (`root:www-data`)
---
### 2. Client Script for JSON Creation & Upload (`fail2ban_log2json.sh`)
- Generates daily JSON from Fail2Ban logs (`fail2ban-events-YYYYMMDD.json`).
- Uploads the JSON directly to the endpoint using `curl` with authentication (Username + Password + UUID).
- Logs upload results locally.
- All key settings (log file, output directory, endpoint URL, auth credentials) configurable at the top of the script.
---
### 3. Client List Management Helper (`manage-clients.sh`)
- CLI tool to add, edit, or delete client entries.
- Each client has:
- Username
- Password (bcrypt, server-side hash)
- UUID
- IP address
- Stored in `client-list.json` (`/opt/Fail2Ban-Report/Settings/`).
- Password hash generated via PHP `password_hash()`.
---
### 4. Client UUID Generation (`create-client-uuid.sh`)
- Script to generate a client UUID for installation or rotation.
- Stores UUID in `/opt/Fail2Ban-Report/Settings/client-uuid`.
- Lightweight, intended for initialization or rotation only.
---
### 5. `.htaccess` for Endpoint
- Separate `.htaccess` in the `endpoint/` folder to override global security rules.
- By default, only `index.php` is accessible; all other files blocked.
- Optional IP whitelist (`Require ip <IP>`) can be enabled.
- Optional Basic Auth can be added.
- Prevents directory listing and access to sensitive files.
---
### Summary
- Fully new endpoint infrastructure for client JSON push/pull.
- Client scripts for upload, UUID, and client list management created.
- Security enhanced through combined authentication and dedicated `.htaccess` for endpoint.
- Flexible file handling for Fail2Ban events and blocklists implemented.
---
## Changes made for V 0.4.0
### Optimized `firewall-update.sh` for faster processing, improving performance with large JSON files.
@@ -67,6 +282,7 @@ small changes where made in the following files:
- Marker column added between `Action` and `IP` for better visibility.
- Marker filter dropdown integrated into existing filters, maintaining logical order.
- Supports responsive layout using flexbox, keeping filters and buttons aligned.
- changed display time for ban unban and info messages to 7 seconds
#### Implementation Notes
- Marker calculation is based on the currently displayed dataset, not the full JSON.

View File

@@ -1,58 +0,0 @@
#!/bin/bash
# === Configuration ===
LOGFILE="/var/log/fail2ban.log" # Path to Fail2Ban log file (adjust if needed)
OUTPUT_JSON_DIR="/var/www/Fail2Ban-Report/archive" # Target folder for JSON files (adjust if needed)
# === Preparation ===
TODAY=$(date +"%Y-%m-%d") # current date in format "YYYY-MM-DD"
OUTPUT_JSON_FILE="$OUTPUT_JSON_DIR/fail2ban-events-$(date +"%Y%m%d").json"
mkdir -p "$OUTPUT_JSON_DIR"
# === Processing ===
echo "[" > "$OUTPUT_JSON_FILE"
grep -E "Ban |Unban " "$LOGFILE" | awk -v today="$TODAY" '
{
timestamp = $1 " " $2;
# Process only entries from today
if (index(timestamp, today) != 1) {
next;
}
action = $(NF-1);
ip = $NF;
text = $0;
c = 0;
delete arr;
while (match(text, /\[[^]]+\]/)) {
content = substr(text, RSTART+1, RLENGTH-2);
c++;
arr[c] = content;
text = substr(text, RSTART + RLENGTH);
}
jail = "unknown";
for(i=1; i<=c; i++) {
if (arr[i] !~ /^[0-9]+$/) {
jail = arr[i];
break;
}
}
printf " {\n \"timestamp\": \"%s\",\n \"action\": \"%s\",\n \"ip\": \"%s\",\n \"jail\": \"%s\"\n },\n", timestamp, action, ip, jail;
}
' >> "$OUTPUT_JSON_FILE"
# Remove last comma if present
if [ -s "$OUTPUT_JSON_FILE" ]; then
sed -i '$ s/},/}/' "$OUTPUT_JSON_FILE"
fi
echo "]" >> "$OUTPUT_JSON_FILE"
# === Completion message ===
echo "✅ JSON created: $OUTPUT_JSON_FILE"

View File

@@ -1,66 +0,0 @@
<?php
// abuseipdb.php
$config = parse_ini_file('/opt/Fail2Ban-Report/fail2ban-report.config');
$apiKey = trim($config['abuseipdb_key'] ?? '');
if (!$apiKey) {
echo json_encode([
'success' => false,
'message' => 'AbuseIPDB API key not set.',
'type' => 'error'
]);
return;
}
$ipToCheck = $ip ?? null;
if (!$ipToCheck) {
echo json_encode([
'success' => false,
'message' => 'No IP specified for AbuseIPDB check.',
'type' => 'error'
]);
return;
}
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.abuseipdb.com/api/v2/check?ipAddress=$ipToCheck",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Key: $apiKey",
"Accept: application/json"
],
]);
$response = curl_exec($curl);
curl_close($curl);
if ($response) {
$json = json_decode($response, true);
$count = $json['data']['totalReports'] ?? null;
if ($count === null) {
echo json_encode([
'success' => false,
'message' => 'AbuseIPDB: Unexpected API response.',
'type' => 'error'
]);
return;
}
$msg = "AbuseIPDB: $ipToCheck was reported $count time(s).";
echo json_encode([
'success' => true,
'message' => $msg,
'type' => ($count >= 10) ? 'error' : (($count > 0) ? 'info' : 'success')
]);
} else {
echo json_encode([
'success' => false,
'message' => 'AbuseIPDB request failed.',
'type' => 'error'
]);
}

View File

@@ -1,77 +0,0 @@
<?php
// ipinfo.php
$config = parse_ini_file('/opt/Fail2Ban-Report/fail2ban-report.config');
$apiKey = trim($config['ipinfo_key'] ?? '');
if (!$apiKey) {
echo json_encode([
'success' => false,
'message' => 'IPInfo API key not set.',
'type' => 'error'
]);
return;
}
$ipToCheck = $ip ?? null;
if (!$ipToCheck) {
echo json_encode([
'success' => false,
'message' => 'No IP specified for IPInfo check.',
'type' => 'error'
]);
return;
}
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://ipinfo.io/{$ipToCheck}/json?token={$apiKey}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Accept: application/json"
],
]);
$response = curl_exec($curl);
$curlError = curl_error($curl);
curl_close($curl);
if ($response) {
$json = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_encode([
'success' => false,
'message' => 'IPInfo: Invalid JSON response.',
'type' => 'error'
]);
return;
}
// Example fields from IPInfo
$ip = $json['ip'] ?? 'unknown';
$hostname = $json['hostname'] ?? 'N/A';
$city = $json['city'] ?? 'N/A';
$region = $json['region'] ?? 'N/A';
$country = $json['country'] ?? 'N/A';
$org = $json['org'] ?? 'N/A';
$loc = $json['loc'] ?? 'N/A';
$postal = $json['postal'] ?? 'N/A';
$msg = "IPInfo: $ip - Hostname: $hostname, Location: $city, $region, $country, Org: $org";
echo json_encode([
'success' => true,
'message' => $msg,
'data' => $json,
'type' => 'info'
]);
} else {
$errorMsg = $curlError ?: 'IPInfo request failed.';
echo json_encode([
'success' => false,
'message' => $errorMsg,
'type' => 'error'
]);
}

View File

@@ -1,176 +0,0 @@
# Fail2Ban-Report Installer Setup Guide
## Introduction
This document explains the interactive steps and choices presented to you during the Fail2Ban-Report automatic installation process. It also describes what the installer does behind the scenes to set up the tool properly, including creation and configuration of the mandatory config file.
---
## Installer Interactive Questions and Options
### 1. Web Root Directory
- **Prompt:**
`Please enter the installation directory for the web tool (default: /var/www/html):`
- **Purpose:**
This is where the web interface files of Fail2Ban-Report will be installed.
Press **Enter** to accept the default or specify a custom path if your web server uses a different document root.
---
### 2. Shell Scripts Directory
- **Prompt:**
`Please enter the directory to store shell scripts (default: /opt/Fail2Ban-Report):`
- **Purpose:**
The two main shell scripts (`fail2ban_log2json.sh` and `firewall-update.sh`) will be copied here.
This directory should be outside the web root for security reasons.
Press **Enter** to accept the default or specify a custom path.
---
### 3. Git Availability and Download Method
- The installer checks if `git` is installed.
- If **git is not found**, you will be asked:
`Would you like to download the repository as a ZIP archive using wget instead? (Y/N):`
- If you answer **yes**, the installer will download and extract the ZIP archive using `wget` and `unzip`.
- If you answer **no**, the installation will abort because one of these methods is required.
---
### 4. jq Installation
- The installer checks if the JSON processor tool `jq` is installed.
- If **not found**, you will be prompted:
`Would you like to install jq now? (Y/N):`
- If you choose **yes**, the script attempts to install `jq` using your package manager (`apt`, `dnf`, or `pacman`).
- If you choose **no** or your package manager is unsupported, installation will stop, since `jq` is required.
---
### 5. Fail2Ban-Report Config File Setup (Mandatory)
- The installer **always creates or overwrites** the config file located inside the shell scripts directory (e.g. `/opt/Fail2Ban-Report/fail2ban-report.config`).
- You will be prompted to configure the following mandatory setting:
`How many days of daily reports should be shown in the main interface? (default: 7)`
- The value you enter is saved under the section:
```
[Fail2Ban-Daily-List-Settings]
max_display_days=VALUE
```
- This setting controls how many days are shown in the “Select Date” dropdown in the web interface.
---
### 6. AbuseIPDB Reputation Reporting (Optional)
- You will see an informational message about the AbuseIPDB feature that allows IP reputation lookups.
- You will be asked:
`Would you like to enable AbuseIPDB support? (Y/N):`
- If **yes**, you will be prompted to enter your AbuseIPDB API key:
`Please enter your AbuseIPDB API key (or leave blank to add later):`
- If you enter a key, it will be saved in the config automatically.
- If left blank, the config will enable reporting but without an API key; you must add it manually later.
- If you answer **no**, the config file will disable reporting completely.
---
### 7. Cronjob Installation for fail2ban_log2json.sh
- The installer will ask:
`Install daily cronjob for fail2ban_log2json.sh at 3 AM? (Y/N):`
- If **yes**, a daily cronjob running at 3 AM will be created.
- If **no**, no cronjob is installed for this script.
---
### 8. Cronjob Installation for firewall-update.sh
- You will be asked to select how often to run the firewall update script:
Please select how often the firewall-update.sh cronjob should run:
```
Every 5 minutes
Every 15 minutes
Do not install cronjob
Enter your choice [1-3]:
```
- Based on your choice, the cronjob will be installed or skipped.
---
## What the Installer Does
- **Repository Setup:**
Clones or downloads the Fail2Ban-Report repository into the chosen web root directory.
- **Dependency Handling:**
Checks for `jq` and installs it if requested.
- **Script Deployment:**
Copies `fail2ban_log2json.sh` and `firewall-update.sh` into the chosen scripts directory, sets executable permissions, and configures internal paths.
- **Config File Generation:**
Creates a mandatory `.config` file in the scripts directory with your settings including max display days and AbuseIPDB options.
- **Permissions:**
Sets ownership of the web directory files to `www-data:www-data`.
- **Cleanup:**
Removes shell scripts from the web directory for security and deletes the installer script after successful completion.
- **Cronjob Setup:**
Adds cronjobs for automatic execution of the log parser and firewall updater, based on your choices.
---
## Important Notes
- The shell scripts **must not** be located inside the web root for security reasons.
- Cronjobs involving firewall updates should run with root privileges.
- You need to manually secure the web directory by configuring the `.htaccess` file as included in the repository.
- The AbuseIPDB feature requires a valid API key for functionality.
- The config file path is dynamically referenced by PHP scripts, so if you move the config, update the PHP files accordingly.
---
## After Installation
- ⚠️ Make sure that the directory is inaccessible for unauthorized users ⚠️
- Verify that your web server serves the Fail2Ban-Report directory correctly.
- Access the Fail2Ban-Report web interface via your browser.
- Confirm that cronjobs are running and updating the data and firewall rules as expected.
---
Thank you for choosing Fail2Ban-Report!

View File

@@ -1,297 +0,0 @@
#!/bin/bash
# Colors for output
NORMAL='\033[0;39m'
GREEN='\033[1;32m'
RED='\033[1;31m'
YELLOW='\033[33m'
BLUE='\033[34m'
REPO_URL="https://github.com/SubleXBle/Fail2Ban-Report.git"
BRANCH_NAME="latest"
DEFAULT_WEBROOT="/var/www/html"
DEFAULT_SH_PATH="/opt/Fail2Ban-Report"
DEFAULT_CONFIG_PATH="$DEFAULT_SH_PATH/fail2ban-report.config"
PHP_CONFIG_PATH="$DEFAULT_SH_PATH/includes/config.php"
echo -e "${BLUE}--- Fail2Ban-Report Installer ---${NORMAL}"
# Ask for Webroot path
read -rp "Please enter the installation directory for the web tool (default: $DEFAULT_WEBROOT): " WEBROOT
WEBROOT=${WEBROOT:-$DEFAULT_WEBROOT}
TARGET_DIR="${WEBROOT%/}/Fail2Ban-Report"
# Ask for .sh script storage path
read -rp "Please enter the directory to store shell scripts (default: $DEFAULT_SH_PATH): " SH_PATH
SH_PATH=${SH_PATH:-$DEFAULT_SH_PATH}
# Ask for config file path (optional)
read -rp "Please enter the path for the config file (default: $DEFAULT_CONFIG_PATH): " CONFIG_PATH
CONFIG_PATH=${CONFIG_PATH:-$DEFAULT_CONFIG_PATH}
echo -e "\nUsing webroot installation path: $TARGET_DIR"
echo -e "Using shell script path: $SH_PATH"
echo -e "Using config file path: $CONFIG_PATH\n"
# Check for git with polite option for wget fallback
echo -e "${BLUE}Checking if git is installed...${NORMAL}"
if ! command -v git &>/dev/null; then
echo -e "${YELLOW}Git is not installed.${NORMAL}"
read -rp "Would you like to download the repository as a ZIP archive using wget instead? (Y/N): " use_wget
use_wget=${use_wget,,}
if [[ "$use_wget" == "y" ]]; then
ZIP_URL="https://github.com/SubleXBle/Fail2Ban-Report/archive/refs/heads/$BRANCH_NAME.zip"
ZIP_FILE="/tmp/fail2ban_report.zip"
echo -e "${BLUE}Downloading ZIP archive...${NORMAL}"
if ! command -v wget &>/dev/null; then
echo -e "${RED}wget is not installed. Please install wget or git manually and rerun this installer.${NORMAL}"
exit 1
fi
wget -O "$ZIP_FILE" "$ZIP_URL"
echo -e "${BLUE}Extracting ZIP archive...${NORMAL}"
if ! command -v unzip &>/dev/null; then
echo -e "${RED}unzip is not installed. Please install unzip manually and rerun this installer.${NORMAL}"
exit 1
fi
unzip -o "$ZIP_FILE" -d /tmp/
rm -f "$ZIP_FILE"
mv "/tmp/Fail2Ban-Report-$BRANCH_NAME" "$TARGET_DIR"
else
echo -e "${RED}Git is required or please choose to download via wget. Exiting.${NORMAL}"
exit 1
fi
else
# git is installed, proceed with clone or pull
if [ -d "$TARGET_DIR" ]; then
if [ -d "$TARGET_DIR/.git" ]; then
echo -e "${BLUE}Repository already exists. Pulling latest changes...${NORMAL}"
cd "$TARGET_DIR" || { echo -e "${RED}Cannot change directory to $TARGET_DIR${NORMAL}"; exit 1; }
git pull origin "$BRANCH_NAME"
else
echo -e "${YELLOW}$TARGET_DIR exists but is not a git repository.${NORMAL}"
# Check if remote repo reachable
if git ls-remote "$REPO_URL" &>/dev/null; then
read -rp "Do you want to delete $TARGET_DIR and clone fresh? (Y/N): " yn
yn=${yn,,}
if [[ "$yn" == "y" ]]; then
rm -rf "$TARGET_DIR"
echo -e "${BLUE}Cloning Fail2Ban-Report repository into $TARGET_DIR...${NORMAL}"
git clone -b "$BRANCH_NAME" "$REPO_URL" "$TARGET_DIR"
else
echo -e "${RED}Installation aborted by user.${NORMAL}"
exit 1
fi
else
echo -e "${RED}Cannot reach remote repository at $REPO_URL.${NORMAL}"
echo -e "${RED}Please check your internet connection or fix $TARGET_DIR manually.${NORMAL}"
exit 1
fi
fi
else
echo -e "${YELLOW}Cloning Fail2Ban-Report repository into $TARGET_DIR...${NORMAL}"
git clone -b "$BRANCH_NAME" "$REPO_URL" "$TARGET_DIR"
fi
fi
# Check and optionally install jq
echo -e "\n${BLUE}Checking for jq...${NORMAL}"
if ! command -v jq &>/dev/null; then
echo -e "${YELLOW}jq is not installed.${NORMAL}"
read -rp "Would you like to install jq now? (Y/N): " install_jq
install_jq=${install_jq,,}
if [[ "$install_jq" == "y" ]]; then
if command -v apt &>/dev/null; then
sudo apt update && sudo apt install -y jq
elif command -v dnf &>/dev/null; then
sudo dnf install -y jq
elif command -v pacman &>/dev/null; then
sudo pacman -Sy jq --noconfirm
else
echo -e "${RED}Unsupported package manager. Please install jq manually.${NORMAL}"
exit 1
fi
else
echo -e "${RED}jq is required for this tool. Exiting.${NORMAL}"
exit 1
fi
else
echo -e "${GREEN}jq is already installed.${NORMAL}"
fi
# Ensure shell script path exists
mkdir -p "$SH_PATH"
# === fail2ban_log2json.sh installation ===
if [ -f "$TARGET_DIR/fail2ban_log2json.sh" ]; then
echo -e "\n${BLUE}Installing fail2ban_log2json.sh...${NORMAL}"
cp "$TARGET_DIR/fail2ban_log2json.sh" "$SH_PATH/"
chmod +x "$SH_PATH/fail2ban_log2json.sh"
else
echo -e "${RED}fail2ban_log2json.sh not found in repo.${NORMAL}"
exit 1
fi
# Set ARCHIVE_PATH in fail2ban_log2json.sh
ARCHIVE_PATH="${TARGET_DIR}/archive"
ESCAPED_ARCHIVE_PATH=$(echo "$ARCHIVE_PATH" | sed 's_/_\\/_g')
if grep -q "^ARCHIVE_PATH=" "$SH_PATH/fail2ban_log2json.sh"; then
sed -i "s/^ARCHIVE_PATH=.*/ARCHIVE_PATH=\"$ESCAPED_ARCHIVE_PATH\"/" "$SH_PATH/fail2ban_log2json.sh"
else
sed -i "1iARCHIVE_PATH=\"$ESCAPED_ARCHIVE_PATH\"" "$SH_PATH/fail2ban_log2json.sh"
fi
mkdir -p "$ARCHIVE_PATH"
chmod 755 "$ARCHIVE_PATH"
# === firewall-update.sh installation ===
if [ -f "$TARGET_DIR/firewall-update.sh" ]; then
echo -e "\n${BLUE}Installing firewall-update.sh...${NORMAL}"
cp "$TARGET_DIR/firewall-update.sh" "$SH_PATH/"
chmod +x "$SH_PATH/firewall-update.sh"
# Set correct blocklist.json path in firewall-update.sh
ESCAPED_BLOCKLIST_PATH=$(echo "$ARCHIVE_PATH/blocklist.json" | sed 's_/_\\/_g')
sed -i "s|^BLOCKLIST_JSON=.*|BLOCKLIST_JSON=\"$ESCAPED_BLOCKLIST_PATH\"|" "$SH_PATH/firewall-update.sh"
else
echo -e "${RED}firewall-update.sh not found in repo.${NORMAL}"
fi
# === Create PHP config include with config path constant ===
echo -e "\n${BLUE}Creating PHP config include file for config path...${NORMAL}"
mkdir -p "$(dirname "$PHP_CONFIG_PATH")"
cat > "$PHP_CONFIG_PATH" <<EOF
<?php
define('FAIL2BAN_CONFIG_PATH', '$CONFIG_PATH');
EOF
chmod 644 "$PHP_CONFIG_PATH"
echo -e "${GREEN}Created $PHP_CONFIG_PATH with config path constant.${NORMAL}"
# === AbuseIPDB and max_display_days config setup ===
echo -e "\n${BLUE}Configuring reporting and display settings...${NORMAL}"
echo "This feature lets you check IP reputation from the web interface using AbuseIPDB."
echo "To enable this, you'll need a (free) API key from https://www.abuseipdb.com/"
echo "If you don't have a key now, you can leave it blank and add it later manually."
read -rp "Would you like to enable AbuseIPDB support? (Y/N): " ENABLE_ABUSE
ENABLE_ABUSE=${ENABLE_ABUSE,,}
read -rp "How many days of daily reports should be shown in the main list? (default: 7): " MAX_DAYS
MAX_DAYS=${MAX_DAYS:-7}
if [[ "$ENABLE_ABUSE" == "y" ]]; then
read -rp "Please enter your AbuseIPDB API key (or leave blank to add later): " API_KEY
API_KEY=${API_KEY:-""}
echo -e "${YELLOW}Creating AbuseIPDB-enabled config file...${NORMAL}"
cat > "$CONFIG_PATH" <<EOF
[reports]
report=true
report_types=abuseipdb
[AbuseIPDB API Key]
abuseipdb_key=$API_KEY
[Fail2Ban-Daily-List-Settings]
max_display_days=$MAX_DAYS
EOF
if [[ -z "$API_KEY" ]]; then
echo -e "${YELLOW}Warning: No API key entered. Add your AbuseIPDB API key manually in $CONFIG_PATH to enable reporting.${NORMAL}"
fi
else
echo -e "${YELLOW}Creating config file with AbuseIPDB disabled...${NORMAL}"
cat > "$CONFIG_PATH" <<EOF
[reports]
report=false
report_types=
[AbuseIPDB API Key]
abuseipdb_key=
[Fail2Ban-Daily-List-Settings]
max_display_days=$MAX_DAYS
EOF
fi
chmod 600 "$CONFIG_PATH"
echo -e "${GREEN}Config file created at $CONFIG_PATH${NORMAL}"
# Set ownership to www-data
echo -e "\n${BLUE}Setting ownership of $TARGET_DIR to www-data:www-data...${NORMAL}"
chown -R www-data:www-data "$TARGET_DIR"
# Remove fail2ban_log2json.sh from web directory (cleanup)
rm -f "$TARGET_DIR/fail2ban_log2json.sh"
# .htaccess Hinweis
echo -e "\n${BLUE}IMPORTANT: Please configure your .htaccess file to secure the application.${NORMAL}"
echo "An example .htaccess file is included in the repository."
# --- Cronjob installation for fail2ban_log2json.sh ---
read -rp $'\nInstall daily cronjob for fail2ban_log2json.sh at 3 AM? (Y/N): ' INSTALL_CRON
INSTALL_CRON=${INSTALL_CRON,,}
if [[ "$INSTALL_CRON" == "y" ]]; then
CRON_CMD="0 3 * * * $SH_PATH/fail2ban_log2json.sh > /dev/null 2>&1"
(crontab -l 2>/dev/null | grep -v -F "$SH_PATH/fail2ban_log2json.sh"; echo "$CRON_CMD") | crontab -
echo -e "${GREEN}Cronjob installed: $CRON_CMD${NORMAL}"
else
echo -e "${BLUE}Skipping cronjob installation for fail2ban_log2json.sh.${NORMAL}"
fi
# --- Cronjob installation for firewall-update.sh with choice ---
echo -e "\nPlease select how often the firewall-update.sh cronjob should run:"
echo "1) Every 5 minutes"
echo "2) Every 15 minutes"
echo "3) Do not install cronjob"
read -rp "Enter your choice [1-3]: " FW_CRON_CHOICE
case "$FW_CRON_CHOICE" in
1)
FW_CRON_SCHEDULE="*/5 * * * *"
;;
2)
FW_CRON_SCHEDULE="*/15 * * * *"
;;
3)
FW_CRON_SCHEDULE=""
;;
*)
echo -e "${YELLOW}Invalid choice. No cronjob will be installed for firewall-update.sh.${NORMAL}"
FW_CRON_SCHEDULE=""
;;
esac
if [ -n "$FW_CRON_SCHEDULE" ]; then
FW_CRON_CMD="$FW_CRON_SCHEDULE $SH_PATH/firewall-update.sh > /dev/null 2>&1"
(crontab -l 2>/dev/null | grep -v -F "$SH_PATH/firewall-update.sh"; echo "$FW_CRON_CMD") | crontab -
echo -e "${GREEN}Firewall cronjob installed: $FW_CRON_CMD${NORMAL}"
else
echo -e "${BLUE}No firewall cronjob installed.${NORMAL}"
fi
echo -e "\n${GREEN}Installation completed successfully!${NORMAL}"
echo "Webroot path: $TARGET_DIR"
echo "Shell scripts in: $SH_PATH"
echo "Firewall script path: $SH_PATH/firewall-update.sh"
echo
# === Cleanup frontend install directory ===
echo -e "${BLUE}Cleaning up frontend installation directory...${NORMAL}"
find "$TARGET_DIR" -type f -name "*.sh" -exec rm -f {} \;
find "$TARGET_DIR" -type f -name "*.md" -exec rm -f {} \;
find "$TARGET_DIR" -type f -name "*.config" -exec rm -f {} \;
if [ -d "$TARGET_DIR/assets/images" ]; then
rm -rf "$TARGET_DIR/assets/images"
echo -e "${GREEN}Removed $TARGET_DIR/assets/images/${NORMAL}"
fi
# Remove this installer script itself
INSTALLER_PATH="$(realpath "$0")"
echo -e "${YELLOW}Removing installer script: $INSTALLER_PATH${NORMAL}"
rm -f "$INSTALLER_PATH"
echo "Please ensure your webserver configuration is properly adjusted and test the blocklist system."

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
screenshots/Info-050-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB