From 59c8758ec80657e7fca86f409ad06ca235f338a53ce15a158767bf52c5aeaa96 Mon Sep 17 00:00:00 2001 From: larssand Date: Tue, 23 Jun 2026 08:35:07 +0200 Subject: [PATCH] add improvment on basle line finding --- src/fgai/baseline.py | 3 ++- src/fgai/dashboard.py | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/fgai/baseline.py b/src/fgai/baseline.py index 766c63d..e60ecf6 100644 --- a/src/fgai/baseline.py +++ b/src/fgai/baseline.py @@ -156,7 +156,8 @@ class BaselineStore: reason = f"{field} value deviates from its stream baseline" deviation = abs(current_value - mean(history)) if deviation > (pstdev(history) or 1.0) * 3: - output[entity].append({"field": field, "stream_id": stream, "score": 15, "reason": reason}) + samples = sorted({event.fields.get(field, "") for event in events if event.fields.get("fgai_stream_id") == stream and event.fields.get(str(getattr(profiles.get(stream), "entity_field", "")).lower()) == entity and event.fields.get(field)})[:5] + output[entity].append({"field": field, "stream_id": stream, "score": 15, "reason": reason, "current": round(current_value, 2), "baseline": round(mean(history), 2), "sample_values": samples}) # Detect selected categorical values that have not appeared for this entity in prior data. for event in events: profile = profiles.get(event.fields.get("fgai_stream_id", "")) diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index 12db6c5..28fe8f3 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -154,12 +154,15 @@ async function refresh() { ]); const reputationRows = Object.entries(data.reputation || {}).map(([ip, intel]) => ({ip, ...intel})); const relatedRows = (data.cross_source_correlations || []).flatMap(correlation => (correlation.samples || []).map(sample => ({source_ip: correlation.source_ip, ...sample}))); - const fieldRows = Object.entries(data.field_deviations || {}).flatMap(([entity, deviations]) => (deviations || []).map(item => ({entity, ...item}))); + const streamTitles = Object.fromEntries((configuration.graylog_streams || []).map(item => [item.id, item.title || item.id])); + const fieldRows = Object.entries(data.field_deviations || {}).flatMap(([entity, deviations]) => (deviations || []).map(item => ({entity, stream_title: streamTitles[item.stream_id] || item.stream_id, ...item}))); document.getElementById('fieldDeviations').innerHTML = table(fieldRows, [ - {label:'Entity', key:'entity'}, {label:'Stream', key:'stream_id'}, {label:'Field', key:'field'}, {label:'Score', key:'score'}, {label:'Review', render:r => esc(r.feedback || 'unreviewed')}, {label:'Evidence', key:'reason'}, {label:'Action', render:r => ` `} + {label:'Entity', key:'entity'}, {label:'Stream', key:'stream_title'}, {label:'Field', key:'field'}, {label:'Score', key:'score'}, {label:'Review', render:r => esc(r.feedback || 'unreviewed')}, {label:'Evidence', render:r => esc(`${r.reason}; current ${r.current ?? '-'} vs baseline ${r.baseline ?? '-'}; values: ${(r.sample_values || []).join(', ') || '-'}`)}, {label:'Action', render:r => ` `} ]); document.querySelectorAll('.feedback').forEach(button => button.addEventListener('click', async () => { - await fetch('/api/feedback', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({status:button.dataset.status, entity:button.dataset.entity, stream_id:button.dataset.stream, field:button.dataset.field})}); + const note = prompt('Review note (optional):') || ''; + const days = prompt('Expiry in days (0 = no expiry):', '0') || '0'; + await fetch('/api/feedback', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({status:button.dataset.status, entity:button.dataset.entity, stream_id:button.dataset.stream, field:button.dataset.field, note, expires_at: Number(days) > 0 ? Math.floor(Date.now()/1000) + Number(days) * 86400 : 0})}); refresh(); })); document.getElementById('relatedActivity').innerHTML = table(relatedRows, [