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:
parent
a81e8dd8ca
commit
cec4893db9
621
.claude/EVOLUTION-PLAN.md
Normal file
621
.claude/EVOLUTION-PLAN.md
Normal 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*
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
});
|
||||
@ -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'
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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,
|
||||
|
||||
@ -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" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
package/secubox/luci-app-ipblocklist/Makefile
Normal file
17
package/secubox/luci-app-ipblocklist/Makefile
Normal 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))
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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 ;;
|
||||
|
||||
@ -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",
|
||||
|
||||
45
package/secubox/secubox-app-ipblocklist/Makefile
Normal file
45
package/secubox/secubox-app-ipblocklist/Makefile
Normal 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))
|
||||
@ -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'
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user