poc4 wit rust
This commit is contained in:
@@ -68,3 +68,5 @@ html,body{min-height:100%;background:var(--bg);color:var(--text)}body{overflow-x
|
||||
/* Compact secondary navigation inside data-heavy primary views. */
|
||||
.subtabs{display:flex;align-items:center;gap:5px;margin:0 0 12px;padding:5px;border:1px solid var(--line-soft);border-radius:9px;background:#0e0f11;overflow-x:auto;scrollbar-width:thin}.subtab-button{flex:0 0 auto;border:1px solid transparent;border-radius:6px;background:transparent;color:#777880;padding:7px 11px;font-size:11px;font-weight:600;cursor:pointer;white-space:nowrap;transition:.14s ease}.subtab-button:hover{color:#d6d6db;background:#141517}.subtab-button.active{color:#f4f4f5;background:#191a1d;border-color:#2b2c30;box-shadow:0 1px 2px rgba(0,0,0,.18)}.subtab-button.active::before{content:'';display:inline-block;width:6px;height:6px;margin-right:7px;border-radius:999px;background:var(--green);vertical-align:1px}.subtab-panel{display:none}.subtab-panel.active{display:block}.panel-stack{display:grid;gap:12px;align-content:start;min-width:0}
|
||||
@media(max-width:780px){.subtabs{margin-bottom:10px}.subtab-button{padding:7px 10px}.panel-stack{width:100%}}
|
||||
|
||||
.metric-sub.metric-sub-bad{color:#d99a9a}
|
||||
|
||||
+36
-14
@@ -14,7 +14,7 @@
|
||||
liveEnabled: false, paused: false, live: [], liveById: new Map(), liveSequence: 0,
|
||||
liveRenderTimer: null, liveFilterTimer: null, historyLoaded: false, snapshot: [],
|
||||
batchTimes: [], uiDropped: 0, serverDropped: 0,
|
||||
incidents: [], analytics: null, analyticsWindow: 0, throughput: null, throughputWindow: 0, status: null, config: null, ruleSources: [], ruleSourcesLoaded: false,
|
||||
incidents: [], analytics: null, analyticsWindow: 0, throughput: null, throughputWindow: 0, currentThroughput: null, currentThroughputWindow: 0, status: null, config: null, ruleSources: [], ruleSourcesLoaded: false,
|
||||
selectedRuleSources: new Set(), sourceQueue: null, sourceQueueTimer: null,
|
||||
ndrIncidents: [], assets: [], iocs: [], pcaps: [], pcapMode: 'blocks', ndrSummary: {},
|
||||
ruleIntelligence: [], ruleSnapshots: [], mergedRulesOffset: 0, mergedRulesQuery: '', backups: [], audit: [],
|
||||
@@ -374,16 +374,40 @@
|
||||
state.analyticsPollTimer=setTimeout(()=>{if(Number(windowSec)===selectedWindow())loadAnalytics(windowSec,true);},delay);
|
||||
}
|
||||
|
||||
function renderCurrentThroughput(t) {
|
||||
const windowSec=Number(t?.window_seconds||selectedWindow());
|
||||
if(windowSec && windowSec!==selectedWindow())return;
|
||||
state.currentThroughput=t||{}; state.currentThroughputWindow=selectedWindow();
|
||||
const total=Math.max(0,Number(t?.current_bps||0)), inbound=Math.max(0,Number(t?.current_in_bps||0)), outbound=Math.max(0,Number(t?.current_out_bps||0));
|
||||
const ingress=Math.max(0,Number(t?.current_ingress_bps||0));
|
||||
const other=Math.max(0,Number(t?.current_other_bps ?? (total-inbound-outbound))), pps=Math.max(0,Number(t?.current_pps||0));
|
||||
if($('metricThroughput'))$('metricThroughput').textContent=fmtBits(total);
|
||||
if($('metricThroughputSplit'))$('metricThroughputSplit').textContent=`IN ${fmtBits(inbound)} · OUT ${fmtBits(outbound)}${other>0?` · OTHER ${fmtBits(other)}`:''} · ${Math.round(pps).toLocaleString()} pps`;
|
||||
const quality=$('metricThroughputQuality');
|
||||
if(quality){
|
||||
const age=t?.current_sample_age_ms;
|
||||
const ageText=Number.isFinite(Number(age))?`${(Number(age)/1000).toFixed(1)}s`:'waiting';
|
||||
const capture=Math.max(0,Math.min(100,Number(t?.capture_efficiency_pct ?? 0)));
|
||||
const queue=Math.max(0,Math.min(100,Number(t?.queue_fill_pct||0)));
|
||||
const loss=Math.max(0,Number(t?.loss_pps||0));
|
||||
const inspectRatio=Math.max(0,Math.min(100,Number(t?.inspection_ratio_pct ?? 100)));
|
||||
const kernelDrops=Math.max(0,Number(t?.kernel_udp_drops||0));
|
||||
const queueDrops=Math.max(0,Number(t?.queue_dropped_datagrams||0));
|
||||
const truncated=Math.max(0,Number(t?.truncated_datagrams||0));
|
||||
const buffer=Number(t?.rcvbuf_bytes||0), queueBytes=Number(t?.queue_capacity_bytes||0);
|
||||
quality.textContent=`TZSP RX ${fmtBits(ingress)} · capture ${capture.toFixed(1)}% · queue ${queue.toFixed(0)}% · loss ${loss.toFixed(loss<10?1:0)}/s`;
|
||||
quality.title=`sample ${ageText} old · inspected/TZSP ${inspectRatio.toFixed(1)}% · socket ${fmtBytes(buffer)} · userspace queue ${fmtBytes(queueBytes)} · kernel drops ${kernelDrops.toLocaleString()} · queue drops ${queueDrops.toLocaleString()} · truncated ${truncated.toLocaleString()}`;
|
||||
const pipelineBehind=ingress>10_000_000 && inspectRatio<90;
|
||||
const unhealthy=loss>0||queue>=80||pipelineBehind||t?.current_sample_fresh===false||t?.rx_thread_alive===false||t?.worker_thread_alive===false;
|
||||
quality.classList.toggle('metric-sub-bad',unhealthy);
|
||||
}
|
||||
}
|
||||
|
||||
function renderThroughput(t) {
|
||||
const windowSec=Number(t?.window_seconds||selectedWindow());
|
||||
if(windowSec!==selectedWindow())return;
|
||||
state.throughput=t; state.throughputWindow=windowSec;
|
||||
if($('metricThroughput'))$('metricThroughput').textContent=fmtBits(t.current_bps||0);
|
||||
if($('metricThroughputSplit')){
|
||||
const total=Math.max(0,Number(t.current_bps||0)), inbound=Math.max(0,Number(t.current_in_bps||0)), outbound=Math.max(0,Number(t.current_out_bps||0));
|
||||
const other=Math.max(0,Number(t.current_other_bps ?? (total-inbound-outbound)));
|
||||
$('metricThroughputSplit').textContent=`IN ${fmtBits(inbound)} · OUT ${fmtBits(outbound)}${other>0?` · OTHER ${fmtBits(other)}`:''}`;
|
||||
}
|
||||
renderCurrentThroughput(t);
|
||||
if($('metricPeakThroughput'))$('metricPeakThroughput').textContent=fmtBits(t.peak_bps||0);
|
||||
if($('metricBytes'))$('metricBytes').textContent=fmtBytes(t.bytes||0);
|
||||
const charts=window.MikroSuricataCharts; if(charts?.drawThroughput)charts.drawThroughput($('throughputChart'),t.timeline||[]);
|
||||
@@ -420,12 +444,8 @@
|
||||
if(!state.throughput || state.throughputWindow!==windowSec){state.throughput=a;state.throughputWindow=windowSec;}
|
||||
$('metricEvents').textContent = Number(a.events||0).toLocaleString();
|
||||
const traffic=(state.throughput && state.throughputWindow===windowSec)?state.throughput:a;
|
||||
if($('metricThroughput'))$('metricThroughput').textContent=fmtBits(traffic.current_bps||0);
|
||||
if($('metricThroughputSplit')){
|
||||
const total=Math.max(0,Number(traffic.current_bps||0)), inbound=Math.max(0,Number(traffic.current_in_bps||0)), outbound=Math.max(0,Number(traffic.current_out_bps||0));
|
||||
const other=Math.max(0,Number(traffic.current_other_bps ?? (total-inbound-outbound)));
|
||||
$('metricThroughputSplit').textContent=`IN ${fmtBits(inbound)} · OUT ${fmtBits(outbound)}${other>0?` · OTHER ${fmtBits(other)}`:''}`;
|
||||
}
|
||||
const live=(state.currentThroughput && state.currentThroughputWindow===windowSec)?state.currentThroughput:a;
|
||||
renderCurrentThroughput(live);
|
||||
if($('metricPeakThroughput'))$('metricPeakThroughput').textContent=fmtBits(traffic.peak_bps||0);
|
||||
$('metricBytes').textContent = fmtBytes(traffic.bytes||0); $('metricAlerts').textContent = Number(a.alerts||0).toLocaleString(); $('metricBlocked').textContent = Number(a.blocked||0).toLocaleString();
|
||||
$('metricEventRate').textContent = `${Math.round(Number(a.events||0)/(Number(a.window_seconds||3600)/60)).toLocaleString()} / min`;
|
||||
@@ -542,7 +562,7 @@
|
||||
$('ndrIncidentRows').innerHTML=state.ndrIncidents.length?state.ndrIncidents.map(x=>`<tr><td><span class="risk-score ${riskClass(x.risk_score)}">${Number(x.risk_score||0)}</span></td><td>${fmtTime(x.last_seen)}</td><td class="mono">${esc(x.subject_ip||'—')}</td><td class="break stages-col">${esc((x.stages||[]).join(' → ')||'detection')}</td><td>${renderAttack(x.mitre)}</td><td class="details-cell" title="${esc(x.summary||x.title||'')}">${esc(x.summary||x.title||'—')}</td><td>${Number(x.event_count||0).toLocaleString()}${x.blocked?' · blocked':''}</td><td><span class="status-chip ${x.status==='open'?'bad':''}">${esc(x.status||'open')}</span></td><td><button class="link-btn" data-ndr-incident="${Number(x.id)}">evidence</button> · <button class="link-btn" data-ndr-status="${Number(x.id)}" data-status="${x.status==='closed'?'open':'closed'}">${x.status==='closed'?'reopen':'close'}</button></td></tr>`).join(''):'<tr><td colspan="9" class="empty">No correlated NDR incidents yet.</td></tr>';
|
||||
$('assetRows').innerHTML=state.assets.length?state.assets.map(x=>`<tr><td><span class="risk-score ${riskClass(x.risk_score)}">${Number(x.risk_score||0)}</span></td><td class="mono">${esc(x.ip)}</td><td><strong>${esc(x.hostname||'—')}</strong><div class="muted mono">${esc(x.mac||x.identity_source||'—')}</div></td><td class="break">${esc((x.protocols||[]).slice(0,8).join(', ')||'—')}</td><td class="break">${esc((x.ports||[]).slice(0,12).join(', ')||'—')}</td><td>${Number(x.alert_count||0).toLocaleString()}</td><td>${fmtTime(x.last_seen)}</td></tr>`).join(''):'<tr><td colspan="7" class="empty">Assets appear after traffic or RouterOS inventory sync.</td></tr>';
|
||||
$('iocRows').innerHTML=state.iocs.length?state.iocs.map(x=>`<tr><td><span class="status-chip">${esc(x.indicator_type)}</span></td><td class="mono break">${esc(x.indicator)}</td><td>${Number(x.confidence||0)}%</td><td>S${esc(x.severity||'—')}</td><td>${esc(x.source||'—')}</td><td>${Number(x.hit_count||0).toLocaleString()}</td><td>${fmtTime(x.last_hit_at)}</td><td><button class="link-btn danger-link" data-delete-ioc="${Number(x.id)}">delete</button></td></tr>`).join(''):'<tr><td colspan="8" class="empty">No local IOCs configured.</td></tr>';
|
||||
const pcapDescriptions={blocks:'Mode: blocks · PCAP is persisted only after a successful RouterOS block; recent packets come from the bounded RAM ring.',alerts:'Mode: alerts · Suricata persists packets associated with alerts.',all:'Mode: all · Suricata persists all observed packets into the rotating PCAP log.',off:'Mode: off · forensic PCAP persistence is disabled.'};
|
||||
const pcapDescriptions={blocks:'Mode: blocks (legacy) · with the Rust data-plane this is converted to Suricata alert capture so Python never processes every packet.',alerts:'Mode: alerts · Suricata persists packets associated with alerts.',all:'Mode: all · Suricata persists all observed packets into the rotating PCAP log.',off:'Mode: off · forensic PCAP persistence is disabled.'};
|
||||
if($('pcapMeta'))$('pcapMeta').textContent=pcapDescriptions[state.pcapMode]||`Mode: ${state.pcapMode}`;
|
||||
$('pcapRows').innerHTML=state.pcaps.length?state.pcaps.map(x=>{const url=`/api/forensics/pcap?name=${encodeURIComponent(x.name)}`;return `<tr><td class="mono">${esc(x.name)}</td><td>${fmtBytes(x.size_bytes)}</td><td>${fmtTime(Number(x.modified_at||0)*1000)}</td><td><a class="link-btn" href="${url}" data-download-url="${url}">download</a></td></tr>`;}).join(''):'<tr><td colspan="4" class="empty">No forensic PCAP files yet.</td></tr>';
|
||||
}
|
||||
@@ -708,6 +728,7 @@
|
||||
if(msg.data?.status)renderStatus(msg.data.status);
|
||||
if(msg.data?.analytics)renderAnalytics(msg.data.analytics);
|
||||
} else if(msg.type==='status')renderStatus(msg.data||{});
|
||||
else if(msg.type==='throughput')renderCurrentThroughput(msg.data||{});
|
||||
else if(msg.type==='analytics')renderAnalytics(msg.data||{});
|
||||
};
|
||||
ws.onclose=()=>{ if (!state.authEnabled || state.authenticated) scheduleReconnect(); };
|
||||
@@ -790,6 +811,7 @@
|
||||
api('/api/stats').then(renderStats),
|
||||
api('/api/alerts?limit=250').then(alerts=>{state.incidents=alerts.alerts||[];renderIncidents();}),
|
||||
loadOverviewSnapshot(windowSec,true),
|
||||
loadThroughput(windowSec,true),
|
||||
loadAnalytics(windowSec,true),
|
||||
state.view==='intelligence'?loadIntelligence(true):Promise.resolve(),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user