Compare commits

...

2 Commits

Author SHA1 Message Date
CyberMind
323363e701
Merge pull request #694 from CyberMind-FR/feature/692-dpi-beaconing-rule-fires-on-sub-second-a
Some checks are pending
License Headers / check (push) Waiting to run
dpi 1.1.1: beaconing period-band + dashboard cards on exfil engine (closes #692, #693)
2026-06-22 10:07:40 +02:00
c91931380f fix(dpi): beaconing period-band + dashboard cards on exfil engine (closes #692, closes #693)
#692 — beaconing scenario was firing on sub-second app/media chatter
("~39 ms" false positives). Now requires a C2-plausible cadence: mean IAT in
[1 s, 1 h], CV <= 0.25, >=6 flows, to an EXTERNAL exfil-relevant/unclassified
dest (never known media/game/social CDNs). Detail reads in seconds.

#693 — the DPI dashboard headline stat cards were legacy netifyd widgets,
empty on R3 boards (netifyd inactive). Repointed to the real exfil engine:
R3 Devices / captured Flows / Categories / Exfil Alerts, all from /exfil.
netifyd list cards degrade gracefully.

secubox-dpi 1.1.1. Verified live on gk2: 39 ms synthetic beacon dropped, 5 s
beacon fires; admin.gk2/dpi/ stat cards + exfil panel populated.
2026-06-22 10:07:24 +02:00
3 changed files with 47 additions and 20 deletions

View File

@ -33,7 +33,12 @@ const (
upExfilBytes = 5 << 20 // >=5 MB outbound to a cloud → volume alert
beaconMinFlows = 6 // >=6 flows same dst → candidate beacon
beaconCVMax = 0.25 // iat coefficient-of-variation <= 0.25 → periodic
topN = 12
// #692 — period band (ndpiReader iat is in ms). Real C2 beacons sit at
// seconds-to-minutes; sub-second cadence is app polling / media chunks /
// websocket keepalives, not exfil. Only flag a steady period in this band.
beaconMinIntervalMs = 1000.0 // >=1 s between flows
beaconMaxIntervalMs = 3600000.0 // <=1 h between flows
topN = 12
)
var (
@ -299,13 +304,16 @@ func main() {
alerts = append(alerts, base("new_cloud", "première sortie vers "+label))
}
}
// 3) beaconing: many flows, low inter-arrival variance
if a.Flows >= beaconMinFlows {
// 3) beaconing: many flows, low inter-arrival variance, at a C2-plausible
// cadence (1 s1 h), to an external dest that is exfil-relevant or
// unclassified. Excludes sub-second app chatter and periodic fetches
// to known media/game/social CDNs (#692).
if a.Flows >= beaconMinFlows && a.external && (exfilDest || a.Category == "") {
avg := a.iatAvg / float64(a.Flows)
std := a.iatStd / float64(a.Flows)
if avg > 0 && std/avg <= beaconCVMax {
if avg >= beaconMinIntervalMs && avg <= beaconMaxIntervalMs && std/avg <= beaconCVMax {
alerts = append(alerts, base("beaconing",
fmt.Sprintf("%d flux périodiques (~%.0f ms)", a.Flows, avg)))
fmt.Sprintf("%d flux périodiques (~%.1f s)", a.Flows, avg/1000)))
}
}
// 4) unclassified flow to an external host with notable upload

View File

@ -1,3 +1,15 @@
secubox-dpi (1.1.1-1~bookworm1) bookworm; urgency=low
* #692 collector: beaconing scenario now requires a C2-plausible cadence
(1 s1 h mean interval) to an external exfil-relevant/unclassified dest —
drops sub-second app/media chatter that previously raised false positives
(e.g. "~39 ms" alerts). Detail now reads in seconds.
* #693 dashboard: headline stat cards repointed to the real R3 exfil engine
(R3 devices / captured flows / categories / exfil alerts) instead of the
legacy netifyd widgets (netifyd is not used on the R3 boards).
-- Gerald KERMA <devel@cybermind.fr> Mon, 22 Jun 2026 10:15:00 +0000
secubox-dpi (1.1.0-1~bookworm1) bookworm; urgency=low
* #687 Phase 2/3: ship the per-device R3 cloud-exfiltration pipeline as a

View File

@ -327,22 +327,23 @@
</div>
</div>
<!-- #693 headline metrics driven by the R3 exfil engine (/exfil) -->
<div class="stats-row">
<div class="stat-card yellow">
<div class="value" id="exfil-devices-count">0</div>
<div class="label">📟 R3 Devices</div>
</div>
<div class="stat-card cyan">
<div class="value" id="flow-count">0</div>
<div class="label">Active Flows</div>
<div class="value" id="exfil-flows-count">0</div>
<div class="label">🌐 Flows (60s)</div>
</div>
<div class="stat-card green">
<div class="value" id="apps-count">0</div>
<div class="label">Applications</div>
<div class="value" id="exfil-cats-count">0</div>
<div class="label">🏷️ Categories</div>
</div>
<div class="stat-card purple">
<div class="value" id="protocols-count">0</div>
<div class="label">Protocols</div>
</div>
<div class="stat-card yellow">
<div class="value" id="devices-count">0</div>
<div class="label">Devices</div>
<div class="stat-card red">
<div class="value" id="exfil-alert-count">0</div>
<div class="label">🛰️ Exfil Alerts</div>
</div>
</div>
@ -446,7 +447,6 @@
const data = await api('/top_apps?limit=10');
const container = document.getElementById('top-apps');
const apps = Array.isArray(data) ? data : [];
document.getElementById('apps-count').textContent = apps.length;
container.innerHTML = apps.length > 0 ? apps.map(app => `
<div class="app-item">
@ -460,7 +460,6 @@
const data = await api('/top_protocols?limit=10');
const container = document.getElementById('top-protocols');
const protocols = Array.isArray(data) ? data : [];
document.getElementById('protocols-count').textContent = protocols.length;
container.innerHTML = protocols.length > 0 ? protocols.map(proto => `
<div class="app-item">
@ -474,7 +473,6 @@
const data = await api('/bandwidth_by_device');
const container = document.getElementById('bandwidth-devices');
const devices = Array.isArray(data) ? data : [];
document.getElementById('devices-count').textContent = devices.length;
container.innerHTML = devices.length > 0 ? devices.map(dev => `
<div class="app-item">
@ -488,7 +486,6 @@
const data = await api('/active_flows');
const container = document.getElementById('active-flows');
const flows = data.flows || [];
document.getElementById('flow-count').textContent = flows.length;
container.innerHTML = flows.length > 0 ? flows.slice(0, 15).map(f => `
<div class="app-item">
@ -556,6 +553,16 @@
badge.className = 'badge ' + (count > 0 ? 'badge-red' : 'badge-green');
card.style.borderColor = count > 0 ? '#ff4466' : 'var(--p31-decay)';
// #693 headline stat cards from the real DPI engine
const totalFlows = devices.reduce((n, d) => n + (d.flows || 0), 0);
const cats = new Set();
devices.forEach(d => Object.keys(d.by_category || {}).forEach(c => cats.add(c)));
const setCard = (id, v) => { const el = document.getElementById(id); if (el) el.textContent = v; };
setCard('exfil-devices-count', devices.length);
setCard('exfil-flows-count', totalFlows);
setCard('exfil-cats-count', cats.size);
setCard('exfil-alert-count', count);
// alert feed (severity-first)
alertsBox.innerHTML = alerts.length ? alerts.map(a => {
const k = EXFIL_KINDS[a.kind] || { label: (a.kind || '?').toUpperCase(), cls: 'badge-amber', icon: '⚠️' };