mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
Merge pull request #721 from CyberMind-FR/feature/720-dpi-phase-3-per-device-daily-history-tim
Some checks are pending
License Headers / check (push) Waiting to run
Some checks are pending
License Headers / check (push) Waiting to run
dpi Phase 3: per-device daily history + timeline (closes #720)
This commit is contained in:
commit
cdbe51dcb2
|
|
@ -81,6 +81,36 @@ async def exfil_state():
|
|||
return {"generated_at": 0, "devices": [], "alerts": [], "alert_count": 0,
|
||||
"note": "no capture window completed yet (or wg-toolbox idle)"}
|
||||
|
||||
|
||||
@app.get("/history")
|
||||
async def exfil_history(device: str = "", days: int = 14):
|
||||
"""#720 — per-device DAILY timeline from the collector history.json. Without
|
||||
?device, returns board-wide daily totals. Fail-empty."""
|
||||
import json as _json
|
||||
from pathlib import Path as _P
|
||||
p = _P("/var/lib/secubox/dpi/history.json")
|
||||
try:
|
||||
rows = _json.loads(p.read_text()) if p.exists() else []
|
||||
except Exception as e: # pragma: no cover
|
||||
return {"device": device, "days": [], "error": str(e)}
|
||||
if device:
|
||||
per = [r for r in rows if r.get("device") == device]
|
||||
return {"device": device, "days": per[-days:]}
|
||||
# board-wide: sum per day
|
||||
by_day: dict = {}
|
||||
for r in rows:
|
||||
d = by_day.setdefault(r.get("day"), {
|
||||
"day": r.get("day"), "flows": 0, "up_bytes": 0, "down_bytes": 0,
|
||||
"alerts": 0, "devices": 0})
|
||||
d["flows"] += int(r.get("flows", 0) or 0)
|
||||
d["up_bytes"] += int(r.get("up_bytes", 0) or 0)
|
||||
d["down_bytes"] += int(r.get("down_bytes", 0) or 0)
|
||||
d["alerts"] += int(r.get("alerts", 0) or 0)
|
||||
d["devices"] += 1
|
||||
days_sorted = sorted(by_day.values(), key=lambda x: x["day"] or "")
|
||||
return {"device": "", "days": days_sorted[-days:]}
|
||||
|
||||
|
||||
app.include_router(auth_router, prefix="/auth")
|
||||
router = APIRouter()
|
||||
log = get_logger("dpi")
|
||||
|
|
|
|||
|
|
@ -52,8 +52,12 @@ var (
|
|||
cumulPath = env("SECUBOX_DPI_CUMUL", "/var/lib/secubox/dpi/cumulative.json")
|
||||
// #718 — ASN DB: classify SNI-less flows (IP-only/QUIC) by destination ASN.
|
||||
asnDBPath = env("SECUBOX_DPI_ASN", "/var/lib/GeoIP/GeoLite2-ASN.mmdb")
|
||||
// #720 — per-device DAILY history buckets for the timeline view.
|
||||
historyPath = env("SECUBOX_DPI_HISTORY", "/var/lib/secubox/dpi/history.json")
|
||||
)
|
||||
|
||||
const historyDays = 14 // keep two weeks of daily buckets
|
||||
|
||||
// #718 — ASN-org name fragments that mark a destination as cloud/hosting egress
|
||||
// (lowercase substring match). Used only when there is no SNI category, so a big
|
||||
// upload to an IP-only AWS/Google/etc host is still caught as cloud exfil.
|
||||
|
|
@ -396,9 +400,76 @@ func main() {
|
|||
saveSeen(newseen)
|
||||
writeState(aggs, alerts, now)
|
||||
updateCumulative(aggs, alerts, now)
|
||||
updateHistory(aggs, alerts, now)
|
||||
fmt.Printf("collector: %d flows-agg, %d alerts @ %d\n", len(aggs), len(alerts), now)
|
||||
}
|
||||
|
||||
// #720 — per-device DAILY history. Adds THIS window's increments to today's
|
||||
// bucket (exact, no double counting), prunes to historyDays. JSON-backed (the
|
||||
// static collector has no CGO SQLite driver); rich SQL can be layered later.
|
||||
func updateHistory(aggs map[string]*agg, alerts []alert, now int64) {
|
||||
type bucket struct {
|
||||
Day string `json:"day"`
|
||||
Device string `json:"device"`
|
||||
Flows int `json:"flows"`
|
||||
UpBytes int64 `json:"up_bytes"`
|
||||
DownBytes int64 `json:"down_bytes"`
|
||||
Alerts int `json:"alerts"`
|
||||
ByCat map[string]int `json:"by_category"`
|
||||
TS int64 `json:"ts"`
|
||||
}
|
||||
day := time.Unix(now, 0).UTC().Format("2006-01-02")
|
||||
// load existing → index by day|device
|
||||
idx := map[string]*bucket{}
|
||||
if b, err := os.ReadFile(historyPath); err == nil {
|
||||
var rows []*bucket
|
||||
if json.Unmarshal(b, &rows) == nil {
|
||||
for _, r := range rows {
|
||||
if r.ByCat == nil {
|
||||
r.ByCat = map[string]int{}
|
||||
}
|
||||
idx[r.Day+"|"+r.Device] = r
|
||||
}
|
||||
}
|
||||
}
|
||||
// add this window's per-device increments
|
||||
for _, a := range aggs {
|
||||
k := day + "|" + a.Device
|
||||
bk := idx[k]
|
||||
if bk == nil {
|
||||
bk = &bucket{Day: day, Device: a.Device, ByCat: map[string]int{}}
|
||||
idx[k] = bk
|
||||
}
|
||||
bk.Flows += a.Flows
|
||||
bk.UpBytes += a.Up
|
||||
bk.DownBytes += a.Down
|
||||
if a.Category != "" {
|
||||
bk.ByCat[a.Category] += a.Flows
|
||||
}
|
||||
bk.TS = now
|
||||
}
|
||||
for _, al := range alerts {
|
||||
if bk := idx[day+"|"+al.Device]; bk != nil {
|
||||
bk.Alerts++
|
||||
}
|
||||
}
|
||||
// prune to historyDays, sort by day then device
|
||||
cutoff := time.Unix(now, 0).UTC().AddDate(0, 0, -historyDays).Format("2006-01-02")
|
||||
out := make([]*bucket, 0, len(idx))
|
||||
for _, bk := range idx {
|
||||
if bk.Day >= cutoff {
|
||||
out = append(out, bk)
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].Day != out[j].Day {
|
||||
return out[i].Day < out[j].Day
|
||||
}
|
||||
return out[i].Device < out[j].Device
|
||||
})
|
||||
writeJSON(historyPath, out)
|
||||
}
|
||||
|
||||
// cumDev is the persisted per-device cumulative rollup. Superset of the state
|
||||
// device schema (adds last_seen/first_seen) so the report's _dpi_stats reads it
|
||||
// unchanged.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
secubox-dpi (1.1.4-1~bookworm1) bookworm; urgency=low
|
||||
|
||||
* #720 Phase 3 — per-device DAILY history buckets (collector history.json, 14d)
|
||||
+ GET /api/v1/dpi/history (per-device or board-wide timeline) + dashboard
|
||||
"Timeline" panel (flux/jour, red bar = alert day).
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Mon, 22 Jun 2026 13:45:00 +0000
|
||||
|
||||
secubox-dpi (1.1.3-1~bookworm1) bookworm; urgency=low
|
||||
|
||||
* #718 Phase 3 — ASN enrichment: classify SNI-less flows (IP-only / QUIC,
|
||||
|
|
|
|||
|
|
@ -360,6 +360,14 @@
|
|||
<p id="exfil-foot" style="color: var(--text-dim); font-size: 0.7rem; margin-top: 0.75rem;"></p>
|
||||
</div>
|
||||
|
||||
<!-- #720 — per-day timeline (board-wide) -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>📈 Timeline — flux/jour (14 j)</h2></div>
|
||||
<div id="dpi-timeline" style="display:flex; align-items:flex-end; gap:4px; height:90px; padding-top:8px;">
|
||||
<p class="empty">Loading…</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>📊 Top Applications</h2></div>
|
||||
|
|
@ -654,9 +662,27 @@
|
|||
loadBlockRules();
|
||||
}
|
||||
|
||||
async function loadTimeline() {
|
||||
const data = await api('/history?days=14');
|
||||
const box = document.getElementById('dpi-timeline');
|
||||
const days = (data && data.days) || [];
|
||||
if (!days.length) { box.innerHTML = '<p class="empty">Pas encore d\'historique (≥1 jour de capture).</p>'; return; }
|
||||
const max = Math.max(1, ...days.map(d => d.flows || 0));
|
||||
box.innerHTML = days.map(d => {
|
||||
const h = Math.round(6 + 78 * (d.flows || 0) / max);
|
||||
const dd = (d.day || '').slice(5); // MM-DD
|
||||
return `<div style="flex:1; display:flex; flex-direction:column; align-items:center; justify-content:flex-end; min-width:0;">
|
||||
<span style="font-size:0.6rem; color:var(--p31-mid);">${d.flows||0}</span>
|
||||
<span title="${d.day}: ${d.flows||0} flux · ${d.alerts||0} alertes" style="display:block; width:100%; max-width:18px; height:${h}px; border-radius:3px 3px 0 0; background:${(d.alerts||0)>0 ? '#ff4466' : 'var(--p31-peak)'};"></span>
|
||||
<span style="font-size:0.55rem; color:var(--text-dim); white-space:nowrap;">${dd}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function refreshAll() {
|
||||
loadStatus();
|
||||
loadExfil(); // #695: also fills Top Apps/Protocols/Bandwidth/Active-Flows from the exfil engine
|
||||
loadTimeline();
|
||||
loadBlockRules();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user