Compare commits

..

3 Commits

Author SHA1 Message Date
CyberMind
f34dc633a8
Merge pull request #523 from CyberMind-FR/feature/522-phase-13-b-dns-guard-resolve-blocklisted
Some checks are pending
License Headers / check (push) Waiting to run
Phase 13.A + 13.B — protection enforcement spine + DNS-guard (#521 #522)
2026-06-11 07:25:58 +02:00
1063c91815 feat(toolbox): Phase 13.B — DNS-guard (resolve blocklisted domains + DoH/DoT detection) (ref #522)
Closes the DoH / hardcoded-IP bypass gap on top of 13.A.

  - secubox-blacklist-sync: Source 3 resolves threat_intel domain IOCs
    (ioc_type='domain') to A/AAAA via getent (2s per-lookup timeout,
    2000-domain cap) and pushes the IPs into blacklist_v4/v6 — a device
    that resolves a known-bad domain out-of-band (DoH / hardcoded IP) is
    still dropped at forward. Writes /run/secubox/blacklist-sync.json.
  - secubox-blacklist.nft: doh_detect_v4/v6 sets + count-only doh_watch
    chain (priority -11) countering device egress to known DoH/DoT
    providers (Cloudflare/Google/Quad9/AdGuard/OpenDNS/CleanBrowsing/
    Control-D/NextDNS) on tcp 443/853. Made the drop-in IDEMPOTENT via
    the create-or-replace idiom (table{} ; delete table ; table{...}) so
    nft -f reloads don't append duplicate rules.
  - secubox-blacklist-sync: populates DoH sets each cycle;
    SECUBOX_DOH_BLOCK=1 also enforce-drops them (forces devices back
    through Vortex DNS). Default off — intrusive, gated per #519 Q1.
  - api.py /admin/blacklist: doh_detect counts, doh_hits, resolved_domains,
    doh_block flag.
  - changelog 2.6.9.

Live on gk2: idempotency verified (enforce=4 / doh_watch=2 rules after
double reload), endpoint shows v4_count 20 + doh_detect 15 v4 / 6 v6,
doh_block false. resolved_domains 0 (threat_intel has no domain IOCs
yet — threatfox feed currently returns 0; path is correct).
2026-06-11 07:24:24 +02:00
a5c07c3d50 feat(toolbox): Phase 13.A — unified nft blacklist enforcement spine (ref #521)
First track of the protection enforcement plane (#519). Pure netfilter,
additive drops, DEFAULT-DROP doctrine preserved.

  - nftables.d/secubox-blacklist.nft: table inet secubox_blacklist with
    blacklist_v4/v6 sets (interval,timeout) + a single forward-hook
    chain (priority -10, policy accept) counter-dropping any routed
    packet whose saddr OR daddr is blacklisted. One rule set covers
    every device egress path (captive/WG/br-lxc/LAN), no per-iface logic.
  - sbin/secubox-blacklist-sync: unions CrowdSec ban decisions (cscli)
    + threat-intel C2 IPs (threat_intel ip IOCs) into the sets, 2h
    per-element timeout (auto-expire on stale feeds), 50k cap, idempotent
    add (no flush race with CrowdSec's own table).
  - systemd sync .service + .timer (5min + OnBootSec 90s).
  - postinst: install drop-in, reload nft, enable+start timer, first sync.
  - api.py GET /admin/blacklist: element counts + drop counters (nft -j).

  - debian/rules FIX: moved the whole override_dh_strip body into
    execute_after_dh_auto_install. dh_strip is NOT in the binary-indep
    sequence for an Architecture: all package, so override_dh_strip
    silently never ran — the nft drop-ins / unbound / nginx / perf
    drop-ins (and now the blacklist files) had stopped shipping in the
    .deb and gk2 was running stale on-disk copies (the live-config-drift
    we kept hitting). Now they all ship again.

  - changelog 2.6.8.

Live on gk2: table loaded, first sync populated 18 v4 C2 IPs with 2h
timeouts, enforce chain active (4 drop rules, counters wired), timer
enabled, /admin/blacklist returns active:true.
2026-06-11 07:16:35 +02:00
8 changed files with 410 additions and 2 deletions

View File

@ -1,3 +1,55 @@
secubox-toolbox (2.6.9-1~bookworm1) bookworm; urgency=medium
* Phase 13.B (#522, parent #519) — DNS-guard : resolve blocklisted
domains into the enforcement set + DoH/DoT detection.
- secubox-blacklist-sync : Source 3 resolves threat_intel
ioc_type='domain' to A/AAAA (getent, per-lookup timeout 2s,
2000-domain cap) and pushes the IPs into blacklist_v4/v6 — so a
device that resolves a known-bad domain via DoH / hardcoded IP
is still dropped at the forward hook. Writes a state file
/run/secubox/blacklist-sync.json (resolved count + doh_block).
- nftables.d/secubox-blacklist.nft : doh_detect_v4/v6 sets + a
count-only doh_watch chain (priority -11) that counters device
egress to known DoH/DoT provider endpoints (Cloudflare/Google/
Quad9/AdGuard/OpenDNS/CleanBrowsing/Control-D/NextDNS) on
tcp 443/853. Detection only by default.
- secubox-blacklist-sync : populates the DoH sets every cycle ;
SECUBOX_DOH_BLOCK=1 ALSO adds them to the enforced blacklist
(forces devices back through Vortex DNS). Default off — gated.
- api.py /admin/blacklist : adds doh_detect counts, doh_hits
counter, resolved_domains, doh_block flag.
Detection-first ; DoH blocking opt-in only (intrusive, per #519 Q1).
-- Gerald KERMA <devel@cybermind.fr> Thu, 11 Jun 2026 09:30:00 +0200
secubox-toolbox (2.6.8-1~bookworm1) bookworm; urgency=medium
* Phase 13.A (#521, parent #519) — unified blacklist enforcement spine.
- nftables.d/secubox-blacklist.nft : table inet secubox_blacklist
with blacklist_v4 / blacklist_v6 sets (flags interval,timeout)
+ a single forward-hook chain (priority -10, policy accept) that
counter-drops any routed packet whose saddr OR daddr is
blacklisted. One rule set covers every device egress path
(captive / R3 WG / br-lxc / LAN) — no per-interface logic.
Loaded by nftables.service at boot ; survives reboot.
- sbin/secubox-blacklist-sync : unions CrowdSec ban decisions
(cscli) + threat-intel C2 IPs (threat_intel ip IOCs from
toolbox.db) into the sets, per-element timeout 2h (auto-expire
on stale feeds), 50k safety cap, idempotent add (no flush race
with CrowdSec's own table).
- systemd/secubox-blacklist-sync.{service,timer} : oneshot +
every-5-min timer (OnBootSec 90s).
- postinst : install drop-in to /etc/nftables.d/, reload nft,
enable+start the timer, kick a first sync.
- api.py : GET /admin/blacklist — element counts + drop counters
from nft -j (read-only status).
Doctrine preserved : policy accept only ADDS drops (a drop in any
chain wins) — never a WAF/default-drop bypass. Detection sources are
high-confidence only (CrowdSec bans + threat-intel C2) ; WAF-ban +
Vortex-resolved-IP feeds land in 13.B/13.D.
-- Gerald KERMA <devel@cybermind.fr> Thu, 11 Jun 2026 09:00:00 +0200
secubox-toolbox (2.6.7-1~bookworm1) bookworm; urgency=medium secubox-toolbox (2.6.7-1~bookworm1) bookworm; urgency=medium
* Phase 12.C (#518, parent #514) — operator-grade / state-adjacent * Phase 12.C (#518, parent #514) — operator-grade / state-adjacent

View File

@ -151,6 +151,26 @@ fi
fi fi
fi fi
# Phase 13.A (#521) : unified blacklist enforcement spine.
# nft drop-in (loaded by nftables.service at boot) + sync timer that
# unions CrowdSec decisions + threat-intel C2 IPs into the sets.
if [ -f /usr/share/secubox/toolbox/nftables.d/secubox-blacklist.nft ]; then
install -d -m 0755 /etc/nftables.d
install -m 0644 /usr/share/secubox/toolbox/nftables.d/secubox-blacklist.nft \
/etc/nftables.d/secubox-blacklist.nft
if systemctl is-active --quiet nftables.service 2>/dev/null; then
systemctl reload nftables.service 2>/dev/null \
|| /usr/sbin/nft -f /etc/nftables.d/secubox-blacklist.nft 2>/dev/null \
|| true
fi
if [ -d /run/systemd/system ]; then
systemctl enable secubox-blacklist-sync.timer 2>/dev/null || true
systemctl start secubox-blacklist-sync.timer 2>/dev/null || true
# Kick an immediate first sync (best-effort).
systemctl start secubox-blacklist-sync.service 2>/dev/null || true
fi
fi
# Phase 7 (#498) : install unbound listeners. # Phase 7 (#498) : install unbound listeners.
# 99-secubox-wg.conf — WG peers (10.99.x). Without it WG peers' DNS # 99-secubox-wg.conf — WG peers (10.99.x). Without it WG peers' DNS
# queries get ICMP port unreachable and the iPhone stops resolving. # queries get ICMP port unreachable and the iPhone stops resolving.

View File

@ -54,8 +54,14 @@ override_dh_installsystemd:
override_dh_auto_test: override_dh_auto_test:
@true @true
override_dh_strip: # NOTE: dh_strip is NOT invoked in the binary-indep sequence (this is an
@true # Architecture: all package with no binaries to strip), so an
# override_dh_strip target would silently never run. Everything that
# used to live there is installed from execute_after_dh_auto_install
# below — which DOES run — so nft drop-ins / unbound / nginx / perf
# drop-ins / blacklist spine actually ship in the .deb (#521 caught the
# stale-on-disk drift this caused).
execute_after_dh_auto_install:
# Phase 6.Q (#496) : DB tuning helper + uvicorn perf drop-in # Phase 6.Q (#496) : DB tuning helper + uvicorn perf drop-in
install -m 755 sbin/secubox-toolbox-db-tune \ install -m 755 sbin/secubox-toolbox-db-tune \
debian/secubox-toolbox/usr/sbin/ debian/secubox-toolbox/usr/sbin/
@ -73,6 +79,15 @@ override_dh_strip:
# by symlinking /etc/nftables.d/secubox-toolbox-wg.nft → this file. # by symlinking /etc/nftables.d/secubox-toolbox-wg.nft → this file.
install -m 0644 nftables.d/secubox-toolbox-wg-fanout.nft \ install -m 0644 nftables.d/secubox-toolbox-wg-fanout.nft \
debian/secubox-toolbox/usr/share/secubox/toolbox/nftables.d/ debian/secubox-toolbox/usr/share/secubox/toolbox/nftables.d/
# Phase 13.A (#521) : unified blacklist enforcement spine.
install -m 0644 nftables.d/secubox-blacklist.nft \
debian/secubox-toolbox/usr/share/secubox/toolbox/nftables.d/
install -m 0755 sbin/secubox-blacklist-sync \
debian/secubox-toolbox/usr/sbin/
install -m 0644 systemd/secubox-blacklist-sync.service \
debian/secubox-toolbox/lib/systemd/system/
install -m 0644 systemd/secubox-blacklist-sync.timer \
debian/secubox-toolbox/lib/systemd/system/
install -m 0755 sbin/secubox-toolbox-wg-restore \ install -m 0755 sbin/secubox-toolbox-wg-restore \
debian/secubox-toolbox/usr/sbin/ debian/secubox-toolbox/usr/sbin/
install -m 0644 systemd/secubox-toolbox-wg-restore.service \ install -m 0644 systemd/secubox-toolbox-wg-restore.service \

View File

@ -0,0 +1,70 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
#
# Phase 13.A (#521) — unified blacklist enforcement spine.
#
# One authoritative pair of sets (v4/v6) fed by secubox-blacklist-sync
# from ALL ban sources (CrowdSec decisions + threat-intel C2 IPs ;
# WAF-ban + Vortex-resolved feeds land in 13.B/13.D). A single
# forward-hook chain drops any ROUTED packet whose source OR destination
# is blacklisted — so it covers every device egress path at once
# (captive 10.99.0.0/24, R3 WG wg-toolbox 10.99.1.0/24, br-lxc, LAN)
# with no per-interface logic.
#
# Doctrine : policy accept here only ADDS drops ; it never bypasses the
# main inet filter default-drop (a drop in any chain wins regardless of
# another chain's accept policy). Loaded by nftables.service at boot
# via /etc/nftables.d/*.nft so it survives reboot (#498/#501 pattern).
#
# The sets are `interval` (CIDR support) + `timeout` (elements auto-expire
# if the sync stops re-adding them — fail-open on stale data, never a
# permanent black hole from a one-off bad feed).
#
# Idempotent load : the create-or-replace idiom below makes `nft -f` on
# this file safe to run repeatedly (postinst reload, manual reload)
# WITHOUT appending duplicate rules to the chains. The sets are wiped on
# reload but secubox-blacklist-sync repopulates them immediately (postinst
# kicks a sync ; the timer keeps them fresh every 5 min) — a brief empty
# window is the fail-open direction anyway.
table inet secubox_blacklist { }
delete table inet secubox_blacklist
table inet secubox_blacklist {
set blacklist_v4 {
type ipv4_addr
flags interval, timeout
}
set blacklist_v6 {
type ipv6_addr
flags interval, timeout
}
chain enforce {
type filter hook forward priority -10; policy accept;
ip daddr @blacklist_v4 counter drop
ip saddr @blacklist_v4 counter drop
ip6 daddr @blacklist_v6 counter drop
ip6 saddr @blacklist_v6 counter drop
}
# Phase 13.B (#522) — DoH/DoT detection. COUNT-ONLY by default : a
# device reaching a known DoH/DoT provider endpoint is counted (for
# passive stats + the "this device bypasses Vortex DNS" signal) but
# NOT blocked. Blocking is opt-in : secubox-blacklist-sync with
# SECUBOX_DOH_BLOCK=1 ALSO adds these IPs to blacklist_v4/v6 above,
# where the enforce chain drops them — forcing the device back through
# Vortex DNS. Default off (intrusive ; gated per #519 open Q1).
set doh_detect_v4 {
type ipv4_addr
flags interval, timeout
}
set doh_detect_v6 {
type ipv6_addr
flags interval, timeout
}
chain doh_watch {
type filter hook forward priority -11; policy accept;
ip daddr @doh_detect_v4 tcp dport { 443, 853 } counter
ip6 daddr @doh_detect_v6 tcp dport { 443, 853 } counter
}
}

View File

@ -0,0 +1,147 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
#
# SecuBox-Deb :: secubox-blacklist-sync
#
# Phase 13.A (#521) — union all ban sources into the nft enforcement
# sets (inet secubox_blacklist blacklist_v4 / blacklist_v6). Idempotent,
# safe to run on a timer. Each element gets a timeout so stale entries
# auto-expire if a source stops listing them.
set -euo pipefail
readonly MODULE="secubox-blacklist-sync"
readonly VERSION="13.A"
NFT=/usr/sbin/nft
TABLE="inet secubox_blacklist"
TOOLBOX_DB=/var/lib/secubox/toolbox/toolbox.db
ELEM_TIMEOUT="${SECUBOX_BL_TIMEOUT:-2h}" # auto-expiry per element
# Safety cap : never load more than this many IPs (a runaway feed
# shouldn't be able to black-hole the box).
MAX_ELEMS="${SECUBOX_BL_MAX:-50000}"
log() { logger -t "$MODULE" -- "$*" 2>/dev/null || echo "[$MODULE] $*" >&2; }
# The nft table must already exist (loaded by nftables.service from the
# drop-in). If it doesn't, load the drop-in once.
if ! $NFT list table $TABLE >/dev/null 2>&1; then
if [ -r /etc/nftables.d/secubox-blacklist.nft ]; then
$NFT -f /etc/nftables.d/secubox-blacklist.nft || {
log "ERROR: table missing and drop-in load failed"; exit 1; }
else
log "ERROR: table $TABLE missing and no drop-in to load"; exit 1
fi
fi
# ── Collect IPs from all sources into temp files (v4 / v6) ──
TMP4=$(mktemp); TMP6=$(mktemp)
trap 'rm -f "$TMP4" "$TMP6"' EXIT
# Source 1 : threat-intel C2 IPs (feodo / threatfox / sslbl) from the
# toolbox SQLite. ioc_type='ip'.
if [ -r "$TOOLBOX_DB" ] && command -v sqlite3 >/dev/null 2>&1; then
sqlite3 "$TOOLBOX_DB" \
"SELECT DISTINCT ioc FROM threat_intel WHERE ioc_type='ip';" \
2>/dev/null >> "$TMP4.raw" || true
fi
# Source 2 : CrowdSec decisions (ban scope=Ip). JSON via cscli.
if command -v cscli >/dev/null 2>&1; then
cscli decisions list -o json 2>/dev/null \
| { command -v jq >/dev/null 2>&1 \
&& jq -r '.[] | select(.decisions != null) | .decisions[] | select(.type=="ban") | .value' 2>/dev/null \
|| sed -n 's/.*"value":"\([0-9a-fA-F:.]*\)".*/\1/p'; } \
>> "$TMP4.raw" || true
fi
# Source 3 (Phase 13.B #522) : DNS-guard — resolve blocklisted DOMAINS
# (threat_intel ioc_type='domain') to their A/AAAA records and add those
# IPs. Closes the DoH / hardcoded-IP bypass : even if a device resolves a
# known-bad domain out-of-band, the resulting IP is dropped at forward.
# Bounded : cap on domains/cycle + per-lookup timeout so the sync never
# hangs on a dead resolver.
DOMAIN_CAP="${SECUBOX_BL_DOMAIN_CAP:-2000}"
RESOLVE_TIMEOUT="${SECUBOX_BL_RESOLVE_TIMEOUT:-2}"
resolved_domains=0
if [ -r "$TOOLBOX_DB" ] && command -v sqlite3 >/dev/null 2>&1; then
while IFS= read -r dom; do
[ -n "$dom" ] || continue
# getent ahosts returns both A + AAAA ; timeout guards a dead lookup.
ips=$(timeout "$RESOLVE_TIMEOUT" getent ahosts "$dom" 2>/dev/null \
| awk '{print $1}' | sort -u)
if [ -n "$ips" ]; then
printf '%s\n' "$ips" >> "$TMP4.raw"
resolved_domains=$((resolved_domains + 1))
fi
done < <(sqlite3 "$TOOLBOX_DB" \
"SELECT DISTINCT ioc FROM threat_intel WHERE ioc_type='domain' LIMIT $DOMAIN_CAP;" \
2>/dev/null)
fi
# Split v4 / v6, validate, dedup, cap.
if [ -f "$TMP4.raw" ]; then
# IPv6 = contains a colon ; IPv4 = dotted quad (optionally /CIDR).
grep -E ':' "$TMP4.raw" 2>/dev/null | grep -vE '^\s*$' | sort -u > "$TMP6" || true
grep -vE ':' "$TMP4.raw" 2>/dev/null \
| grep -E '^([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?$' \
| sort -u > "$TMP4" || true
rm -f "$TMP4.raw"
fi
n4=$(wc -l < "$TMP4" 2>/dev/null || echo 0)
n6=$(wc -l < "$TMP6" 2>/dev/null || echo 0)
if [ "$n4" -gt "$MAX_ELEMS" ]; then
log "WARN: $n4 v4 IPs exceeds cap $MAX_ELEMS — truncating"
head -n "$MAX_ELEMS" "$TMP4" > "$TMP4.cap" && mv "$TMP4.cap" "$TMP4"
n4=$MAX_ELEMS
fi
# ── Push into the nft sets (batched add-element with per-elem timeout) ──
# We don't flush : timeouts handle expiry, and re-adding refreshes the
# timeout on still-active entries. Batching keeps it to one nft call.
add_batch() {
local set="$1" file="$2" fam="$3"
[ -s "$file" ] || return 0
local elems
elems=$(awk -v t="$ELEM_TIMEOUT" 'NF{printf "%s timeout %s, ", $1, t}' "$file")
elems="${elems%, }"
[ -n "$elems" ] || return 0
# add (not flush+add) so concurrent CrowdSec table updates don't race.
$NFT add element $TABLE "$set" "{ $elems }" 2>/dev/null \
|| log "WARN: partial add to $set ($fam)"
}
add_batch blacklist_v4 "$TMP4" v4
add_batch blacklist_v6 "$TMP6" v6
# ── Phase 13.B (#522) : DoH/DoT detection-list population ──
# Known DoH/DoT provider endpoints. Count-only by default (the doh_watch
# chain just counters) ; SECUBOX_DOH_BLOCK=1 also adds them to the
# enforced blacklist so devices are forced back through Vortex DNS.
DOH_BLOCK="${SECUBOX_DOH_BLOCK:-0}"
DOH_V4="1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 9.9.9.9 149.112.112.112 \
94.140.14.14 94.140.15.15 208.67.222.222 208.67.220.220 \
185.228.168.9 76.76.2.0 76.76.10.0 45.90.28.0 45.90.30.0"
DOH_V6="2606:4700:4700::1111 2606:4700:4700::1001 2001:4860:4860::8888 \
2001:4860:4860::8844 2620:fe::fe 2620:fe::9"
doh4_elems=$(printf '%s ' $DOH_V4 | awk -v t="$ELEM_TIMEOUT" '{for(i=1;i<=NF;i++)printf "%s timeout %s, ",$i,t}')
doh6_elems=$(printf '%s ' $DOH_V6 | awk -v t="$ELEM_TIMEOUT" '{for(i=1;i<=NF;i++)printf "%s timeout %s, ",$i,t}')
[ -n "$doh4_elems" ] && $NFT add element $TABLE doh_detect_v4 "{ ${doh4_elems%, } }" 2>/dev/null || true
[ -n "$doh6_elems" ] && $NFT add element $TABLE doh_detect_v6 "{ ${doh6_elems%, } }" 2>/dev/null || true
if [ "$DOH_BLOCK" = "1" ]; then
# Opt-in : also enforce-drop DoH so devices fall back to Vortex.
[ -n "$doh4_elems" ] && $NFT add element $TABLE blacklist_v4 "{ ${doh4_elems%, } }" 2>/dev/null || true
[ -n "$doh6_elems" ] && $NFT add element $TABLE blacklist_v6 "{ ${doh6_elems%, } }" 2>/dev/null || true
fi
# ── Report + state file (consumed by /admin/blacklist) ──
live4=$($NFT list set $TABLE blacklist_v4 2>/dev/null | grep -c 'timeout' || echo 0)
live6=$($NFT list set $TABLE blacklist_v6 2>/dev/null | grep -c 'timeout' || echo 0)
STATE=/run/secubox/blacklist-sync.json
mkdir -p /run/secubox 2>/dev/null || true
printf '{"ts":%s,"v4_added":%s,"v6_added":%s,"resolved_domains":%s,"doh_block":%s}\n' \
"$(date +%s)" "${n4:-0}" "${n6:-0}" "${resolved_domains:-0}" "${DOH_BLOCK}" \
> "$STATE" 2>/dev/null || true
log "synced: +${n4} v4 / +${n6} v6 (live ~${live4}/${live6}, resolved ${resolved_domains:-0} domains, doh_block=${DOH_BLOCK}, timeout ${ELEM_TIMEOUT})"
exit 0

View File

@ -2085,6 +2085,74 @@ async def admin_social_aggregate(hours: int = 24) -> dict:
return _s.aggregate(hours=hours) return _s.aggregate(hours=hours)
@router.get("/admin/blacklist")
async def admin_blacklist() -> dict:
"""Phase 13.A (#521) + 13.B (#522) — enforcement-spine status :
element counts + drop counters of the unified nft blacklist sets,
plus the DoH/DoT detection counters and the last-sync state.
Read-only ; parses `nft -j list table inet secubox_blacklist`.
"""
import json as _json
import subprocess as _sp
from pathlib import Path as _P
out: dict = {
"active": False,
"v4_count": 0,
"v6_count": 0,
"drops": 0,
"doh_detect_v4": 0,
"doh_detect_v6": 0,
"doh_hits": 0,
"resolved_domains": 0,
"doh_block": False,
"sources": ["crowdsec", "threat-intel", "dns-guard"],
}
try:
r = _sp.run(
["/usr/sbin/nft", "-j", "list", "table", "inet", "secubox_blacklist"],
capture_output=True, text=True, timeout=5,
)
if r.returncode == 0:
data = _json.loads(r.stdout or "{}")
for item in data.get("nftables", []):
if "set" in item:
s = item["set"]
n = len(s.get("elem", []) or [])
name = s.get("name")
if name == "blacklist_v4":
out["v4_count"] = n
elif name == "blacklist_v6":
out["v6_count"] = n
elif name == "doh_detect_v4":
out["doh_detect_v4"] = n
elif name == "doh_detect_v6":
out["doh_detect_v6"] = n
if "rule" in item:
chain = item["rule"].get("chain", "")
for ex in item["rule"].get("expr", []):
c = ex.get("counter")
if not c:
continue
pk = int(c.get("packets", 0) or 0)
if chain == "doh_watch":
out["doh_hits"] += pk
else:
out["drops"] += pk
out["active"] = True
except Exception as e: # noqa: BLE001
log.warning("admin_blacklist nft parse failed: %s", e)
# Last-sync state file (resolved-domain count + doh_block flag).
try:
st = _P("/run/secubox/blacklist-sync.json")
if st.exists():
j = _json.loads(st.read_text())
out["resolved_domains"] = int(j.get("resolved_domains", 0) or 0)
out["doh_block"] = str(j.get("doh_block", "0")) == "1"
except Exception:
pass
return out
@router.get("/social/report/{token}.pdf") @router.get("/social/report/{token}.pdf")
async def social_report_pdf(token: str) -> Response: async def social_report_pdf(token: str) -> Response:
"""Phase 11.C (#508) — bilingual FR/EN evidence PDF for a peer. """Phase 11.C (#508) — bilingual FR/EN evidence PDF for a peer.

View File

@ -0,0 +1,20 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Phase 13.A (#521) — union ban sources into the nft blacklist sets.
[Unit]
Description=SecuBox blacklist enforcement sync (CrowdSec + threat-intel -> nft sets)
Documentation=https://github.com/CyberMind-FR/secubox-deb/issues/521
After=nftables.service secubox-toolbox.service
Wants=nftables.service
[Service]
Type=oneshot
ExecStart=/usr/sbin/secubox-blacklist-sync
# Needs CAP_NET_ADMIN for nft set writes ; root is simplest + the script
# is read-only against the toolbox DB + cscli.
User=root
Nice=10
IOSchedulingClass=idle
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,16 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Phase 13.A (#521) — periodic blacklist sync.
[Unit]
Description=SecuBox blacklist sync timer (every 5 min + boot)
Documentation=https://github.com/CyberMind-FR/secubox-deb/issues/521
[Timer]
# Run shortly after boot, then every 5 minutes. The element timeout
# (2h) is well above the period so active entries never lapse.
OnBootSec=90s
OnUnitActiveSec=5min
AccuracySec=30s
Persistent=true
[Install]
WantedBy=timers.target