diff --git a/images/findings.jpg b/images/findings.jpg new file mode 100644 index 0000000..a12818e Binary files /dev/null and b/images/findings.jpg differ diff --git a/images/settings.jpg b/images/settings.jpg new file mode 100644 index 0000000..2b82aea Binary files /dev/null and b/images/settings.jpg differ diff --git a/src/fgai/baseline.py b/src/fgai/baseline.py index e60ecf6..3ce3a91 100644 --- a/src/fgai/baseline.py +++ b/src/fgai/baseline.py @@ -156,8 +156,10 @@ 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: - 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}) + matching = [event 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)] + samples = sorted({event.fields.get(field, "") for event in matching})[:5] + evidence_events = [{"timestamp": event.fields.get("eventtime", event.fields.get("timestamp", "")), "source": event.src_ip or event.fields.get("source", ""), "destination": event.dst_ip or "", "action": event.action, "severity": event.severity, "service": event.fields.get("service", ""), "value": event.fields.get(field, ""), "message": event.fields.get("message", event.fields.get("msg", ""))[:240]} for event in matching[: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, "sample_events": evidence_events}) # 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 28fe8f3..f2eb9ac 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -157,7 +157,7 @@ async function refresh() { 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_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 => ` `} + {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 => { const summary=esc(`${r.reason}; current ${r.current ?? '-'} vs baseline ${r.baseline ?? '-'}; values: ${(r.sample_values || []).join(', ') || '-'}`); const events=(r.sample_events || []).map(item => esc(`${item.timestamp} | ${item.source} -> ${item.destination} | ${item.action} ${item.service} | ${item.value} | ${item.message}`)).join('
'); return events ? `
${summary}

${events}

` : summary; }}, {label:'Action', render:r => ` `} ]); document.querySelectorAll('.feedback').forEach(button => button.addEventListener('click', async () => { const note = prompt('Review note (optional):') || '';