feat(security): Implement SysWarden Evolution #1-3 security enhancements

Evolution #1 - IP Blocklist (secubox-app-ipblocklist, luci-app-ipblocklist):
- Pre-emptive blocking layer with ipset (~100k IPs)
- Default sources: Data-Shield, Firehol Level 1
- Supports nftables (fw4) and iptables backends
- LuCI KISS dashboard with sources/whitelist management

Evolution #2 - AbuseIPDB Reporter (luci-app-crowdsec-dashboard v0.8.0):
- New "AbuseIPDB" tab in CrowdSec Dashboard
- crowdsec-reporter.sh CLI for reporting blocked IPs
- RPCD handler luci.crowdsec-abuseipdb with 9 methods
- Cron job for automatic reporting every 15 minutes
- IP reputation checker in dashboard

Evolution #3 - Log Denoising (luci-app-system-hub v0.5.2):
- Three modes: RAW, SMART (noise ratio), SIGNAL_ONLY (filter known IPs)
- Integrates with IP Blocklist ipset + CrowdSec decisions
- RPCD methods: get_denoised_logs, get_denoise_stats
- Denoise mode selector panel with noise ratio indicator

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-20 20:11:24 +01:00
parent a81e8dd8ca
commit cec4893db9
27 changed files with 3389 additions and 16 deletions

621
.claude/EVOLUTION-PLAN.md Normal file
View File

@ -0,0 +1,621 @@
# SecuBox — Plan d'Évolution : Intégration des patterns SysWarden
> Document interne CyberMind.FR — Usage Claude Code
> Auteur : Gandalf (gkerma) — Version 1.0 — Février 2026
> Référence : `secubox-openwrt` × `syswarden` cross-analysis
---
## Contexte et Objectif
Ce document est un plan d'implémentation actionnable pour Claude Code. Il décrit quatre évolutions techniques à apporter à `secubox-openwrt`, inspirées de l'analyse comparative du projet `syswarden` (fork `gkerma/syswarden`).
**Objectif global** : combler les surfaces de sécurité non couvertes par SecuBox en s'inspirant des patterns éprouvés de SysWarden, sans dénaturer l'architecture LuCI modulaire existante.
**Périmètre** : les quatre évolutions sont indépendantes et peuvent être implémentées dans l'ordre de priorité défini. Chacune est spécifiée avec suffisamment de détail pour qu'un agent Claude Code puisse l'implémenter sans intervention humaine supplémentaire.
---
## ÉVOLUTION #1`luci-app-ipblocklist` (Priorité HAUTE)
### Problème adressé
SecuBox s'appuie exclusivement sur CrowdSec pour le blocage IP, qui est réactif/collaboratif. Il n'existe aucune couche de défense statique pré-emptive à l'image du layer 1 de SysWarden (blocklist ~100k IPs connues dans ipset kernel).
### Architecture cible
```
Trafic entrant
└─► [Layer 1] ipset Data-Shield (~100k IPs) → DROP kernel immédiat (NOUVEAU)
└─► [Layer 2] CrowdSec bouncer → blocage collaboratif dynamique (EXISTANT)
└─► [Layer 3] Netifyd DPI → détection applicative (EXISTANT)
```
### Nouveau module à créer : `luci-app-ipblocklist`
**Structure de fichiers à créer** :
```
luci-app-ipblocklist/
├── Makefile
├── README.md
├── htdocs/luci-static/resources/
│ ├── view/ipblocklist/
│ │ └── dashboard.js # Vue principale LuCI
│ └── ipblocklist/
│ ├── api.js # Client RPC
│ └── dashboard.css # Styles (dark cybersecurity theme, cohérent avec CrowdSec)
└── root/
├── etc/
│ ├── config/ipblocklist # UCI config (sources, schedule, whitelist)
│ └── cron.d/ipblocklist # Cron hourly update
├── usr/
│ ├── libexec/rpcd/ipblocklist # Backend shell RPCD
│ └── share/
│ ├── luci/menu.d/ipblocklist.json
│ └── rpcd/acl.d/ipblocklist.json
└── sbin/
└── ipblocklist-update.sh # Script principal de mise à jour
```
### Spécifications `ipblocklist-update.sh`
Ce script s'inspire directement de `install-syswarden.sh` mais adapté OpenWrt :
```bash
#!/bin/sh
# ipblocklist-update.sh — SecuBox IP Blocklist Manager
# Compatible OpenWrt — utilise ipset natif + nftables/iptables selon disponibilité
IPSET_NAME="secubox_blocklist"
SOURCES_UCI="ipblocklist"
LOG_FILE="/var/log/ipblocklist.log"
WHITELIST_FILE="/etc/ipblocklist/whitelist.txt"
# Sources de blocklists (configurables via UCI)
# Défaut: Data-Shield (même source que SysWarden)
DEFAULT_SOURCES="
https://raw.githubusercontent.com/duggytuxy/Data-Shield_IPv4_Blocklist/main/data-shield-blocklist-ipv4.txt
https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset
"
# Détection automatique du backend firewall (pattern SysWarden)
detect_firewall() {
if command -v nft >/dev/null 2>&1; then
echo "nftables"
elif command -v iptables >/dev/null 2>&1; then
echo "iptables"
else
echo "none"
fi
}
# Initialisation ipset
init_ipset() {
ipset create "$IPSET_NAME" hash:net hashsize 65536 maxelem 200000 2>/dev/null || true
ipset flush "$IPSET_NAME"
}
# Téléchargement et chargement avec TCP latency check (pattern SysWarden smart mirror)
load_blocklist() {
local sources
sources=$(uci get "${SOURCES_UCI}.global.sources" 2>/dev/null || echo "$DEFAULT_SOURCES")
local count=0
for url in $sources; do
local tmp
tmp=$(mktemp)
if wget -q -T 15 -O "$tmp" "$url" 2>/dev/null; then
while IFS= read -r line; do
[ -z "$line" ] && continue
echo "${line%%#*}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]' || continue
# Vérification whitelist
grep -qF "$line" "$WHITELIST_FILE" 2>/dev/null && continue
ipset add "$IPSET_NAME" "$line" 2>/dev/null && count=$((count + 1))
done < "$tmp"
fi
rm -f "$tmp"
done
echo "$(date): Loaded $count IPs into $IPSET_NAME" >> "$LOG_FILE"
}
# Application des règles firewall
apply_rules() {
local fw
fw=$(detect_firewall)
case "$fw" in
nftables)
# Intégration nftables OpenWrt (table fw4)
nft add set inet fw4 "$IPSET_NAME" { type ipv4_addr \; flags interval \; } 2>/dev/null || true
nft add rule inet fw4 forward ip saddr @"$IPSET_NAME" drop 2>/dev/null || true
nft add rule inet fw4 input ip saddr @"$IPSET_NAME" drop 2>/dev/null || true
;;
iptables)
iptables -I INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null || true
iptables -I FORWARD -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null || true
;;
esac
}
# Persistance via hotplug OpenWrt
save_persistence() {
local save_dir="/etc/ipblocklist"
mkdir -p "$save_dir"
ipset save "$IPSET_NAME" > "${save_dir}/ipset.save"
}
main() {
init_ipset
load_blocklist
apply_rules
save_persistence
}
main "$@"
```
### Spécifications UCI (`/etc/config/ipblocklist`)
```
config global 'global'
option enabled '1'
option update_interval '3600'
list sources 'https://raw.githubusercontent.com/duggytuxy/Data-Shield_IPv4_Blocklist/main/data-shield-blocklist-ipv4.txt'
list sources 'https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset'
option log_drops '1'
option whitelist_file '/etc/ipblocklist/whitelist.txt'
```
### Interface LuCI (`dashboard.js`) — Fonctionnalités requises
1. **Status Card** : nombre d'IPs en blocklist, dernière mise à jour, taille ipset
2. **Sources Manager** : liste des sources URL, ajout/suppression, activation par source
3. **Whitelist Manager** : IPs/CIDRs à exclure, import depuis fichier
4. **Logs Viewer** : journal des blocages avec pagination (10/20/50 entrées)
5. **Manual Actions** : bouton "Update Now", bouton "Flush", bouton "Test IP"
6. **Statistics** : graphe hits par heure (sparkline, réutiliser le style Netdata dashboard)
### Dépendances Makefile
```makefile
PKG_NAME:=luci-app-ipblocklist
PKG_VERSION:=1.0.0
LUCI_DEPENDS:=+ipset +kmod-ipt-ipset +iptables-mod-ipset
LUCI_TITLE:=SecuBox IP Blocklist — Static threat defense layer
```
### Tests de validation
- [ ] `ipset list secubox_blocklist` retourne > 50000 entrées après update
- [ ] Une IP connue malveillante (ex: `1.1.1.1` dans whitelist = exclue, IP Firehol level1 = bloquée)
- [ ] Reboot : l'ipset est rechargé depuis `/etc/ipblocklist/ipset.save` via hotplug
- [ ] UCI toggle `enabled=0` désactive le cron et vide l'ipset
- [ ] Interface LuCI : toutes les sections s'affichent sans erreur JS console
---
## ÉVOLUTION #2 — Reporting AbuseIPDB dans `luci-app-crowdsec-dashboard` (Priorité HAUTE)
### Problème adressé
SecuBox n'a aucun mécanisme de reporting sortant vers les bases communautaires. SysWarden implémente `syswarden_reporter.py` qui soumet automatiquement les IPs bloquées à AbuseIPDB. Intégrer ce pattern dans le CrowdSec dashboard renforce la posture communautaire et constitue un argument favorable pour la certification ANSSI.
### Modifications à apporter au module existant
**Fichier cible** : `luci-app-crowdsec-dashboard/`
**Nouveaux fichiers à créer** :
```
luci-app-crowdsec-dashboard/
└── root/
├── etc/config/crowdsec_abuseipdb # UCI config clé API + seuils
└── usr/
├── libexec/rpcd/crowdsec_abuseipdb # Backend RPCD
└── sbin/
└── crowdsec-reporter.sh # Script de reporting (version shell de syswarden_reporter.py)
```
**Modifications à apporter au dashboard JS existant** :
- Ajouter un onglet "AbuseIPDB Reporter" dans `luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec/`
- Nouveau fichier : `reporter.js`
### Spécifications `crowdsec-reporter.sh`
```bash
#!/bin/sh
# crowdsec-reporter.sh — SecuBox AbuseIPDB Reporter
# Inspired by syswarden_reporter.py — shell version for OpenWrt
ABUSEIPDB_API_URL="https://api.abuseipdb.com/api/v2/report"
UCI_CONFIG="crowdsec_abuseipdb"
LOG_FILE="/var/log/crowdsec-reporter.log"
STATE_FILE="/var/lib/crowdsec-reporter/reported.txt"
get_api_key() {
uci get "${UCI_CONFIG}.global.api_key" 2>/dev/null
}
get_confidence_threshold() {
uci get "${UCI_CONFIG}.global.confidence_threshold" 2>/dev/null || echo "80"
}
# Récupère les décisions CrowdSec récentes (dernière heure)
get_recent_decisions() {
if command -v cscli >/dev/null 2>&1; then
cscli decisions list --output json 2>/dev/null | \
jsonfilter -e '@[*].value' 2>/dev/null
else
# Fallback: lecture des logs CrowdSec
grep -h "ban" /var/log/crowdsec/*.log 2>/dev/null | \
grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort -u
fi
}
# Reporting vers AbuseIPDB (pattern SysWarden)
report_ip() {
local ip="$1"
local api_key="$2"
local categories="18,21" # Brute-Force, Web App Attack
# Éviter les doublons (anti-spam AbuseIPDB 15min cooldown)
mkdir -p "$(dirname "$STATE_FILE")"
grep -qF "$ip" "$STATE_FILE" 2>/dev/null && return 0
local response
response=$(wget -q -O- \
--header="Key: ${api_key}" \
--header="Accept: application/json" \
--post-data="ip=${ip}&categories=${categories}&comment=Blocked+by+SecuBox+CrowdSec+on+OpenWrt" \
"$ABUSEIPDB_API_URL" 2>/dev/null)
if echo "$response" | grep -q '"abuseConfidenceScore"'; then
echo "$ip" >> "$STATE_FILE"
echo "$(date): Reported $ip to AbuseIPDB" >> "$LOG_FILE"
return 0
fi
return 1
}
main() {
local api_key
api_key=$(get_api_key)
[ -z "$api_key" ] && echo "No API key configured" && exit 1
# Rotation du fichier d'état (garder 7 jours = pattern SysWarden logrotate)
find "$(dirname "$STATE_FILE")" -name "reported.txt.*" -mtime +7 -delete 2>/dev/null
local decisions
decisions=$(get_recent_decisions)
local reported=0
for ip in $decisions; do
report_ip "$ip" "$api_key" && reported=$((reported + 1))
done
echo "$(date): Reported $reported IPs to AbuseIPDB" >> "$LOG_FILE"
}
main "$@"
```
### Interface LuCI — Onglet "AbuseIPDB Reporter" (`reporter.js`)
Sections requises :
1. **Configuration** : champ API Key (masqué), seuil de confiance (slider 0-100), toggle enabled
2. **Statistics** : IPs reportées aujourd'hui / cette semaine / total, score AbuseIPDB du routeur
3. **History** : tableau des derniers reportings (IP, date, catégories, score retourné)
4. **Cron Status** : fréquence de reporting, dernier run, prochain run
### Cron à configurer
```
# /etc/cron.d/crowdsec-reporter
*/15 * * * * root /usr/sbin/crowdsec-reporter.sh >/dev/null 2>&1
```
### Tests de validation
- [ ] Avec clé API valide : au moins un reporting réussi visible dans l'historique
- [ ] Cooldown : même IP non re-reportée avant 15 minutes
- [ ] Sans clé API : interface affiche un état "non configuré" sans erreur
- [ ] Toggle disabled : cron supprimé, script ne s'exécute pas
---
## ÉVOLUTION #3 — Log Denoising dans `luci-app-system-hub` (Priorité MOYENNE)
### Problème adressé
`luci-app-system-hub` agrège les logs de tous les composants SecuBox mais affiche le bruit brut : scans automatisés, bruteforce repetitif depuis des IPs déjà dans la blocklist. SysWarden vante comme bénéfice principal la "réduction du bruit" pour ne voir que les "vrais signaux". Appliquer cette philosophie à la vue logs de System Hub.
### Modifications à apporter au module existant
**Fichier cible principal** : `luci-app-system-hub/htdocs/luci-static/resources/view/system_hub/logs.js` (ou équivalent selon structure existante)
**Nouveau backend à créer** : `luci-app-system-hub/root/usr/libexec/rpcd/system_hub_denoiser`
### Spécifications du backend `system_hub_denoiser`
```bash
#!/bin/sh
# system_hub_denoiser — Filtre les logs en excluant les IPs déjà dans ipblocklist
# et les événements répétitifs sans valeur opérationnelle
IPSET_NAME="secubox_blocklist"
MAX_LINES="${1:-200}"
# Récupère les IPs de la blocklist pour filtrage rapide
get_blocklist_ips() {
ipset list "$IPSET_NAME" 2>/dev/null | grep -E '^[0-9]+\.' | head -1000
}
# Filtre un flux de logs
filter_logs() {
local input="$1"
local mode="${2:-smart}" # smart | raw | signal_only
case "$mode" in
raw)
# Aucun filtrage, logs bruts
cat "$input"
;;
smart)
# Supprime les entrées provenant d'IPs en blocklist statique
# Supprime les patterns repetitifs sans valeur (scans SYN purs)
grep -v -f /tmp/denoiser_iplist.txt "$input" 2>/dev/null | \
grep -vE "(SYN_RECV|SYN_SENT)" | \
grep -vE "kernel: \[.*\] DROP" | \
tail -n "$MAX_LINES"
;;
signal_only)
# Mode le plus agressif : uniquement les événements CrowdSec, auth failures, erreurs
grep -E "(crowdsec|ALERT|ERROR|WARN|authentication failure|Failed password|CRITICAL)" "$input" | \
tail -n "$MAX_LINES"
;;
esac
}
# Point d'entrée RPCD
case "$1" in
get_filtered_logs)
get_blocklist_ips > /tmp/denoiser_iplist.txt 2>/dev/null
# Collecte logs de toutes les sources SecuBox
{
tail -n 500 /var/log/crowdsec/crowdsec.log 2>/dev/null
tail -n 500 /var/log/ipblocklist.log 2>/dev/null
logread 2>/dev/null | tail -n 500
} | filter_logs /dev/stdin "$2"
rm -f /tmp/denoiser_iplist.txt
;;
get_stats)
# Retourne les statistiques de débruitage
local total raw filtered
raw=$(logread 2>/dev/null | wc -l)
filtered=$(logread 2>/dev/null | grep -c -E "(crowdsec|ALERT|ERROR|WARN|authentication failure)" 2>/dev/null || echo 0)
printf '{"total":%d,"signals":%d,"noise_ratio":%d}\n' \
"$raw" "$filtered" \
"$(( (raw - filtered) * 100 / (raw + 1) ))"
;;
esac
```
### Modifications UI dans System Hub
Ajouter dans la vue logs existante :
1. **Sélecteur de mode** (toggle 3 positions) :
- `RAW` — logs bruts complets
- `SMART` *(défaut)* — filtré : supprime IPs blocklist + scans répétitifs
- `SIGNAL ONLY` — uniquement alertes et événements CrowdSec
2. **Indicateur de débruitage** : badge "X% noise filtered" calculé en temps réel
3. **Option "Show suppressed"** : accordéon permettant de voir les entrées filtrées en gris/opacité réduite
### Tests de validation
- [ ] Mode SMART : les logs ne contiennent plus d'entrées provenant d'IPs dans `secubox_blocklist`
- [ ] Indicateur de débruitage affiche un pourcentage cohérent (> 0% si blocklist active)
- [ ] Mode RAW : tous les logs originaux visibles
- [ ] Mode SIGNAL ONLY : uniquement les entrées contenant les keywords définis
- [ ] Performance : filtrage < 500ms pour 10000 entrées de logs
---
## ÉVOLUTION #4 — Module SIEM Connector pour cibles x86 (Priorité BASSE/CONDITIONNELLE)
### Problème adressé
SecuBox supporte officiellement `x86-64` (PC, VM, Proxmox). Sur ces cibles, Wazuh XDR Agent est déployable — SysWarden l'automatise complètement. SecuBox n'a aucun équivalent.
### Condition d'activation
Ce module est conditionnel : activable uniquement sur cibles `x86-64` et `x86-generic`. Les Makefiles des autres architectures doivent exclure ce paquet via `DEPENDS += @TARGET_x86`.
### Nouveau module à créer : `luci-app-siem-connector`
**Structure** :
```
luci-app-siem-connector/
├── Makefile # DEPENDS += @TARGET_x86 || @TARGET_x86_64
├── README.md
├── htdocs/luci-static/resources/
│ ├── view/siem/
│ │ ├── setup.js # Wizard de déploiement Wazuh Agent
│ │ └── status.js # Status et métriques de l'agent
│ └── siem/
│ └── dashboard.css
└── root/
├── etc/config/siem_connector
└── usr/
├── libexec/rpcd/siem_connector
└── sbin/
└── wazuh-deploy.sh # Pattern SysWarden : déploiement automatisé agent
```
### Spécifications `wazuh-deploy.sh`
Portage du module Wazuh de SysWarden pour OpenWrt x86 :
```bash
#!/bin/sh
# wazuh-deploy.sh — SecuBox SIEM Connector
# Déploiement automatisé Wazuh Agent (pattern SysWarden)
# Cible: OpenWrt x86 uniquement
MANAGER_IP=""
AGENT_NAME=""
AGENT_GROUP="secubox"
install_wazuh_agent() {
# Détection OS (OpenWrt = Linux, paquet via opkg si disponible en feed)
if command -v opkg >/dev/null 2>&1; then
# Installation depuis feed optionnel ou package manuel
opkg update
opkg install wazuh-agent 2>/dev/null || {
# Fallback: téléchargement direct (pattern SysWarden repo detection)
local arch
arch=$(uname -m)
local pkg_url="https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/"
# ... logique de téléchargement selon architecture
}
fi
}
configure_agent() {
local ossec_conf="/var/ossec/etc/ossec.conf"
cat > "$ossec_conf" << EOF
<ossec_config>
<client>
<server>
<address>${MANAGER_IP}</address>
<port>1514</port>
<protocol>tcp</protocol>
</server>
<config-profile>${AGENT_GROUP}</config-profile>
</client>
</ossec_config>
EOF
}
whitelist_wazuh_in_secubox() {
# Pattern SysWarden : whitelister les ports Wazuh dans ipblocklist
echo "$MANAGER_IP" >> /etc/ipblocklist/whitelist.txt
# Règle firewall pour permettre 1514/1515 vers le manager
uci add firewall rule
uci set firewall.@rule[-1].name='Allow-Wazuh-Manager'
uci set firewall.@rule[-1].dest_ip="$MANAGER_IP"
uci set firewall.@rule[-1].dest_port='1514 1515'
uci set firewall.@rule[-1].target='ACCEPT'
uci commit firewall
/etc/init.d/firewall restart
}
```
### Interface LuCI — Wizard de déploiement
Wizard en 4 étapes (inspiré de l'interactivité de SysWarden) :
1. **Prérequis** : vérification architecture x86, connexion réseau vers manager IP
2. **Configuration** : champs Manager IP, Agent Name, Group
3. **Déploiement** : progress bar, log en temps réel
4. **Vérification** : statut agent (actif/inactif), test de connexion vers manager
### Tests de validation
- [ ] Le Makefile exclut correctement le paquet sur architectures non-x86 (`DEPENDS += @TARGET_x86`)
- [ ] Wizard étape 1 : détecte et bloque l'installation si architecture incompatible
- [ ] Post-déploiement : `wazuh-agent` en statut `active` dans System Hub
- [ ] Whitelisting automatique : l'IP manager présente dans `/etc/ipblocklist/whitelist.txt`
- [ ] Port 1514/1515 : règle UCI firewall créée et visible dans LuCI
---
## Plan de Séquencement et Interdépendances
```
Semaine 1-2 : ÉVOLUTION #1 (luci-app-ipblocklist)
↓ fournit ipset "secubox_blocklist" utilisé par #3
Semaine 2-3 : ÉVOLUTION #2 (AbuseIPDB dans crowdsec-dashboard)
↓ indépendant, peut être parallélisé avec #1
Semaine 3-4 : ÉVOLUTION #3 (Log Denoising dans system-hub)
↓ dépend de #1 pour le filtrage par ipset
Semaine 5+ : ÉVOLUTION #4 (SIEM Connector x86)
↓ conditionnel, dépend de #1 pour le whitelisting
```
---
## Conventions de Développement SecuBox à Respecter
Toutes les évolutions doivent respecter les conventions identifiées dans `CLAUDE.md` du repo `secubox-openwrt` :
### Style Shell (RPCD backends)
- POSIX sh uniquement (pas bash), compatible BusyBox OpenWrt
- Toutes les fonctions documentées avec commentaire d'en-tête
- Gestion d'erreurs via `|| true` pour les commandes non-critiques
- Log systématique dans `/var/log/<module>.log`
- Usage de `uci` pour toute configuration persistante
### Style JavaScript (LuCI views)
- Réutiliser les classes CSS existantes de `luci-app-crowdsec-dashboard` pour la cohérence visuelle (dark cybersecurity theme)
- Utiliser `L.ui.showModal()` pour les confirmations destructives
- Auto-refresh via `setInterval` avec intervalle configurable (défaut 30s)
- Gestion des erreurs : afficher message d'erreur inline, pas d'alert() natif
### Structure Makefile
```makefile
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-XXXX
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=Gandalf <gandalf@cybermind.fr>
PKG_LICENSE:=Apache-2.0
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/luci/luci.mk
LUCI_TITLE:=SecuBox XXXX — Description courte
LUCI_DEPENDS:=+dep1 +dep2
$(eval $(call BuildPackage,$(PKG_NAME)))
```
### ACL JSON (permissions RPCD)
```json
{
"luci-app-XXXX": {
"description": "Grant access to SecuBox XXXX",
"read": { "uci": ["XXXX"], "file": {"/var/log/XXXX.log": ["read"]} },
"write": { "uci": ["XXXX"] }
}
}
```
---
## Critères de Validation Globaux (pour CI/CD)
Les GitHub Actions existantes (`build-openwrt-packages.yml`, `test-validate.yml`) doivent passer pour chaque évolution :
```bash
# Lint shell
shellcheck luci-app-*/root/usr/libexec/rpcd/*
shellcheck luci-app-*/root/usr/sbin/*.sh
# Lint JSON
for f in luci-app-*/root/usr/share/luci/menu.d/*.json; do
jsonlint "$f" && echo "OK: $f" || echo "FAIL: $f"
done
for f in luci-app-*/root/usr/share/rpcd/acl.d/*.json; do
jsonlint "$f" && echo "OK: $f" || echo "FAIL: $f"
done
# Validation Makefiles
for pkg in luci-app-ipblocklist luci-app-siem-connector; do
make package/${pkg}/compile V=s ARCH=x86_64 OPENWRT_VERSION=23.05.5
done
# Tests fonctionnels (nécessitent environnement OpenWrt)
./secubox-tools/secubox-debug.sh luci-app-ipblocklist
./secubox-tools/secubox-debug.sh luci-app-siem-connector
```
---
## Référentiel ANSSI CSPN — Mapping des Évolutions
| Évolution | Critère CSPN adressé |
|---|---|
| #1 ipblocklist | Contrôle d'accès réseau / Filtrage préventif |
| #2 AbuseIPDB reporter | Journalisation / Partage d'information sur incidents |
| #3 Log denoising | Journalisation / Détection d'événements pertinents |
| #4 SIEM connector | Supervision / Remontée d'alertes vers SIEM |
---
*Document généré par analyse croisée `gkerma/syswarden` × `gkerma/secubox-openwrt`*
*CyberMind.FR — Usage interne — Style OPORD confidentiel*

View File

@ -2597,3 +2597,83 @@ git checkout HEAD -- index.html
- UCI config sections: main, server, federation, admin, database, network, identity, mesh
- Matrix API responding with v1.1-v1.12 support
- Files: `package/secubox/secubox-app-matrix/`, `package/secubox/luci-app-matrix/`
28. **Log Denoising for System Hub (2026-02-20)**
- Added smart log denoising to System Hub inspired by SysWarden patterns (Evolution #3)
- Three denoising modes:
- **RAW**: All logs displayed without filtering (default)
- **SMART**: Known threat IPs highlighted, all logs visible, noise ratio computed
- **SIGNAL_ONLY**: Only new/unknown threats shown, known IPs filtered out
- Noise filtering integrates with:
- IP Blocklist (Evolution #1): ipset with 100k+ blocked IPs
- CrowdSec decisions: Active bans from threat detection
- RPCD methods added to `luci.system-hub`:
- `get_denoised_logs(lines, filter, mode)`: Returns logs with noise ratio stats
- `get_denoise_stats()`: Returns known threat counts and blocklist status
- LuCI dashboard enhancements:
- Denoise mode selector panel (RAW/SMART/SIGNAL ONLY)
- Mode description tooltip
- Noise ratio percentage indicator with color coding
- Known threats counter from ipblocklist + CrowdSec
- Warning badge when IP Blocklist disabled
- Side panel metrics include noise stats when filtering active
- Implementation:
- Extracts IPs from log lines using regex
- Skips private/local IP ranges (10.*, 172.16-31.*, 192.168.*, 127.*)
- Checks both nftables sets and iptables ipsets for compatibility
- Queries CrowdSec decisions via `cscli decisions list`
- Part of SysWarden Evolution plan (Evolution #3 of 4)
- Files modified:
- `luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub`
- `luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json`
- `luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js`
- `luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js`
- `luci-app-system-hub/Makefile` (version bumped to 0.5.2-r1)
28. **IP Blocklist - Static Threat Defense Layer (2026-02-20)**
- Evolution #1 from SysWarden-inspired EVOLUTION-PLAN.md
- Created `secubox-app-ipblocklist` backend package:
- `ipblocklist-update.sh` - Main update script with ipset management
- UCI config: sources (blocklist URLs), whitelist, update interval
- Cron hourly update job
- Supports nftables (fw4) and legacy iptables backends
- Default sources: Data-Shield (~100k IPs), Firehol Level 1
- CLI: start, stop, update, flush, status, test, logs
- Created `luci-app-ipblocklist` dashboard:
- Status card: entry count, memory usage, last update
- Enable/Disable toggle, Update Now, Flush buttons
- Test IP form with blocked/allowed result
- Sources manager with add/remove URLs
- Whitelist manager with add/remove entries
- Logs viewer with monospace output
- RPCD methods (12 total): status, logs, sources, whitelist, update, flush,
test_ip, set_enabled, add_source, remove_source, add_whitelist, remove_whitelist
- Architecture: Layer 1 pre-emptive blocking before CrowdSec Layer 2 reactive
- Files: `package/secubox/secubox-app-ipblocklist/`, `package/secubox/luci-app-ipblocklist/`
29. **AbuseIPDB Reporter - Evolution #2 (2026-02-20)**
- Evolution #2 from SysWarden-inspired EVOLUTION-PLAN.md
- Added AbuseIPDB reporting to CrowdSec Dashboard (v0.8.0):
- New "AbuseIPDB" tab in CrowdSec Dashboard navigation
- UCI config `/etc/config/crowdsec_abuseipdb` for API key and settings
- `crowdsec-reporter.sh` CLI tool for IP reporting
- Cron job for automatic reporting every 15 minutes
- Reporter features:
- Report CrowdSec blocked IPs to AbuseIPDB community database
- Check IP reputation with confidence score
- Cooldown to prevent duplicate reports (15 min default)
- Daily/weekly/total stats tracking
- Rate limiting with 1-second delay between reports
- RPCD handler `luci.crowdsec-abuseipdb` with 9 methods:
- status, history, check_ip, report, set_enabled
- set_api_key, get_config, save_config, logs
- Dashboard features:
- Status card with reported counts
- Enable/Disable and Report Now buttons
- API key configuration form
- IP reputation checker
- Recent reports history table
- Logs viewer
- Attack categories: 18 (Brute-Force), 21 (Web App Attack)
- Files: `luci-app-crowdsec-dashboard/root/usr/sbin/crowdsec-reporter.sh`,
`luci-app-crowdsec-dashboard/htdocs/luci-static/resources/view/crowdsec-dashboard/reporter.js`

View File

@ -864,13 +864,59 @@ _Last updated: 2026-02-20 (v0.24.0 - Matrix + SaaS Relay + Media Hub)_
- 4 views: Overview, Devices, Policies, Settings
- RPCD handler with 11 methods + public ACL for unauthenticated access
### Just Completed (2026-02-20 PM)
- **IP Blocklist - Evolution #1** — DONE (2026-02-20)
- Created `secubox-app-ipblocklist` backend package
- `ipblocklist-update.sh` CLI with ipset management
- Supports nftables (fw4) and iptables backends
- Default sources: Data-Shield (~100k IPs), Firehol Level 1
- Created `luci-app-ipblocklist` KISS dashboard
- RPCD handler with 12 methods
- Layer 1 pre-emptive defense before CrowdSec Layer 2
- **AbuseIPDB Reporter - Evolution #2** — DONE (2026-02-20)
- Added to `luci-app-crowdsec-dashboard` (v0.8.0)
- New "AbuseIPDB" tab in CrowdSec Dashboard
- `crowdsec-reporter.sh` CLI for reporting blocked IPs
- RPCD handler `luci.crowdsec-abuseipdb` with 9 methods
- UCI config for API key, categories, cooldown settings
- Cron job for automatic reporting every 15 minutes
- IP reputation checker in dashboard
- **Log Denoising - Evolution #3** — DONE (2026-02-20)
- Added smart log denoising to `luci-app-system-hub` (v0.5.2)
- Three modes: RAW (all logs), SMART (filter known IPs), SIGNAL_ONLY (new threats only)
- Integrates with IP Blocklist ipset + CrowdSec decisions
- RPCD methods: `get_denoised_logs`, `get_denoise_stats`
- LuCI dashboard additions:
- Denoise mode selector panel
- Noise ratio indicator with color coding
- Known threats counter
- Blocklist status warning
- Filters private IPs (10.*, 172.16-31.*, 192.168.*, 127.*)
- Supports both nftables and iptables backends
### SysWarden Evolution Plan (2026-02-20)
Implementing 4 evolutions inspired by SysWarden patterns:
| # | Module | Priority | Status |
|---|--------|----------|--------|
| 1 | `luci-app-ipblocklist` | HIGH | DONE |
| 2 | AbuseIPDB Reporter | HIGH | DONE |
| 3 | Log Denoising (System Hub) | MEDIUM | DONE |
| 4 | SIEM Connector (x86 only) | LOW | TODO |
**Next**: Evolution #4 - SIEM Connector (optional, x86 only)
### Next Up — Couche 1
1. **Guacamole Pre-built Binaries**
- Current LXC build-from-source approach is too slow
- Need to find/create pre-built ARM64 binaries for guacd + Tomcat
2. **Multi-Node Mesh Testing**
3. **Multi-Node Mesh Testing**
- Deploy second SecuBox node to test real peer-to-peer sync
- Validate bidirectional threat intelligence sharing

View File

@ -8,8 +8,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-crowdsec-dashboard
PKG_VERSION:=0.7.0
PKG_RELEASE:=32
PKG_VERSION:=0.8.0
PKG_RELEASE:=1
PKG_ARCH:=all
PKG_LICENSE:=Apache-2.0
@ -26,16 +26,27 @@ include $(TOPDIR)/feeds/luci/luci.mk
define Package/luci-app-crowdsec-dashboard/conffiles
/etc/config/crowdsec-dashboard
/etc/config/crowdsec_abuseipdb
endef
define Package/luci-app-crowdsec-dashboard/install
# UCI config file
# UCI config files
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./root/etc/config/crowdsec-dashboard $(1)/etc/config/
$(INSTALL_CONF) ./root/etc/config/crowdsec_abuseipdb $(1)/etc/config/
# RPCD backend (MUST be 755 for ubus calls)
# Cron job for AbuseIPDB reporter
$(INSTALL_DIR) $(1)/etc/cron.d
$(INSTALL_DATA) ./root/etc/cron.d/crowdsec-reporter $(1)/etc/cron.d/
# Reporter script
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./root/usr/sbin/crowdsec-reporter.sh $(1)/usr/sbin/
# RPCD backends (MUST be 755 for ubus calls)
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.crowdsec-dashboard $(1)/usr/libexec/rpcd/
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.crowdsec-abuseipdb $(1)/usr/libexec/rpcd/
# ACL permissions
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d

View File

@ -0,0 +1,349 @@
'use strict';
'require view';
'require dom';
'require poll';
'require ui';
'require rpc';
var callStatus = rpc.declare({
object: 'luci.crowdsec-abuseipdb',
method: 'status',
expect: { }
});
var callHistory = rpc.declare({
object: 'luci.crowdsec-abuseipdb',
method: 'history',
params: ['lines'],
expect: { }
});
var callCheckIp = rpc.declare({
object: 'luci.crowdsec-abuseipdb',
method: 'check_ip',
params: ['ip'],
expect: { }
});
var callReport = rpc.declare({
object: 'luci.crowdsec-abuseipdb',
method: 'report',
expect: { }
});
var callSetEnabled = rpc.declare({
object: 'luci.crowdsec-abuseipdb',
method: 'set_enabled',
params: ['enabled'],
expect: { }
});
var callSetApiKey = rpc.declare({
object: 'luci.crowdsec-abuseipdb',
method: 'set_api_key',
params: ['api_key'],
expect: { }
});
var callGetConfig = rpc.declare({
object: 'luci.crowdsec-abuseipdb',
method: 'get_config',
expect: { }
});
var callSaveConfig = rpc.declare({
object: 'luci.crowdsec-abuseipdb',
method: 'save_config',
params: ['confidence_threshold', 'categories', 'report_interval', 'max_reports_per_run', 'cooldown_minutes', 'comment_prefix'],
expect: { }
});
var callLogs = rpc.declare({
object: 'luci.crowdsec-abuseipdb',
method: 'logs',
params: ['lines'],
expect: { }
});
return view.extend({
refreshInterval: 30,
load: function() {
return Promise.all([
callStatus(),
callGetConfig(),
callHistory(20),
callLogs(30)
]);
},
formatTimestamp: function(ts) {
if (!ts || ts === 0) return 'Never';
var d = new Date(ts * 1000);
return d.toLocaleString();
},
renderStatusCard: function(status) {
var enabled = status.enabled === true;
var apiConfigured = status.api_key_configured === true;
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'AbuseIPDB Reporter Status'),
E('div', { 'class': 'table', 'style': 'margin-bottom: 1em' }, [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'width: 200px; font-weight: bold' }, 'Status'),
E('div', { 'class': 'td' }, [
E('span', {
'style': 'padding: 4px 12px; border-radius: 4px; background: ' +
(enabled && apiConfigured ? '#4CAF50' : enabled ? '#FF9800' : '#f44336') + '; color: white;'
}, enabled && apiConfigured ? 'Active' : enabled ? 'Enabled (No API Key)' : 'Disabled')
])
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'font-weight: bold' }, 'API Key'),
E('div', { 'class': 'td' }, apiConfigured ? 'Configured' : 'Not configured')
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'font-weight: bold' }, 'Pending IPs'),
E('div', { 'class': 'td' }, [
E('strong', { 'style': 'color: #2196F3' }, (status.pending_ips || 0).toString())
])
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'font-weight: bold' }, 'Reported Today'),
E('div', { 'class': 'td' }, [
E('strong', { 'style': 'color: #4CAF50' }, (status.reported_today || 0).toString())
])
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'font-weight: bold' }, 'Reported This Week'),
E('div', { 'class': 'td' }, (status.reported_week || 0).toString())
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'font-weight: bold' }, 'Total Reported'),
E('div', { 'class': 'td' }, (status.reported_total || 0).toString())
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'font-weight: bold' }, 'Last Report'),
E('div', { 'class': 'td' }, this.formatTimestamp(status.last_report))
])
])
]);
},
renderControls: function(status, config) {
var self = this;
var enabled = status.enabled === true;
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Controls'),
E('div', { 'style': 'display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 1em' }, [
E('button', {
'class': enabled ? 'btn cbi-button-remove' : 'btn cbi-button-apply',
'click': ui.createHandlerFn(this, function() {
return callSetEnabled(!enabled).then(function() {
ui.addNotification(null, E('p', {}, enabled ? 'Reporter disabled' : 'Reporter enabled'));
return self.refresh();
});
})
}, enabled ? 'Disable' : 'Enable'),
E('button', {
'class': 'btn cbi-button-action',
'disabled': !enabled || !status.api_key_configured,
'click': ui.createHandlerFn(this, function() {
ui.showModal('Reporting...', [
E('p', { 'class': 'spinning' }, 'Reporting blocked IPs to AbuseIPDB...')
]);
return callReport().then(function(res) {
ui.hideModal();
ui.addNotification(null, E('p', {}, res.message || 'Report started'));
setTimeout(function() { self.refresh(); }, 5000);
});
})
}, 'Report Now')
])
]);
},
renderApiKeyConfig: function(config) {
var self = this;
var input = E('input', {
'type': 'password',
'placeholder': 'Enter AbuseIPDB API key...',
'style': 'width: 400px; margin-right: 10px'
});
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'API Key Configuration'),
E('p', { 'style': 'color: #888; margin-bottom: 1em' }, [
'Get your free API key from ',
E('a', { 'href': 'https://www.abuseipdb.com/account/api', 'target': '_blank' }, 'AbuseIPDB'),
' (requires account registration)'
]),
E('div', { 'style': 'display: flex; align-items: center; flex-wrap: wrap; gap: 10px' }, [
input,
E('button', {
'class': 'btn cbi-button-apply',
'click': function() {
var key = input.value.trim();
if (!key) {
ui.addNotification(null, E('p', {}, 'Please enter an API key'));
return;
}
return callSetApiKey(key).then(function(res) {
if (res.success) {
input.value = '';
ui.addNotification(null, E('p', {}, 'API key saved'));
return self.refresh();
}
});
}
}, 'Save Key'),
config.api_key_set ? E('span', { 'style': 'color: #4CAF50' }, 'Key is configured') : null
])
]);
},
renderCheckIp: function() {
var input = E('input', {
'type': 'text',
'placeholder': 'Enter IP to check...',
'style': 'width: 200px; margin-right: 10px'
});
var result = E('div', { 'id': 'check-result', 'style': 'margin-top: 1em' });
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Check IP Reputation'),
E('div', { 'style': 'display: flex; align-items: center; flex-wrap: wrap; gap: 10px' }, [
input,
E('button', {
'class': 'btn cbi-button-action',
'click': function() {
var ip = input.value.trim();
if (!ip) {
result.textContent = 'Please enter an IP';
return;
}
result.innerHTML = '<span class="spinning">Checking...</span>';
callCheckIp(ip).then(function(res) {
if (res.success) {
var scoreColor = res.confidence_score > 75 ? '#f44336' :
res.confidence_score > 25 ? '#FF9800' : '#4CAF50';
result.innerHTML = '<div style="background: #1a1a2e; padding: 1em; border-radius: 4px; margin-top: 1em">' +
'<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1em">' +
'<div><strong>Confidence Score</strong><br><span style="font-size: 1.5em; color: ' + scoreColor + '">' + res.confidence_score + '%</span></div>' +
'<div><strong>Total Reports</strong><br>' + res.total_reports + '</div>' +
'<div><strong>Country</strong><br>' + res.country + '</div>' +
'<div><strong>ISP</strong><br>' + (res.isp || 'Unknown') + '</div>' +
(res.domain ? '<div><strong>Domain</strong><br>' + res.domain + '</div>' : '') +
(res.last_reported ? '<div><strong>Last Reported</strong><br>' + res.last_reported + '</div>' : '') +
'</div></div>';
} else {
result.innerHTML = '<span style="color: #f44336">Error: ' + (res.error || 'Check failed') + '</span>';
}
}).catch(function(e) {
result.innerHTML = '<span style="color: #f44336">Error: ' + e.message + '</span>';
});
}
}, 'Check')
]),
result
]);
},
renderHistory: function(history) {
var entries = (history && history.history) || [];
var rows = entries.map(function(entry) {
return E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'width: 180px' }, entry.timestamp || '?'),
E('div', { 'class': 'td', 'style': 'width: 150px; font-family: monospace' }, entry.ip || '?'),
E('div', { 'class': 'td', 'style': 'width: 80px; text-align: center' }, [
E('span', {
'style': 'padding: 2px 8px; border-radius: 4px; background: ' +
(parseInt(entry.score) > 75 ? '#f44336' : parseInt(entry.score) > 25 ? '#FF9800' : '#4CAF50') + '; color: white;'
}, entry.score || '?')
])
]);
});
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Recent Reports'),
E('div', { 'class': 'table' }, [
E('div', { 'class': 'tr cbi-section-table-titles' }, [
E('div', { 'class': 'th', 'style': 'width: 180px' }, 'Timestamp'),
E('div', { 'class': 'th', 'style': 'width: 150px' }, 'IP Address'),
E('div', { 'class': 'th', 'style': 'width: 80px; text-align: center' }, 'Score')
])
].concat(rows.length > 0 ? rows : [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'colspan': '3', 'style': 'color: #888; text-align: center' }, 'No reports yet')
])
]))
]);
},
renderLogs: function(logs) {
var entries = (logs && logs.logs) || [];
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Logs'),
E('div', {
'style': 'background: #1a1a2e; color: #0f0; font-family: monospace; padding: 1em; border-radius: 4px; max-height: 250px; overflow-y: auto; font-size: 0.85em'
}, entries.length > 0 ?
entries.map(function(line) {
var color = '#0f0';
if (line.indexOf('[ERROR]') >= 0) color = '#f44336';
else if (line.indexOf('[WARN]') >= 0) color = '#FF9800';
else if (line.indexOf('[INFO]') >= 0) color = '#00bcd4';
else if (line.indexOf('[DEBUG]') >= 0) color = '#888';
return E('div', { 'style': 'color: ' + color + '; margin-bottom: 2px' }, line);
}) :
E('div', { 'style': 'color: #888' }, 'No log entries')
)
]);
},
refresh: function() {
var self = this;
return this.load().then(function(data) {
var container = document.getElementById('reporter-container');
if (container) {
dom.content(container, self.renderContent(data));
}
});
},
renderContent: function(data) {
var status = data[0] || {};
var config = data[1] || {};
var history = data[2] || {};
var logs = data[3] || {};
return E('div', {}, [
E('h2', {}, 'AbuseIPDB Reporter'),
E('p', { 'style': 'color: #888; margin-bottom: 1em' },
'Report CrowdSec blocked IPs to AbuseIPDB community database for collaborative threat intelligence.'),
this.renderStatusCard(status),
this.renderControls(status, config),
this.renderApiKeyConfig(config),
this.renderCheckIp(),
this.renderHistory(history),
this.renderLogs(logs)
]);
},
render: function(data) {
var self = this;
poll.add(function() {
return self.refresh();
}, this.refreshInterval);
return E('div', { 'id': 'reporter-container' }, this.renderContent(data));
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,15 @@
config global 'global'
option enabled '0'
option api_key ''
option confidence_threshold '80'
option report_interval '15'
option categories '18,21'
option max_reports_per_run '50'
option cooldown_minutes '15'
option comment_prefix 'Blocked by SecuBox CrowdSec'
config stats 'stats'
option reported_today '0'
option reported_week '0'
option reported_total '0'
option last_report '0'

View File

@ -0,0 +1,8 @@
# CrowdSec AbuseIPDB Reporter - Run every 15 minutes
*/15 * * * * root [ "$(uci -q get crowdsec_abuseipdb.global.enabled)" = "1" ] && /usr/sbin/crowdsec-reporter.sh report >/dev/null 2>&1
# Reset daily stats at midnight
0 0 * * * root /usr/sbin/crowdsec-reporter.sh reset-daily >/dev/null 2>&1
# Reset weekly stats on Monday at midnight
0 0 * * 1 root /usr/sbin/crowdsec-reporter.sh reset-weekly >/dev/null 2>&1

View File

@ -0,0 +1,236 @@
#!/bin/sh
# RPCD handler for CrowdSec AbuseIPDB Reporter
# Provides API for LuCI dashboard integration
. /usr/share/libubox/jshn.sh
UCI_CONFIG="crowdsec_abuseipdb"
REPORTER_SCRIPT="/usr/sbin/crowdsec-reporter.sh"
# Get reporter status
get_status() {
if [ -x "$REPORTER_SCRIPT" ]; then
"$REPORTER_SCRIPT" status
else
echo '{"error":"Reporter script not found"}'
fi
}
# Get report history
get_history() {
read -r input
json_load "$input"
json_get_var lines lines
[ -z "$lines" ] && lines=20
if [ -x "$REPORTER_SCRIPT" ]; then
"$REPORTER_SCRIPT" history "$lines"
else
echo '{"history":[]}'
fi
}
# Check IP reputation
check_ip() {
read -r input
json_load "$input"
json_get_var ip ip
json_init
if [ -z "$ip" ]; then
json_add_boolean "success" 0
json_add_string "error" "No IP provided"
json_dump
return
fi
if [ -x "$REPORTER_SCRIPT" ]; then
local result
result=$("$REPORTER_SCRIPT" check "$ip" 2>/dev/null)
if echo "$result" | grep -q '"abuseConfidenceScore"'; then
# Parse and return relevant fields
local score=$(echo "$result" | jsonfilter -e '@.data.abuseConfidenceScore' 2>/dev/null || echo "0")
local reports=$(echo "$result" | jsonfilter -e '@.data.totalReports' 2>/dev/null || echo "0")
local country=$(echo "$result" | jsonfilter -e '@.data.countryCode' 2>/dev/null || echo "?")
local isp=$(echo "$result" | jsonfilter -e '@.data.isp' 2>/dev/null || echo "Unknown")
local domain=$(echo "$result" | jsonfilter -e '@.data.domain' 2>/dev/null || echo "")
local is_public=$(echo "$result" | jsonfilter -e '@.data.isPublic' 2>/dev/null || echo "true")
local last_reported=$(echo "$result" | jsonfilter -e '@.data.lastReportedAt' 2>/dev/null || echo "")
json_add_boolean "success" 1
json_add_string "ip" "$ip"
json_add_int "confidence_score" "$score"
json_add_int "total_reports" "$reports"
json_add_string "country" "$country"
json_add_string "isp" "$isp"
json_add_string "domain" "$domain"
json_add_boolean "is_public" "$is_public"
json_add_string "last_reported" "$last_reported"
else
json_add_boolean "success" 0
json_add_string "error" "Failed to check IP"
fi
else
json_add_boolean "success" 0
json_add_string "error" "Reporter script not found"
fi
json_dump
}
# Trigger manual report run
do_report() {
json_init
if [ -x "$REPORTER_SCRIPT" ]; then
"$REPORTER_SCRIPT" report >/dev/null 2>&1 &
json_add_boolean "success" 1
json_add_string "message" "Report run started in background"
else
json_add_boolean "success" 0
json_add_string "error" "Reporter script not found"
fi
json_dump
}
# Enable/disable reporter
set_enabled() {
read -r input
json_load "$input"
json_get_var enabled enabled
json_init
if [ "$enabled" = "1" ] || [ "$enabled" = "true" ]; then
uci set "${UCI_CONFIG}.global.enabled=1"
uci commit "$UCI_CONFIG"
json_add_boolean "success" 1
json_add_string "message" "AbuseIPDB reporter enabled"
else
uci set "${UCI_CONFIG}.global.enabled=0"
uci commit "$UCI_CONFIG"
json_add_boolean "success" 1
json_add_string "message" "AbuseIPDB reporter disabled"
fi
json_dump
}
# Set API key
set_api_key() {
read -r input
json_load "$input"
json_get_var api_key api_key
json_init
if [ -z "$api_key" ]; then
json_add_boolean "success" 0
json_add_string "error" "No API key provided"
json_dump
return
fi
uci set "${UCI_CONFIG}.global.api_key=$api_key"
uci commit "$UCI_CONFIG"
json_add_boolean "success" 1
json_add_string "message" "API key configured"
json_dump
}
# Get configuration
get_config() {
local enabled=$(uci -q get "${UCI_CONFIG}.global.enabled" || echo "0")
local api_key=$(uci -q get "${UCI_CONFIG}.global.api_key" || echo "")
local confidence=$(uci -q get "${UCI_CONFIG}.global.confidence_threshold" || echo "80")
local categories=$(uci -q get "${UCI_CONFIG}.global.categories" || echo "18,21")
local interval=$(uci -q get "${UCI_CONFIG}.global.report_interval" || echo "15")
local max_reports=$(uci -q get "${UCI_CONFIG}.global.max_reports_per_run" || echo "50")
local cooldown=$(uci -q get "${UCI_CONFIG}.global.cooldown_minutes" || echo "15")
local comment=$(uci -q get "${UCI_CONFIG}.global.comment_prefix" || echo "Blocked by SecuBox CrowdSec")
json_init
json_add_boolean "enabled" "$enabled"
json_add_boolean "api_key_set" "$( [ -n "$api_key" ] && echo 1 || echo 0 )"
json_add_int "confidence_threshold" "$confidence"
json_add_string "categories" "$categories"
json_add_int "report_interval" "$interval"
json_add_int "max_reports_per_run" "$max_reports"
json_add_int "cooldown_minutes" "$cooldown"
json_add_string "comment_prefix" "$comment"
json_dump
}
# Save configuration
save_config() {
read -r input
json_load "$input"
json_get_var confidence confidence_threshold
json_get_var categories categories
json_get_var interval report_interval
json_get_var max_reports max_reports_per_run
json_get_var cooldown cooldown_minutes
json_get_var comment comment_prefix
[ -n "$confidence" ] && uci set "${UCI_CONFIG}.global.confidence_threshold=$confidence"
[ -n "$categories" ] && uci set "${UCI_CONFIG}.global.categories=$categories"
[ -n "$interval" ] && uci set "${UCI_CONFIG}.global.report_interval=$interval"
[ -n "$max_reports" ] && uci set "${UCI_CONFIG}.global.max_reports_per_run=$max_reports"
[ -n "$cooldown" ] && uci set "${UCI_CONFIG}.global.cooldown_minutes=$cooldown"
[ -n "$comment" ] && uci set "${UCI_CONFIG}.global.comment_prefix=$comment"
uci commit "$UCI_CONFIG"
json_init
json_add_boolean "success" 1
json_add_string "message" "Configuration saved"
json_dump
}
# Get logs
get_logs() {
read -r input
json_load "$input"
json_get_var lines lines
[ -z "$lines" ] && lines=50
json_init
json_add_array "logs"
if [ -f /var/log/crowdsec-reporter.log ]; then
tail -n "$lines" /var/log/crowdsec-reporter.log 2>/dev/null | while IFS= read -r line; do
json_add_string "" "$line"
done
fi
json_close_array
json_dump
}
# RPCD list method
case "$1" in
list)
echo '{"status":{},"history":{"lines":"int"},"check_ip":{"ip":"str"},"report":{},"set_enabled":{"enabled":"bool"},"set_api_key":{"api_key":"str"},"get_config":{},"save_config":{"confidence_threshold":"int","categories":"str","report_interval":"int","max_reports_per_run":"int","cooldown_minutes":"int","comment_prefix":"str"},"logs":{"lines":"int"}}'
;;
call)
case "$2" in
status) get_status ;;
history) get_history ;;
check_ip) check_ip ;;
report) do_report ;;
set_enabled) set_enabled ;;
set_api_key) set_api_key ;;
get_config) get_config ;;
save_config) save_config ;;
logs) get_logs ;;
*) echo '{"error":"Unknown method"}' ;;
esac
;;
esac

View File

@ -0,0 +1,359 @@
#!/bin/sh
# crowdsec-reporter.sh — SecuBox AbuseIPDB Reporter
# Reports CrowdSec blocked IPs to AbuseIPDB community database
# Inspired by SysWarden reporter pattern — shell version for OpenWrt
ABUSEIPDB_API_URL="https://api.abuseipdb.com/api/v2/report"
ABUSEIPDB_CHECK_URL="https://api.abuseipdb.com/api/v2/check"
UCI_CONFIG="crowdsec_abuseipdb"
LOG_FILE="/var/log/crowdsec-reporter.log"
STATE_DIR="/var/lib/crowdsec-reporter"
REPORTED_FILE="${STATE_DIR}/reported.txt"
# Load configuration from UCI
load_config() {
ENABLED=$(uci -q get "${UCI_CONFIG}.global.enabled" || echo "0")
API_KEY=$(uci -q get "${UCI_CONFIG}.global.api_key" || echo "")
CONFIDENCE_THRESHOLD=$(uci -q get "${UCI_CONFIG}.global.confidence_threshold" || echo "80")
CATEGORIES=$(uci -q get "${UCI_CONFIG}.global.categories" || echo "18,21")
MAX_REPORTS=$(uci -q get "${UCI_CONFIG}.global.max_reports_per_run" || echo "50")
COOLDOWN_MINUTES=$(uci -q get "${UCI_CONFIG}.global.cooldown_minutes" || echo "15")
COMMENT_PREFIX=$(uci -q get "${UCI_CONFIG}.global.comment_prefix" || echo "Blocked by SecuBox CrowdSec")
}
# Log message with timestamp
log_msg() {
local level="$1"
local msg="$2"
echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $msg" >> "$LOG_FILE"
[ "$level" = "ERROR" ] && echo "[$level] $msg" >&2
}
# Initialize state directory
init_state() {
mkdir -p "$STATE_DIR"
touch "$REPORTED_FILE"
# Rotate old reported entries (keep 7 days)
if [ -f "$REPORTED_FILE" ]; then
local cutoff=$(date -d "7 days ago" +%s 2>/dev/null || date -D "%s" -d "$(( $(date +%s) - 604800 ))" +%s 2>/dev/null || echo "0")
local tmp=$(mktemp)
while IFS='|' read -r ip timestamp; do
[ -n "$timestamp" ] && [ "$timestamp" -gt "$cutoff" ] 2>/dev/null && echo "$ip|$timestamp"
done < "$REPORTED_FILE" > "$tmp"
mv "$tmp" "$REPORTED_FILE"
fi
}
# Check if IP was recently reported (cooldown)
is_recently_reported() {
local ip="$1"
local now=$(date +%s)
local cooldown_seconds=$((COOLDOWN_MINUTES * 60))
if grep -q "^${ip}|" "$REPORTED_FILE" 2>/dev/null; then
local last_report=$(grep "^${ip}|" "$REPORTED_FILE" | tail -1 | cut -d'|' -f2)
if [ -n "$last_report" ]; then
local elapsed=$((now - last_report))
[ "$elapsed" -lt "$cooldown_seconds" ] && return 0
fi
fi
return 1
}
# Mark IP as reported
mark_reported() {
local ip="$1"
local now=$(date +%s)
# Remove old entry for this IP
local tmp=$(mktemp)
grep -v "^${ip}|" "$REPORTED_FILE" > "$tmp" 2>/dev/null || true
mv "$tmp" "$REPORTED_FILE"
# Add new entry
echo "${ip}|${now}" >> "$REPORTED_FILE"
}
# Get recent CrowdSec decisions
get_recent_decisions() {
if command -v cscli >/dev/null 2>&1; then
# Get decisions from last hour with high confidence
cscli decisions list --output json 2>/dev/null | \
jsonfilter -e '@[*]' 2>/dev/null | while read -r decision; do
local ip=$(echo "$decision" | jsonfilter -e '@.value' 2>/dev/null)
local scope=$(echo "$decision" | jsonfilter -e '@.scope' 2>/dev/null)
local scenario=$(echo "$decision" | jsonfilter -e '@.scenario' 2>/dev/null)
# Only report IP-scoped decisions
[ "$scope" = "Ip" ] && [ -n "$ip" ] && echo "$ip|$scenario"
done
else
# Fallback: parse CrowdSec logs
if [ -f /var/log/crowdsec.log ]; then
grep -h "ban" /var/log/crowdsec.log 2>/dev/null | \
grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort -u | \
while read -r ip; do
echo "$ip|unknown"
done
fi
fi
}
# Report single IP to AbuseIPDB
report_ip() {
local ip="$1"
local scenario="$2"
# Skip if no API key
[ -z "$API_KEY" ] && return 1
# Skip if recently reported
if is_recently_reported "$ip"; then
log_msg "DEBUG" "Skipping $ip (cooldown active)"
return 0
fi
# Skip private/local IPs
case "$ip" in
10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|127.*|0.*)
log_msg "DEBUG" "Skipping private IP: $ip"
return 0
;;
esac
# Build comment with scenario info
local comment="${COMMENT_PREFIX}"
[ -n "$scenario" ] && [ "$scenario" != "unknown" ] && comment="${comment} - ${scenario}"
comment=$(echo "$comment" | sed 's/ /+/g; s/[^a-zA-Z0-9+_-]//g')
# Make API request
local response
response=$(wget -q -O- \
--header="Key: ${API_KEY}" \
--header="Accept: application/json" \
--post-data="ip=${ip}&categories=${CATEGORIES}&comment=${comment}" \
"$ABUSEIPDB_API_URL" 2>/dev/null)
if echo "$response" | grep -q '"abuseConfidenceScore"'; then
local score=$(echo "$response" | jsonfilter -e '@.data.abuseConfidenceScore' 2>/dev/null || echo "?")
mark_reported "$ip"
log_msg "INFO" "Reported $ip to AbuseIPDB (score: $score)"
return 0
else
local error=$(echo "$response" | jsonfilter -e '@.errors[0].detail' 2>/dev/null || echo "Unknown error")
log_msg "ERROR" "Failed to report $ip: $error"
return 1
fi
}
# Check IP reputation on AbuseIPDB
check_ip() {
local ip="$1"
[ -z "$API_KEY" ] && echo '{"error":"No API key configured"}' && return 1
local response
response=$(wget -q -O- \
--header="Key: ${API_KEY}" \
--header="Accept: application/json" \
"${ABUSEIPDB_CHECK_URL}?ipAddress=${ip}&maxAgeInDays=90" 2>/dev/null)
echo "$response"
}
# Update statistics in UCI
update_stats() {
local reported="$1"
# Get current stats
local today=$(uci -q get "${UCI_CONFIG}.stats.reported_today" || echo "0")
local week=$(uci -q get "${UCI_CONFIG}.stats.reported_week" || echo "0")
local total=$(uci -q get "${UCI_CONFIG}.stats.reported_total" || echo "0")
# Update counters
today=$((today + reported))
week=$((week + reported))
total=$((total + reported))
uci set "${UCI_CONFIG}.stats.reported_today=$today"
uci set "${UCI_CONFIG}.stats.reported_week=$week"
uci set "${UCI_CONFIG}.stats.reported_total=$total"
uci set "${UCI_CONFIG}.stats.last_report=$(date +%s)"
uci commit "$UCI_CONFIG"
}
# Reset daily stats (called by cron at midnight)
reset_daily_stats() {
uci set "${UCI_CONFIG}.stats.reported_today=0"
uci commit "$UCI_CONFIG"
log_msg "INFO" "Daily stats reset"
}
# Reset weekly stats (called by cron on Monday)
reset_weekly_stats() {
uci set "${UCI_CONFIG}.stats.reported_week=0"
uci commit "$UCI_CONFIG"
log_msg "INFO" "Weekly stats reset"
}
# Main reporting routine
do_report() {
load_config
if [ "$ENABLED" != "1" ]; then
log_msg "INFO" "AbuseIPDB reporter is disabled"
return 0
fi
if [ -z "$API_KEY" ]; then
log_msg "ERROR" "No API key configured"
return 1
fi
init_state
log_msg "INFO" "Starting AbuseIPDB reporting run"
local reported=0
local skipped=0
get_recent_decisions | head -n "$MAX_REPORTS" | while IFS='|' read -r ip scenario; do
[ -z "$ip" ] && continue
if report_ip "$ip" "$scenario"; then
reported=$((reported + 1))
else
skipped=$((skipped + 1))
fi
# Rate limiting - small delay between requests
sleep 1
done
# Count actually reported (from subshell issue, re-count)
reported=$(grep "$(date '+%Y-%m-%d')" "$LOG_FILE" 2>/dev/null | grep -c "Reported.*to AbuseIPDB" || echo "0")
update_stats "$reported"
log_msg "INFO" "Reporting run completed: $reported IPs reported"
}
# Get status for RPCD
get_status() {
load_config
local reported_today=$(uci -q get "${UCI_CONFIG}.stats.reported_today" || echo "0")
local reported_week=$(uci -q get "${UCI_CONFIG}.stats.reported_week" || echo "0")
local reported_total=$(uci -q get "${UCI_CONFIG}.stats.reported_total" || echo "0")
local last_report=$(uci -q get "${UCI_CONFIG}.stats.last_report" || echo "0")
local pending=$(get_recent_decisions 2>/dev/null | wc -l || echo "0")
cat <<EOF
{
"enabled": $( [ "$ENABLED" = "1" ] && echo "true" || echo "false" ),
"api_key_configured": $( [ -n "$API_KEY" ] && echo "true" || echo "false" ),
"confidence_threshold": $CONFIDENCE_THRESHOLD,
"categories": "$CATEGORIES",
"cooldown_minutes": $COOLDOWN_MINUTES,
"reported_today": $reported_today,
"reported_week": $reported_week,
"reported_total": $reported_total,
"last_report": $last_report,
"pending_ips": $pending
}
EOF
}
# Get recent report history
get_history() {
local lines="${1:-20}"
echo '{"history":['
local first=1
grep "Reported.*to AbuseIPDB" "$LOG_FILE" 2>/dev/null | tail -n "$lines" | while IFS= read -r line; do
local timestamp=$(echo "$line" | cut -d' ' -f1-2)
local ip=$(echo "$line" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+')
local score=$(echo "$line" | grep -oE 'score: [0-9]+' | cut -d' ' -f2)
[ $first -eq 0 ] && echo ","
first=0
echo "{\"timestamp\":\"$timestamp\",\"ip\":\"$ip\",\"score\":\"${score:-?}\"}"
done
echo ']}'
}
# Print usage
usage() {
cat <<EOF
Usage: $0 <command> [options]
Commands:
report Run reporting cycle (report CrowdSec decisions to AbuseIPDB)
check <ip> Check IP reputation on AbuseIPDB
status Show reporter status (JSON)
history [n] Show last n reports (default: 20)
logs [n] Show last n log lines (default: 50)
reset-daily Reset daily stats counter
reset-weekly Reset weekly stats counter
enable Enable the reporter
disable Disable the reporter
set-key <key> Set AbuseIPDB API key
help Show this help
EOF
}
# Main entry point
case "$1" in
report)
do_report
;;
check)
[ -z "$2" ] && echo "Usage: $0 check <ip>" && exit 1
load_config
check_ip "$2"
;;
status)
get_status
;;
history)
get_history "${2:-20}"
;;
logs)
tail -n "${2:-50}" "$LOG_FILE" 2>/dev/null || echo "No logs available"
;;
reset-daily)
reset_daily_stats
;;
reset-weekly)
reset_weekly_stats
;;
enable)
uci set "${UCI_CONFIG}.global.enabled=1"
uci commit "$UCI_CONFIG"
echo "AbuseIPDB reporter enabled"
;;
disable)
uci set "${UCI_CONFIG}.global.enabled=0"
uci commit "$UCI_CONFIG"
echo "AbuseIPDB reporter disabled"
;;
set-key)
[ -z "$2" ] && echo "Usage: $0 set-key <api_key>" && exit 1
uci set "${UCI_CONFIG}.global.api_key=$2"
uci commit "$UCI_CONFIG"
echo "API key configured"
;;
help|--help|-h)
usage
;;
*)
usage
exit 1
;;
esac
exit 0

View File

@ -41,6 +41,14 @@
"path": "crowdsec-dashboard/bouncers"
}
},
"admin/secubox/security/crowdsec/reporter": {
"title": "AbuseIPDB",
"order": 50,
"action": {
"type": "view",
"path": "crowdsec-dashboard/reporter"
}
},
"admin/secubox/security/crowdsec/setup": {
"title": "Setup",
"order": 90,

View File

@ -3,6 +3,13 @@
"description": "Grant access to LuCI CrowdSec Dashboard",
"read": {
"ubus": {
"luci.crowdsec-abuseipdb": [
"status",
"history",
"check_ip",
"get_config",
"logs"
],
"luci.crowdsec-dashboard": [
"get_overview",
"decisions",
@ -33,10 +40,16 @@
"uci": [ "get", "state", "configs" ],
"file": [ "read", "stat", "exec" ]
},
"uci": [ "crowdsec", "crowdsec-dashboard" ]
"uci": [ "crowdsec", "crowdsec-dashboard", "crowdsec_abuseipdb" ]
},
"write": {
"ubus": {
"luci.crowdsec-abuseipdb": [
"report",
"set_enabled",
"set_api_key",
"save_config"
],
"luci.crowdsec-dashboard": [
"ban",
"unban",
@ -62,7 +75,7 @@
"uci": [ "set", "add", "delete", "rename", "order", "commit", "apply", "confirm", "rollback" ],
"file": [ "exec" ]
},
"uci": [ "crowdsec", "crowdsec-dashboard" ]
"uci": [ "crowdsec", "crowdsec-dashboard", "crowdsec_abuseipdb" ]
}
}
}

View File

@ -0,0 +1,17 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-ipblocklist
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=Gandalf <gandalf@cybermind.fr>
PKG_LICENSE:=Apache-2.0
LUCI_TITLE:=SecuBox IP Blocklist - Static threat defense dashboard
LUCI_DEPENDS:=+secubox-app-ipblocklist +luci-base
include $(TOPDIR)/feeds/luci/luci.mk
define Package/luci-app-ipblocklist/conffiles
endef
$(eval $(call BuildPackage,luci-app-ipblocklist))

View File

@ -0,0 +1,131 @@
'use strict';
'require rpc';
var callStatus = rpc.declare({
object: 'luci.ipblocklist',
method: 'status',
expect: { }
});
var callLogs = rpc.declare({
object: 'luci.ipblocklist',
method: 'logs',
params: ['lines'],
expect: { }
});
var callSources = rpc.declare({
object: 'luci.ipblocklist',
method: 'sources',
expect: { }
});
var callWhitelist = rpc.declare({
object: 'luci.ipblocklist',
method: 'whitelist',
expect: { }
});
var callUpdate = rpc.declare({
object: 'luci.ipblocklist',
method: 'update',
expect: { }
});
var callFlush = rpc.declare({
object: 'luci.ipblocklist',
method: 'flush',
expect: { }
});
var callTestIp = rpc.declare({
object: 'luci.ipblocklist',
method: 'test_ip',
params: ['ip'],
expect: { }
});
var callSetEnabled = rpc.declare({
object: 'luci.ipblocklist',
method: 'set_enabled',
params: ['enabled'],
expect: { }
});
var callAddSource = rpc.declare({
object: 'luci.ipblocklist',
method: 'add_source',
params: ['url'],
expect: { }
});
var callRemoveSource = rpc.declare({
object: 'luci.ipblocklist',
method: 'remove_source',
params: ['url'],
expect: { }
});
var callAddWhitelist = rpc.declare({
object: 'luci.ipblocklist',
method: 'add_whitelist',
params: ['entry'],
expect: { }
});
var callRemoveWhitelist = rpc.declare({
object: 'luci.ipblocklist',
method: 'remove_whitelist',
params: ['entry'],
expect: { }
});
return L.Class.extend({
getStatus: function() {
return callStatus();
},
getLogs: function(lines) {
return callLogs(lines || 50);
},
getSources: function() {
return callSources();
},
getWhitelist: function() {
return callWhitelist();
},
update: function() {
return callUpdate();
},
flush: function() {
return callFlush();
},
testIp: function(ip) {
return callTestIp(ip);
},
setEnabled: function(enabled) {
return callSetEnabled(enabled ? 1 : 0);
},
addSource: function(url) {
return callAddSource(url);
},
removeSource: function(url) {
return callRemoveSource(url);
},
addWhitelist: function(entry) {
return callAddWhitelist(entry);
},
removeWhitelist: function(entry) {
return callRemoveWhitelist(entry);
}
});

View File

@ -0,0 +1,353 @@
'use strict';
'require view';
'require dom';
'require poll';
'require ui';
'require ipblocklist.api as api';
return view.extend({
refreshInterval: 30,
statusData: null,
load: function() {
return Promise.all([
api.getStatus(),
api.getSources(),
api.getWhitelist(),
api.getLogs(20)
]);
},
formatBytes: function(bytes) {
if (bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
formatTimestamp: function(ts) {
if (!ts || ts === 0) return 'Never';
var d = new Date(ts * 1000);
return d.toLocaleString();
},
renderStatusCard: function(status) {
var enabled = status.enabled === true || status.enabled === '1' || status.enabled === 1;
var entryCount = parseInt(status.entry_count) || 0;
var maxEntries = parseInt(status.max_entries) || 200000;
var memoryBytes = parseInt(status.memory_bytes) || 0;
var usagePercent = maxEntries > 0 ? Math.round((entryCount / maxEntries) * 100) : 0;
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Status'),
E('div', { 'class': 'table', 'style': 'margin-bottom: 1em' }, [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'width: 200px; font-weight: bold' }, 'Service Status'),
E('div', { 'class': 'td' }, [
E('span', {
'class': enabled ? 'badge success' : 'badge warning',
'style': 'padding: 4px 12px; border-radius: 4px; background: ' + (enabled ? '#4CAF50' : '#FF9800') + '; color: white;'
}, enabled ? 'Enabled' : 'Disabled')
])
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'font-weight: bold' }, 'Blocked IPs'),
E('div', { 'class': 'td' }, [
E('strong', { 'style': 'font-size: 1.2em; color: #f44336' }, entryCount.toLocaleString()),
E('span', { 'style': 'color: #888; margin-left: 8px' }, '/ ' + maxEntries.toLocaleString() + ' (' + usagePercent + '%)')
])
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'font-weight: bold' }, 'Memory Usage'),
E('div', { 'class': 'td' }, this.formatBytes(memoryBytes))
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'font-weight: bold' }, 'Firewall Backend'),
E('div', { 'class': 'td' }, status.firewall_backend || 'Unknown')
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'font-weight: bold' }, 'Last Update'),
E('div', { 'class': 'td' }, this.formatTimestamp(status.last_update))
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'font-weight: bold' }, 'Sources'),
E('div', { 'class': 'td' }, (status.source_count || 0) + ' configured')
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'font-weight: bold' }, 'Whitelist'),
E('div', { 'class': 'td' }, (status.whitelist_count || 0) + ' entries')
])
])
]);
},
renderControls: function(status) {
var self = this;
var enabled = status.enabled === true || status.enabled === '1' || status.enabled === 1;
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Controls'),
E('div', { 'style': 'display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 1em' }, [
E('button', {
'class': enabled ? 'btn cbi-button-remove' : 'btn cbi-button-apply',
'click': ui.createHandlerFn(this, function() {
return api.setEnabled(!enabled).then(function() {
ui.addNotification(null, E('p', {}, enabled ? 'IP Blocklist disabled' : 'IP Blocklist enabled'));
return self.refresh();
});
})
}, enabled ? 'Disable' : 'Enable'),
E('button', {
'class': 'btn cbi-button-action',
'click': ui.createHandlerFn(this, function() {
ui.showModal('Updating...', [
E('p', { 'class': 'spinning' }, 'Downloading and applying blocklists...')
]);
return api.update().then(function(res) {
ui.hideModal();
ui.addNotification(null, E('p', {}, res.message || 'Update started'));
setTimeout(function() { self.refresh(); }, 3000);
});
})
}, 'Update Now'),
E('button', {
'class': 'btn cbi-button-negative',
'click': ui.createHandlerFn(this, function() {
ui.showModal('Confirm Flush', [
E('p', {}, 'This will remove all IPs from the blocklist. Continue?'),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, 'Cancel'),
' ',
E('button', {
'class': 'btn cbi-button-negative',
'click': function() {
ui.hideModal();
return api.flush().then(function(res) {
ui.addNotification(null, E('p', {}, res.message || 'Blocklist flushed'));
return self.refresh();
});
}
}, 'Flush')
])
]);
})
}, 'Flush')
])
]);
},
renderTestIp: function() {
var self = this;
var input = E('input', {
'type': 'text',
'placeholder': 'Enter IP address...',
'style': 'width: 200px; margin-right: 10px'
});
var result = E('span', { 'id': 'test-result', 'style': 'margin-left: 10px' });
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Test IP'),
E('div', { 'style': 'display: flex; align-items: center; flex-wrap: wrap; gap: 10px' }, [
input,
E('button', {
'class': 'btn cbi-button-action',
'click': function() {
var ip = input.value.trim();
if (!ip) {
result.textContent = 'Please enter an IP';
return;
}
result.textContent = 'Testing...';
api.testIp(ip).then(function(res) {
if (res.blocked) {
result.innerHTML = '<span style="color: #f44336; font-weight: bold">BLOCKED</span>';
} else {
result.innerHTML = '<span style="color: #4CAF50; font-weight: bold">ALLOWED</span>';
}
}).catch(function(e) {
result.textContent = 'Error: ' + e.message;
});
}
}, 'Test'),
result
])
]);
},
renderSources: function(sources) {
var self = this;
var sourceList = (sources && sources.sources) || [];
var input = E('input', {
'type': 'text',
'placeholder': 'https://...',
'style': 'flex: 1; min-width: 300px; margin-right: 10px'
});
var rows = sourceList.map(function(src) {
return E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'word-break: break-all' }, src),
E('div', { 'class': 'td', 'style': 'width: 80px; text-align: right' }, [
E('button', {
'class': 'btn cbi-button-remove',
'style': 'padding: 2px 8px',
'click': function() {
return api.removeSource(src).then(function() {
ui.addNotification(null, E('p', {}, 'Source removed'));
return self.refresh();
});
}
}, 'Remove')
])
]);
});
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Blocklist Sources'),
E('div', { 'class': 'table' }, rows.length > 0 ? rows : [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'color: #888' }, 'No sources configured')
])
]),
E('div', { 'style': 'display: flex; align-items: center; margin-top: 1em; flex-wrap: wrap; gap: 10px' }, [
input,
E('button', {
'class': 'btn cbi-button-add',
'click': function() {
var url = input.value.trim();
if (!url || !url.startsWith('http')) {
ui.addNotification(null, E('p', {}, 'Please enter a valid URL'));
return;
}
return api.addSource(url).then(function() {
input.value = '';
ui.addNotification(null, E('p', {}, 'Source added'));
return self.refresh();
});
}
}, 'Add Source')
])
]);
},
renderWhitelist: function(whitelist) {
var self = this;
var entries = (whitelist && whitelist.entries) || [];
var input = E('input', {
'type': 'text',
'placeholder': 'IP or CIDR (e.g., 192.168.1.0/24)',
'style': 'width: 200px; margin-right: 10px'
});
var rows = entries.map(function(entry) {
return E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, entry),
E('div', { 'class': 'td', 'style': 'width: 80px; text-align: right' }, [
E('button', {
'class': 'btn cbi-button-remove',
'style': 'padding: 2px 8px',
'click': function() {
return api.removeWhitelist(entry).then(function() {
ui.addNotification(null, E('p', {}, 'Entry removed from whitelist'));
return self.refresh();
});
}
}, 'Remove')
])
]);
});
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Whitelist (Excluded IPs)'),
E('div', { 'class': 'table' }, rows.length > 0 ? rows : [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'color: #888' }, 'No whitelist entries')
])
]),
E('div', { 'style': 'display: flex; align-items: center; margin-top: 1em; flex-wrap: wrap; gap: 10px' }, [
input,
E('button', {
'class': 'btn cbi-button-add',
'click': function() {
var entry = input.value.trim();
if (!entry) {
ui.addNotification(null, E('p', {}, 'Please enter an IP or CIDR'));
return;
}
return api.addWhitelist(entry).then(function() {
input.value = '';
ui.addNotification(null, E('p', {}, 'Added to whitelist'));
return self.refresh();
});
}
}, 'Add')
])
]);
},
renderLogs: function(logs) {
var logEntries = (logs && logs.logs) || [];
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Recent Activity'),
E('div', {
'style': 'background: #1a1a2e; color: #0f0; font-family: monospace; padding: 1em; border-radius: 4px; max-height: 300px; overflow-y: auto; font-size: 0.85em'
}, logEntries.length > 0 ?
logEntries.map(function(line) {
var color = '#0f0';
if (line.indexOf('[ERROR]') >= 0) color = '#f44336';
else if (line.indexOf('[WARN]') >= 0) color = '#FF9800';
else if (line.indexOf('[INFO]') >= 0) color = '#00bcd4';
return E('div', { 'style': 'color: ' + color + '; margin-bottom: 2px' }, line);
}) :
E('div', { 'style': 'color: #888' }, 'No log entries yet')
)
]);
},
refresh: function() {
var self = this;
return this.load().then(function(data) {
var container = document.getElementById('ipblocklist-container');
if (container) {
dom.content(container, self.renderContent(data));
}
});
},
renderContent: function(data) {
var status = data[0] || {};
var sources = data[1] || {};
var whitelist = data[2] || {};
var logs = data[3] || {};
return E('div', {}, [
E('h2', {}, 'IP Blocklist'),
E('p', { 'style': 'color: #888; margin-bottom: 1em' },
'Pre-emptive static threat defense layer. Blocks known malicious IPs at kernel level before CrowdSec reactive analysis.'),
this.renderStatusCard(status),
this.renderControls(status),
this.renderTestIp(),
this.renderSources(sources),
this.renderWhitelist(whitelist),
this.renderLogs(logs)
]);
},
render: function(data) {
var self = this;
poll.add(function() {
return self.refresh();
}, this.refreshInterval);
return E('div', { 'id': 'ipblocklist-container' }, this.renderContent(data));
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,336 @@
#!/bin/sh
# RPCD handler for SecuBox IP Blocklist
# Provides API for LuCI dashboard
. /usr/share/libubox/jshn.sh
UCI_CONFIG="ipblocklist"
LOG_FILE="/var/log/ipblocklist.log"
WHITELIST_FILE="/etc/ipblocklist/whitelist.txt"
UPDATE_SCRIPT="/usr/sbin/ipblocklist-update.sh"
# Get status information
get_status() {
local enabled=$(uci -q get "${UCI_CONFIG}.global.enabled" || echo "0")
local ipset_name=$(uci -q get "${UCI_CONFIG}.global.ipset_name" || echo "secubox_blocklist")
local max_entries=$(uci -q get "${UCI_CONFIG}.global.max_entries" || echo "200000")
# Get ipset statistics
local entry_count=0
local memory_bytes=0
if command -v ipset >/dev/null 2>&1 && ipset list "$ipset_name" >/dev/null 2>&1; then
entry_count=$(ipset list "$ipset_name" 2>/dev/null | grep -c '^[0-9]' || echo "0")
memory_bytes=$(ipset list "$ipset_name" 2>/dev/null | grep "memsize" | awk '{print $2}' || echo "0")
fi
# Get last update timestamp
local last_update=0
if [ -f "$LOG_FILE" ]; then
last_update=$(stat -c %Y "$LOG_FILE" 2>/dev/null || echo "0")
fi
# Detect firewall backend
local fw_backend="unknown"
if command -v nft >/dev/null 2>&1 && nft list tables 2>/dev/null | grep -q "fw4"; then
fw_backend="nftables"
elif command -v iptables >/dev/null 2>&1; then
fw_backend="iptables"
fi
# Count sources
local source_count=$(uci -q get "${UCI_CONFIG}.global.sources" 2>/dev/null | wc -w || echo "0")
# Count whitelist entries
local whitelist_count=0
if [ -f "$WHITELIST_FILE" ]; then
whitelist_count=$(grep -v '^#' "$WHITELIST_FILE" 2>/dev/null | grep -v '^$' | wc -l || echo "0")
fi
json_init
json_add_boolean "enabled" "$enabled"
json_add_string "ipset_name" "$ipset_name"
json_add_int "entry_count" "$entry_count"
json_add_int "max_entries" "$max_entries"
json_add_int "memory_bytes" "${memory_bytes:-0}"
json_add_string "firewall_backend" "$fw_backend"
json_add_int "last_update" "$last_update"
json_add_int "source_count" "$source_count"
json_add_int "whitelist_count" "$whitelist_count"
json_dump
}
# Get log entries
get_logs() {
read -r input
json_load "$input"
json_get_var lines lines
[ -z "$lines" ] && lines=50
json_init
json_add_array "logs"
if [ -f "$LOG_FILE" ]; then
tail -n "$lines" "$LOG_FILE" 2>/dev/null | while IFS= read -r line; do
json_add_string "" "$line"
done
fi
json_close_array
json_dump
}
# Get configured sources
get_sources() {
json_init
json_add_array "sources"
# Get sources from UCI
local idx=0
while true; do
local src=$(uci -q get "${UCI_CONFIG}.global.sources" 2>/dev/null | awk -v n=$((idx+1)) '{print $n}')
[ -z "$src" ] && break
json_add_string "" "$src"
idx=$((idx + 1))
done
# Alternative: iterate UCI list
if [ $idx -eq 0 ]; then
for src in $(uci -q get "${UCI_CONFIG}.global.sources" 2>/dev/null); do
json_add_string "" "$src"
done
fi
json_close_array
json_dump
}
# Get whitelist entries
get_whitelist() {
json_init
json_add_array "entries"
if [ -f "$WHITELIST_FILE" ]; then
grep -v '^#' "$WHITELIST_FILE" 2>/dev/null | grep -v '^$' | while IFS= read -r line; do
json_add_string "" "$line"
done
fi
json_close_array
json_dump
}
# Trigger blocklist update
do_update() {
json_init
if [ -x "$UPDATE_SCRIPT" ]; then
"$UPDATE_SCRIPT" update >/dev/null 2>&1 &
json_add_boolean "success" 1
json_add_string "message" "Update started in background"
else
json_add_boolean "success" 0
json_add_string "error" "Update script not found"
fi
json_dump
}
# Flush blocklist
do_flush() {
json_init
if [ -x "$UPDATE_SCRIPT" ]; then
"$UPDATE_SCRIPT" flush >/dev/null 2>&1
json_add_boolean "success" 1
json_add_string "message" "Blocklist flushed"
else
json_add_boolean "success" 0
json_add_string "error" "Update script not found"
fi
json_dump
}
# Test if IP is blocked
do_test_ip() {
read -r input
json_load "$input"
json_get_var ip ip
json_init
if [ -z "$ip" ]; then
json_add_boolean "success" 0
json_add_string "error" "No IP provided"
json_dump
return
fi
local ipset_name=$(uci -q get "${UCI_CONFIG}.global.ipset_name" || echo "secubox_blocklist")
if ipset test "$ipset_name" "$ip" 2>/dev/null; then
json_add_boolean "success" 1
json_add_boolean "blocked" 1
json_add_string "message" "IP $ip is in blocklist"
else
json_add_boolean "success" 1
json_add_boolean "blocked" 0
json_add_string "message" "IP $ip is not blocked"
fi
json_dump
}
# Set enabled state
do_set_enabled() {
read -r input
json_load "$input"
json_get_var enabled enabled
json_init
if [ "$enabled" = "1" ] || [ "$enabled" = "true" ]; then
uci set "${UCI_CONFIG}.global.enabled=1"
uci commit "$UCI_CONFIG"
# Start blocklist
[ -x "$UPDATE_SCRIPT" ] && "$UPDATE_SCRIPT" start >/dev/null 2>&1 &
json_add_boolean "success" 1
json_add_string "message" "IP Blocklist enabled"
else
uci set "${UCI_CONFIG}.global.enabled=0"
uci commit "$UCI_CONFIG"
# Stop blocklist
[ -x "$UPDATE_SCRIPT" ] && "$UPDATE_SCRIPT" stop >/dev/null 2>&1
json_add_boolean "success" 1
json_add_string "message" "IP Blocklist disabled"
fi
json_dump
}
# Add a source URL
do_add_source() {
read -r input
json_load "$input"
json_get_var url url
json_init
if [ -z "$url" ]; then
json_add_boolean "success" 0
json_add_string "error" "No URL provided"
json_dump
return
fi
uci add_list "${UCI_CONFIG}.global.sources=$url"
uci commit "$UCI_CONFIG"
json_add_boolean "success" 1
json_add_string "message" "Source added: $url"
json_dump
}
# Remove a source URL
do_remove_source() {
read -r input
json_load "$input"
json_get_var url url
json_init
if [ -z "$url" ]; then
json_add_boolean "success" 0
json_add_string "error" "No URL provided"
json_dump
return
fi
uci del_list "${UCI_CONFIG}.global.sources=$url"
uci commit "$UCI_CONFIG"
json_add_boolean "success" 1
json_add_string "message" "Source removed: $url"
json_dump
}
# Add whitelist entry
do_add_whitelist() {
read -r input
json_load "$input"
json_get_var entry entry
json_init
if [ -z "$entry" ]; then
json_add_boolean "success" 0
json_add_string "error" "No entry provided"
json_dump
return
fi
# Check if already in whitelist
if grep -qF "$entry" "$WHITELIST_FILE" 2>/dev/null; then
json_add_boolean "success" 0
json_add_string "error" "Entry already in whitelist"
json_dump
return
fi
echo "$entry" >> "$WHITELIST_FILE"
json_add_boolean "success" 1
json_add_string "message" "Added to whitelist: $entry"
json_dump
}
# Remove whitelist entry
do_remove_whitelist() {
read -r input
json_load "$input"
json_get_var entry entry
json_init
if [ -z "$entry" ]; then
json_add_boolean "success" 0
json_add_string "error" "No entry provided"
json_dump
return
fi
# Create temp file without the entry
local tmp=$(mktemp)
grep -vF "$entry" "$WHITELIST_FILE" > "$tmp" 2>/dev/null || true
mv "$tmp" "$WHITELIST_FILE"
json_add_boolean "success" 1
json_add_string "message" "Removed from whitelist: $entry"
json_dump
}
# RPCD list method
case "$1" in
list)
echo '{"status":{},"logs":{"lines":"int"},"sources":{},"whitelist":{},"update":{},"flush":{},"test_ip":{"ip":"str"},"set_enabled":{"enabled":"bool"},"add_source":{"url":"str"},"remove_source":{"url":"str"},"add_whitelist":{"entry":"str"},"remove_whitelist":{"entry":"str"}}'
;;
call)
case "$2" in
status) get_status ;;
logs) get_logs ;;
sources) get_sources ;;
whitelist) get_whitelist ;;
update) do_update ;;
flush) do_flush ;;
test_ip) do_test_ip ;;
set_enabled) do_set_enabled ;;
add_source) do_add_source ;;
remove_source) do_remove_source ;;
add_whitelist) do_add_whitelist ;;
remove_whitelist) do_remove_whitelist ;;
*) echo '{"error":"Unknown method"}' ;;
esac
;;
esac

View File

@ -0,0 +1,14 @@
{
"admin/services/ipblocklist": {
"title": "IP Blocklist",
"order": 45,
"action": {
"type": "view",
"path": "ipblocklist/dashboard"
},
"depends": {
"acl": ["luci-app-ipblocklist"],
"uci": {"ipblocklist": true}
}
}
}

View File

@ -0,0 +1,24 @@
{
"luci-app-ipblocklist": {
"description": "Grant access to SecuBox IP Blocklist management",
"read": {
"ubus": {
"luci.ipblocklist": ["status", "logs", "sources", "whitelist"]
},
"uci": ["ipblocklist"],
"file": {
"/var/log/ipblocklist.log": ["read"],
"/etc/ipblocklist/whitelist.txt": ["read"]
}
},
"write": {
"ubus": {
"luci.ipblocklist": ["update", "flush", "test_ip", "set_enabled", "add_source", "remove_source", "add_whitelist", "remove_whitelist"]
},
"uci": ["ipblocklist"],
"file": {
"/etc/ipblocklist/whitelist.txt": ["write"]
}
}
}
}

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-system-hub
PKG_VERSION:=0.5.1
PKG_RELEASE:=4
PKG_VERSION:=0.5.2
PKG_RELEASE:=1
PKG_ARCH:=all
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>

View File

@ -50,6 +50,19 @@ var callGetLogs = rpc.declare({
expect: { logs: [] }
});
var callGetDenoisedLogs = rpc.declare({
object: 'luci.system-hub',
method: 'get_denoised_logs',
params: ['lines', 'filter', 'mode'],
expect: {}
});
var callGetDenoiseStats = rpc.declare({
object: 'luci.system-hub',
method: 'get_denoise_stats',
expect: {}
});
var callBackupConfig = rpc.declare({
object: 'luci.system-hub',
method: 'backup_config',
@ -249,6 +262,10 @@ return baseclass.extend({
listServices: callListServices,
serviceAction: callServiceAction,
getLogs: callGetLogs,
getDenoisedLogs: function(lines, filter, mode) {
return callGetDenoisedLogs({ lines: lines, filter: filter, mode: mode });
},
getDenoiseStats: callGetDenoiseStats,
backupConfig: callBackupConfig,
restoreConfig: function(fileName, data) {
if (typeof fileName === 'object')

View File

@ -24,13 +24,34 @@ return view.extend({
severityFilter: 'all',
lastLogCount: 0,
pollInterval: 2,
// Denoising mode: RAW, SMART, SIGNAL_ONLY
denoiseMode: 'RAW',
noiseRatio: 0,
filteredLines: 0,
totalLines: 0,
denoiseStats: null,
load: function() {
return API.getLogs(this.lineCount, '');
var self = this;
return Promise.all([
API.getDenoisedLogs(this.lineCount, '', this.denoiseMode),
API.getDenoiseStats()
]).then(function(results) {
return {
logsData: results[0],
denoiseStats: results[1]
};
});
},
render: function(data) {
this.logs = Array.isArray(data) ? data : (data && data.logs) || [];
// Extract logs and denoising info from new data structure
var logsData = data.logsData || {};
this.logs = logsData.logs || [];
this.noiseRatio = logsData.noise_ratio || 0;
this.filteredLines = logsData.filtered_lines || 0;
this.totalLines = logsData.total_lines || this.logs.length;
this.denoiseStats = data.denoiseStats || {};
this.lastLogCount = this.logs.length;
var content = [
@ -40,6 +61,7 @@ return view.extend({
ThemeAssets.stylesheet('logs.css'),
HubNav.renderTabs('logs'),
this.renderHero(),
this.renderDenoisePanel(),
this.renderControls(),
this.renderBody()
];
@ -51,13 +73,17 @@ return view.extend({
poll.add(function() {
if (!self.autoRefresh) return;
self.updateLiveIndicator(true);
return API.getLogs(self.lineCount, '').then(function(result) {
var newLogs = Array.isArray(result) ? result : (result && result.logs) || [];
return API.getDenoisedLogs(self.lineCount, '', self.denoiseMode).then(function(result) {
var newLogs = result.logs || [];
var hasNewLogs = newLogs.length !== self.lastLogCount;
self.logs = newLogs;
self.noiseRatio = result.noise_ratio || 0;
self.filteredLines = result.filtered_lines || 0;
self.totalLines = result.total_lines || newLogs.length;
self.lastLogCount = newLogs.length;
self.updateStats();
self.updateLogStream(hasNewLogs);
self.updateDenoiseIndicator();
self.updateLiveIndicator(false);
});
}, this.pollInterval);
@ -98,6 +124,74 @@ return view.extend({
]);
},
renderDenoisePanel: function() {
var self = this;
var stats = this.denoiseStats || {};
var knownThreats = stats.total_known_threats || 0;
var ipblocklistEnabled = stats.ipblocklist_enabled;
var modeDescriptions = {
'RAW': _('All logs displayed without filtering'),
'SMART': _('Known threat IPs highlighted, all logs visible'),
'SIGNAL_ONLY': _('Only new/unknown threats shown')
};
return E('div', { 'class': 'sh-denoise-panel', 'style': 'display: flex; align-items: center; gap: 1.5em; padding: 0.8em 1.2em; background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-radius: 8px; margin-bottom: 1em; border: 1px solid rgba(99, 102, 241, 0.3);' }, [
// Mode selector
E('div', { 'style': 'display: flex; align-items: center; gap: 0.5em;' }, [
E('span', { 'style': 'font-weight: 600; color: #94a3b8;' }, '🧹 ' + _('Denoise Mode') + ':'),
E('select', {
'id': 'sh-denoise-mode',
'style': 'background: #334155; color: #f1f5f9; border: 1px solid #475569; border-radius: 4px; padding: 0.4em 0.8em; cursor: pointer;',
'change': function(ev) {
self.denoiseMode = ev.target.value;
self.refreshLogs();
}
}, [
E('option', { 'value': 'RAW', 'selected': this.denoiseMode === 'RAW' ? 'selected' : null }, 'RAW'),
E('option', { 'value': 'SMART', 'selected': this.denoiseMode === 'SMART' ? 'selected' : null }, 'SMART'),
E('option', { 'value': 'SIGNAL_ONLY', 'selected': this.denoiseMode === 'SIGNAL_ONLY' ? 'selected' : null }, 'SIGNAL ONLY')
])
]),
// Mode description
E('span', { 'id': 'sh-denoise-desc', 'style': 'color: #64748b; font-size: 0.9em;' }, modeDescriptions[this.denoiseMode]),
// Spacer
E('div', { 'style': 'flex: 1;' }),
// Noise ratio indicator
E('div', { 'id': 'sh-noise-indicator', 'style': 'display: flex; align-items: center; gap: 0.8em;' }, [
this.denoiseMode !== 'RAW' ? E('div', { 'style': 'text-align: center;' }, [
E('div', { 'style': 'font-size: 0.75em; color: #64748b; text-transform: uppercase;' }, _('Noise Filtered')),
E('div', {
'id': 'sh-noise-ratio',
'style': 'font-size: 1.4em; font-weight: 700; color: ' + this.getNoiseColor(this.noiseRatio) + ';'
}, this.noiseRatio + '%')
]) : null,
E('div', { 'style': 'text-align: center;' }, [
E('div', { 'style': 'font-size: 0.75em; color: #64748b; text-transform: uppercase;' }, _('Known Threats')),
E('div', { 'style': 'font-size: 1.4em; font-weight: 700; color: #f59e0b;' }, knownThreats.toLocaleString())
]),
!ipblocklistEnabled ? E('span', {
'style': 'padding: 0.3em 0.6em; background: #7c3aed; color: #fff; border-radius: 4px; font-size: 0.8em;',
'title': _('Enable IP Blocklist for better noise reduction')
}, '⚠ ' + _('Blocklist Off')) : null
])
]);
},
getNoiseColor: function(ratio) {
if (ratio >= 70) return '#22c55e'; // Green - lots of noise filtered
if (ratio >= 40) return '#f59e0b'; // Orange - moderate
return '#94a3b8'; // Gray - low noise
},
updateDenoiseIndicator: function() {
var ratioEl = document.getElementById('sh-noise-ratio');
if (ratioEl) {
ratioEl.textContent = this.noiseRatio + '%';
ratioEl.style.color = this.getNoiseColor(this.noiseRatio);
}
},
renderControls: function() {
var self = this;
return E('div', { 'class': 'sh-log-controls' }, [
@ -309,12 +403,20 @@ return view.extend({
},
buildMetrics: function() {
return [
var metrics = [
{ label: _('Critical events (last refresh)'), value: this.countSeverity('error') },
{ label: _('Warnings'), value: this.countSeverity('warning') },
{ label: _('Info/Debug'), value: this.countSeverity('info') },
{ label: _('Matched search'), value: this.getFilteredLogs().length }
];
// Add denoising metrics when not in RAW mode
if (this.denoiseMode !== 'RAW') {
metrics.push({ label: _('Noise ratio'), value: this.noiseRatio + '%' });
metrics.push({ label: _('Filtered entries'), value: this.filteredLines });
}
return metrics;
},
refreshLogs: function() {
@ -322,11 +424,15 @@ return view.extend({
ui.showModal(_('Loading logs...'), [
E('p', { 'class': 'spinning' }, _('Fetching system logs'))
]);
return API.getLogs(this.lineCount, '').then(function(result) {
return API.getDenoisedLogs(this.lineCount, '', this.denoiseMode).then(function(result) {
ui.hideModal();
self.logs = Array.isArray(result) ? result : (result && result.logs) || [];
self.logs = result.logs || [];
self.noiseRatio = result.noise_ratio || 0;
self.filteredLines = result.filtered_lines || 0;
self.totalLines = result.total_lines || self.logs.length;
self.updateStats();
self.updateLogStream();
self.updateDenoiseIndicator();
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');

View File

@ -528,6 +528,168 @@ get_logs() {
json_dump
}
# Get denoised logs with smart filtering
# Modes: RAW (all logs), SMART (filter known IPs), SIGNAL_ONLY (new threats only)
get_denoised_logs() {
read -r input
json_load "$input"
local lines filter mode
json_get_var lines lines "100"
json_get_var filter filter ""
json_get_var mode mode "RAW"
json_cleanup
local tmpfile="/tmp/syslog-denoised-$$"
local rawfile="/tmp/syslog-raw-$$"
local total_lines=0
local filtered_lines=0
# Get raw logs first
if [ -n "$filter" ]; then
logread | tail -n "$lines" | grep -i "$filter" > "$rawfile"
else
logread | tail -n "$lines" > "$rawfile"
fi
total_lines=$(wc -l < "$rawfile" 2>/dev/null || echo 0)
case "$mode" in
RAW)
# No filtering - return all logs
cp "$rawfile" "$tmpfile"
;;
SMART|SIGNAL_ONLY)
# Get known blocked IPs from ipblocklist ipset
local ipset_name
ipset_name=$(uci -q get ipblocklist.global.ipset_name 2>/dev/null || echo "secubox_blocklist")
# Create temp file for blocked IPs (extract IPs from ipset)
local blocked_ips="/tmp/blocked_ips_$$"
# Try nftables first, then iptables ipset
if command -v nft >/dev/null 2>&1 && nft list set inet fw4 "$ipset_name" >/dev/null 2>&1; then
nft list set inet fw4 "$ipset_name" 2>/dev/null | \
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]+)?' > "$blocked_ips"
elif command -v ipset >/dev/null 2>&1; then
ipset list "$ipset_name" 2>/dev/null | \
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]+)?' > "$blocked_ips"
else
touch "$blocked_ips"
fi
# Also get CrowdSec decisions if available
if command -v cscli >/dev/null 2>&1; then
cscli decisions list -o json 2>/dev/null | \
jsonfilter -e '@[*].value' 2>/dev/null | \
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' >> "$blocked_ips" 2>/dev/null
fi
# Sort and unique the blocked IPs (remove CIDR for comparison)
sort -u "$blocked_ips" | sed 's|/[0-9]*||g' > "${blocked_ips}.clean"
mv "${blocked_ips}.clean" "$blocked_ips"
# Filter logs
> "$tmpfile"
while IFS= read -r line; do
# Extract IP addresses from log line
local line_ips
line_ips=$(echo "$line" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -5)
local is_known=0
if [ -n "$line_ips" ] && [ -s "$blocked_ips" ]; then
for ip in $line_ips; do
# Skip private/local IPs
case "$ip" in
10.*|172.1[6-9].*|172.2[0-9].*|172.3[01].*|192.168.*|127.*)
continue
;;
esac
# Check if IP is in blocked list
if grep -qF "$ip" "$blocked_ips" 2>/dev/null; then
is_known=1
break
fi
done
fi
if [ "$mode" = "SMART" ]; then
# SMART mode: Show all logs but mark known IPs
if [ "$is_known" -eq 1 ]; then
filtered_lines=$((filtered_lines + 1))
fi
echo "$line" >> "$tmpfile"
else
# SIGNAL_ONLY mode: Only show logs with unknown IPs
if [ "$is_known" -eq 0 ]; then
echo "$line" >> "$tmpfile"
else
filtered_lines=$((filtered_lines + 1))
fi
fi
done < "$rawfile"
rm -f "$blocked_ips"
;;
esac
# Calculate noise ratio
local noise_ratio=0
if [ "$total_lines" -gt 0 ]; then
noise_ratio=$((filtered_lines * 100 / total_lines))
fi
json_init
json_add_string "mode" "$mode"
json_add_int "total_lines" "$total_lines"
json_add_int "filtered_lines" "$filtered_lines"
json_add_int "noise_ratio" "$noise_ratio"
json_add_array "logs"
# Read from file line by line
while IFS= read -r line; do
json_add_string "" "$line"
done < "$tmpfile"
json_close_array
# Cleanup
rm -f "$tmpfile" "$rawfile"
json_dump
}
# Get log denoising stats
get_denoise_stats() {
local ipset_name
ipset_name=$(uci -q get ipblocklist.global.ipset_name 2>/dev/null || echo "secubox_blocklist")
# Count blocked IPs
local blocked_count=0
if command -v nft >/dev/null 2>&1 && nft list set inet fw4 "$ipset_name" >/dev/null 2>&1; then
blocked_count=$(nft list set inet fw4 "$ipset_name" 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | wc -l)
elif command -v ipset >/dev/null 2>&1; then
blocked_count=$(ipset list "$ipset_name" 2>/dev/null | grep -c '^[0-9]' || echo 0)
fi
# Count CrowdSec decisions
local crowdsec_count=0
if command -v cscli >/dev/null 2>&1; then
crowdsec_count=$(cscli decisions list -o json 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l || echo 0)
fi
# Check if ipblocklist is enabled
local ipblocklist_enabled
ipblocklist_enabled=$(uci -q get ipblocklist.global.enabled 2>/dev/null || echo "0")
json_init
json_add_int "blocked_ips" "$blocked_count"
json_add_int "crowdsec_decisions" "$crowdsec_count"
json_add_int "total_known_threats" "$((blocked_count + crowdsec_count))"
json_add_boolean "ipblocklist_enabled" "$ipblocklist_enabled"
json_dump
}
# Create backup
backup_config() {
local backup_file="/tmp/backup-$(date +%Y%m%d-%H%M%S).tar.gz"
@ -2100,6 +2262,8 @@ case "$1" in
"list_services": {},
"service_action": { "service": "string", "action": "string" },
"get_logs": { "lines": 100, "filter": "" },
"get_denoised_logs": { "lines": 100, "filter": "", "mode": "RAW" },
"get_denoise_stats": {},
"backup_config": {},
"restore_config": { "file_name": "string", "data": "string" },
"get_backup_schedule": {},
@ -2175,6 +2339,8 @@ EOF
list_services) list_services ;;
service_action) service_action ;;
get_logs) get_logs ;;
get_denoised_logs) get_denoised_logs ;;
get_denoise_stats) get_denoise_stats ;;
backup_config) backup_config ;;
restore_config) restore_config ;;
get_backup_schedule) get_backup_schedule ;;

View File

@ -11,6 +11,8 @@
"get_components_by_category",
"list_services",
"get_logs",
"get_denoised_logs",
"get_denoise_stats",
"get_storage",
"get_settings",
"get_backup_schedule",

View File

@ -0,0 +1,45 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-ipblocklist
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=Gandalf <gandalf@cybermind.fr>
PKG_LICENSE:=Apache-2.0
include $(INCLUDE_DIR)/package.mk
define Package/secubox-app-ipblocklist
SECTION:=secubox
CATEGORY:=SecuBox
TITLE:=SecuBox IP Blocklist - Static threat defense layer
DEPENDS:=+ipset +wget-ssl +ca-bundle
PKGARCH:=all
endef
define Package/secubox-app-ipblocklist/description
Pre-emptive IP blocklist defense layer for SecuBox.
Downloads and maintains static blocklists (~100k IPs) from community sources
(Data-Shield, Firehol) and applies them via kernel ipset for immediate DROP.
This provides Layer 1 protection before CrowdSec reactive blocking.
endef
define Package/secubox-app-ipblocklist/conffiles
/etc/config/ipblocklist
/etc/ipblocklist/whitelist.txt
endef
define Package/secubox-app-ipblocklist/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/ipblocklist $(1)/etc/config/ipblocklist
$(INSTALL_DIR) $(1)/etc/cron.d
$(INSTALL_DATA) ./files/etc/cron.d/ipblocklist $(1)/etc/cron.d/ipblocklist
$(INSTALL_DIR) $(1)/etc/ipblocklist
$(INSTALL_DATA) ./files/etc/ipblocklist/whitelist.txt $(1)/etc/ipblocklist/whitelist.txt
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/ipblocklist-update.sh $(1)/usr/sbin/ipblocklist-update.sh
endef
$(eval $(call BuildPackage,secubox-app-ipblocklist))

View File

@ -0,0 +1,9 @@
config global 'global'
option enabled '1'
option update_interval '3600'
list sources 'https://raw.githubusercontent.com/duggytuxy/Data-Shield_IPv4_Blocklist/main/data-shield-blocklist-ipv4.txt'
list sources 'https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset'
option log_drops '1'
option whitelist_file '/etc/ipblocklist/whitelist.txt'
option ipset_name 'secubox_blocklist'
option max_entries '200000'

View File

@ -0,0 +1,2 @@
# SecuBox IP Blocklist - Hourly update
0 * * * * root [ "$(uci -q get ipblocklist.global.enabled)" = "1" ] && /usr/sbin/ipblocklist-update.sh update >/dev/null 2>&1

View File

@ -0,0 +1,6 @@
# SecuBox IP Blocklist Whitelist
# Add IPs or CIDRs to exclude from blocking (one per line)
# Example:
# 192.168.1.0/24
# 10.0.0.0/8
# 8.8.8.8

View File

@ -0,0 +1,399 @@
#!/bin/sh
# ipblocklist-update.sh — SecuBox IP Blocklist Manager
# Pre-emptive static threat defense layer using ipset
# Compatible OpenWrt — uses nftables (fw4) or legacy iptables
. /lib/functions.sh
UCI_CONFIG="ipblocklist"
LOG_FILE="/var/log/ipblocklist.log"
STATE_DIR="/var/lib/ipblocklist"
SAVE_FILE="/etc/ipblocklist/ipset.save"
# Get configuration from UCI
get_config() {
ENABLED=$(uci -q get "${UCI_CONFIG}.global.enabled" || echo "1")
IPSET_NAME=$(uci -q get "${UCI_CONFIG}.global.ipset_name" || echo "secubox_blocklist")
MAX_ENTRIES=$(uci -q get "${UCI_CONFIG}.global.max_entries" || echo "200000")
WHITELIST_FILE=$(uci -q get "${UCI_CONFIG}.global.whitelist_file" || echo "/etc/ipblocklist/whitelist.txt")
LOG_DROPS=$(uci -q get "${UCI_CONFIG}.global.log_drops" || echo "1")
}
# Log message with timestamp
log_msg() {
local level="$1"
local msg="$2"
echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $msg" >> "$LOG_FILE"
[ "$level" = "ERROR" ] && echo "[$level] $msg" >&2
}
# Detect firewall backend
detect_firewall() {
if [ -f /etc/config/firewall ] && grep -q "fw4" /etc/config/firewall 2>/dev/null; then
echo "nftables"
elif command -v nft >/dev/null 2>&1 && nft list tables 2>/dev/null | grep -q "fw4"; then
echo "nftables"
elif command -v iptables >/dev/null 2>&1; then
echo "iptables"
else
echo "none"
fi
}
# Initialize ipset
init_ipset() {
log_msg "INFO" "Initializing ipset $IPSET_NAME (max $MAX_ENTRIES entries)"
# Create ipset if it doesn't exist
if ! ipset list "$IPSET_NAME" >/dev/null 2>&1; then
ipset create "$IPSET_NAME" hash:net hashsize 65536 maxelem "$MAX_ENTRIES" 2>/dev/null
if [ $? -ne 0 ]; then
log_msg "ERROR" "Failed to create ipset $IPSET_NAME"
return 1
fi
fi
return 0
}
# Flush ipset
flush_ipset() {
log_msg "INFO" "Flushing ipset $IPSET_NAME"
ipset flush "$IPSET_NAME" 2>/dev/null || true
}
# Load whitelist into memory for fast lookup
load_whitelist() {
WHITELIST_PATTERNS=""
if [ -f "$WHITELIST_FILE" ]; then
WHITELIST_PATTERNS=$(grep -v '^#' "$WHITELIST_FILE" 2>/dev/null | grep -v '^$' | tr '\n' '|')
WHITELIST_PATTERNS="${WHITELIST_PATTERNS%|}" # Remove trailing |
fi
}
# Check if IP is whitelisted
is_whitelisted() {
local ip="$1"
[ -z "$WHITELIST_PATTERNS" ] && return 1
echo "$ip" | grep -qE "^($WHITELIST_PATTERNS)" && return 0
return 1
}
# Download and load a single blocklist source
load_source() {
local url="$1"
local tmp_file=$(mktemp)
local count=0
local skipped=0
log_msg "INFO" "Downloading blocklist from: $url"
if wget -q -T 30 -O "$tmp_file" "$url" 2>/dev/null; then
while IFS= read -r line; do
# Skip empty lines and comments
[ -z "$line" ] && continue
line="${line%%#*}" # Remove inline comments
line=$(echo "$line" | tr -d ' \t\r') # Trim whitespace
[ -z "$line" ] && continue
# Validate IP/CIDR format
echo "$line" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/[0-9]+)?$' || continue
# Check whitelist
if is_whitelisted "$line"; then
skipped=$((skipped + 1))
continue
fi
# Add to ipset
if ipset add "$IPSET_NAME" "$line" 2>/dev/null; then
count=$((count + 1))
fi
done < "$tmp_file"
log_msg "INFO" "Loaded $count IPs from $url (skipped $skipped whitelisted)"
else
log_msg "ERROR" "Failed to download: $url"
fi
rm -f "$tmp_file"
echo "$count"
}
# Load all configured blocklist sources
load_blocklists() {
local total=0
load_whitelist
# Get sources from UCI
config_load "$UCI_CONFIG"
# Iterate over sources
local sources=""
config_list_foreach "global" "sources" _add_source
for url in $SOURCES_LIST; do
local loaded=$(load_source "$url")
total=$((total + loaded))
done
log_msg "INFO" "Total IPs loaded into $IPSET_NAME: $total"
echo "$total"
}
_add_source() {
SOURCES_LIST="$SOURCES_LIST $1"
}
SOURCES_LIST=""
# Apply firewall rules to DROP traffic from blocklist
apply_firewall_rules() {
local fw=$(detect_firewall)
log_msg "INFO" "Applying firewall rules (backend: $fw)"
case "$fw" in
nftables)
# Check if set exists in nftables
if ! nft list set inet fw4 "$IPSET_NAME" >/dev/null 2>&1; then
# Create nftables set that references ipset
nft add set inet fw4 "$IPSET_NAME" "{ type ipv4_addr; flags interval; }" 2>/dev/null || true
fi
# Add rules if not present (check by comment)
if ! nft list chain inet fw4 input 2>/dev/null | grep -q "secubox_blocklist_drop"; then
nft insert rule inet fw4 input ip saddr @"$IPSET_NAME" counter drop comment \"secubox_blocklist_drop\" 2>/dev/null || true
fi
if ! nft list chain inet fw4 forward 2>/dev/null | grep -q "secubox_blocklist_drop"; then
nft insert rule inet fw4 forward ip saddr @"$IPSET_NAME" counter drop comment \"secubox_blocklist_drop\" 2>/dev/null || true
fi
# Sync ipset to nftables set
sync_ipset_to_nft
;;
iptables)
# Add iptables rules if not present
if ! iptables -C INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null; then
iptables -I INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null || true
fi
if ! iptables -C FORWARD -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null; then
iptables -I FORWARD -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null || true
fi
;;
*)
log_msg "ERROR" "No supported firewall backend found"
return 1
;;
esac
return 0
}
# Sync ipset entries to nftables set (for nftables backend)
sync_ipset_to_nft() {
# Export ipset to temp file and import to nftables
local tmp_nft=$(mktemp)
nft flush set inet fw4 "$IPSET_NAME" 2>/dev/null || true
ipset list "$IPSET_NAME" 2>/dev/null | grep -E '^[0-9]+\.' | while read -r ip; do
echo "add element inet fw4 $IPSET_NAME { $ip }" >> "$tmp_nft"
done
if [ -s "$tmp_nft" ]; then
nft -f "$tmp_nft" 2>/dev/null || true
fi
rm -f "$tmp_nft"
}
# Remove firewall rules
remove_firewall_rules() {
local fw=$(detect_firewall)
log_msg "INFO" "Removing firewall rules"
case "$fw" in
nftables)
# Remove rules by comment
nft delete rule inet fw4 input handle $(nft -a list chain inet fw4 input 2>/dev/null | grep "secubox_blocklist_drop" | awk '{print $NF}') 2>/dev/null || true
nft delete rule inet fw4 forward handle $(nft -a list chain inet fw4 forward 2>/dev/null | grep "secubox_blocklist_drop" | awk '{print $NF}') 2>/dev/null || true
nft delete set inet fw4 "$IPSET_NAME" 2>/dev/null || true
;;
iptables)
iptables -D INPUT -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null || true
iptables -D FORWARD -m set --match-set "$IPSET_NAME" src -j DROP 2>/dev/null || true
;;
esac
}
# Save ipset for persistence across reboots
save_ipset() {
log_msg "INFO" "Saving ipset to $SAVE_FILE"
mkdir -p "$(dirname "$SAVE_FILE")"
ipset save "$IPSET_NAME" > "$SAVE_FILE" 2>/dev/null
}
# Restore ipset from saved file
restore_ipset() {
if [ -f "$SAVE_FILE" ]; then
log_msg "INFO" "Restoring ipset from $SAVE_FILE"
ipset restore < "$SAVE_FILE" 2>/dev/null || true
return 0
fi
return 1
}
# Get status information
get_status() {
get_config
local count=0
local memory=""
if ipset list "$IPSET_NAME" >/dev/null 2>&1; then
count=$(ipset list "$IPSET_NAME" 2>/dev/null | grep -c '^[0-9]' || echo "0")
memory=$(ipset list "$IPSET_NAME" 2>/dev/null | grep "memsize" | awk '{print $2}')
fi
local fw=$(detect_firewall)
local last_update=$(stat -c %Y "$LOG_FILE" 2>/dev/null || echo "0")
cat <<EOF
{
"enabled": "$ENABLED",
"ipset_name": "$IPSET_NAME",
"entry_count": $count,
"max_entries": $MAX_ENTRIES,
"memory_bytes": "${memory:-0}",
"firewall_backend": "$fw",
"last_update": $last_update,
"whitelist_file": "$WHITELIST_FILE"
}
EOF
}
# Test if an IP would be blocked
test_ip() {
local ip="$1"
get_config
if ipset test "$IPSET_NAME" "$ip" 2>/dev/null; then
echo "BLOCKED: $ip is in blocklist $IPSET_NAME"
return 0
else
echo "ALLOWED: $ip is not in blocklist"
return 1
fi
}
# Main update routine
do_update() {
get_config
if [ "$ENABLED" != "1" ]; then
log_msg "INFO" "IP Blocklist is disabled, skipping update"
return 0
fi
log_msg "INFO" "Starting blocklist update"
init_ipset || return 1
flush_ipset
load_blocklists
apply_firewall_rules
save_ipset
log_msg "INFO" "Blocklist update completed"
}
# Start service
do_start() {
get_config
if [ "$ENABLED" != "1" ]; then
log_msg "INFO" "IP Blocklist is disabled"
return 0
fi
log_msg "INFO" "Starting IP Blocklist service"
mkdir -p "$STATE_DIR"
# Try to restore from saved ipset first
if restore_ipset; then
apply_firewall_rules
log_msg "INFO" "Restored ipset from saved state"
else
# Full update needed
do_update
fi
}
# Stop service
do_stop() {
get_config
log_msg "INFO" "Stopping IP Blocklist service"
save_ipset
remove_firewall_rules
# Don't destroy ipset, just remove firewall rules
# ipset destroy "$IPSET_NAME" 2>/dev/null || true
}
# Print usage
usage() {
cat <<EOF
Usage: $0 <command> [options]
Commands:
start Start the blocklist service (restore or update)
stop Stop the blocklist service
update Download and apply blocklists
flush Remove all IPs from blocklist
status Show current status (JSON)
test <ip> Test if an IP is blocked
logs Show recent log entries
help Show this help
EOF
}
# Main entry point
case "$1" in
start)
do_start
;;
stop)
do_stop
;;
update)
do_update
;;
flush)
get_config
flush_ipset
save_ipset
log_msg "INFO" "Blocklist flushed"
;;
status)
get_status
;;
test)
test_ip "$2"
;;
logs)
tail -n "${2:-50}" "$LOG_FILE" 2>/dev/null || echo "No logs available"
;;
help|--help|-h)
usage
;;
*)
usage
exit 1
;;
esac
exit 0