From b24b5d48a6b4d0fbd2a0c62438fe28593f30f63ab968562fcbc104e8830e7c1e Mon Sep 17 00:00:00 2001 From: larssand Date: Tue, 30 Jun 2026 16:36:07 +0200 Subject: [PATCH] add ui --- src/fgai/dashboard.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/fgai/dashboard.py b/src/fgai/dashboard.py index f2ccd92..aa9f307 100644 --- a/src/fgai/dashboard.py +++ b/src/fgai/dashboard.py @@ -163,6 +163,29 @@ function compactRelatedActivity(rows) { } return [...groups.values()].sort((left,right) => (right.security-left.security) || (right.count-left.count)).slice(0,25); } +function inferredProfileType(row) { + const explicit = row.detected_log_type || (row.discovery || {}).log_type; + if (explicit) return explicit; + const haystack = `${row.stream_name || ''} ${(row.common_fields || []).map(item => item.field || '').join(' ')} ${(row.categorical_fields || []).join(' ')}`.toLowerCase(); + if (/(windows|winlog|sysmon|powershell|security event)/.test(haystack)) return 'windows'; + if (/(fortigate|firewall|policyid|dstport|srcport)/.test(haystack)) return 'firewall'; + if (/(dns|adguard|query_domain|dns_query|\\bqh\\b)/.test(haystack)) return 'dns'; + if (/(nginx|apache|iis|squid|proxy|url|request_uri)/.test(haystack)) return 'web_proxy'; + if (/(audit|app|application|serverlog|prod)/.test(haystack)) return 'application'; + return 'unknown'; +} +function profileDiscoveryDetails(row) { + const d = row.discovery || {}; + if (!d.field_count) { + const common = (row.common_fields || []).slice(0,8).map(item => `${item.field} ${(Number(item.coverage || 0)*100).toFixed(0)}%/${item.unique_values}`).join(', '); + return `
waiting for discovery data

This row was produced before profile discovery metadata was added. Wait for the next monitor cycle after updating, or re-apply/edit the profile.

Observed common fields
${esc(common || '-')}

`; + } + const selected = d.selected_fields || {}; + const top = (d.top_fields || []).slice(0,8).map(item => `${item.field} ${(Number(item.coverage || 0)*100).toFixed(0)}%/${item.unique_values}`).join(', '); + const rejected = (d.rejected_fields || []).slice(0,8).map(item => `${item.field}: ${item.reason}`).join('; '); + const reasons = (d.reasons || []).join('; '); + return `
${esc(d.field_count)} fields analyzed

Selected
Entity: ${esc((selected.entity || []).join(', ') || '-')}
Time: ${esc((selected.time || []).join(', ') || '-')}
Categorical: ${esc((selected.categorical || []).join(', ') || '-')}
Numeric: ${esc((selected.numeric || []).join(', ') || '-')}

Top fields
${esc(top || '-')}

Rejected
${esc(rejected || '-')}

${esc(reasons || '')}

`; +} function drawTrend(history) { const canvas=document.getElementById('trendChart'), ctx=canvas.getContext('2d'), ratio=window.devicePixelRatio||1, cw=canvas.clientWidth, ch=canvas.clientHeight; canvas.width=cw*ratio; canvas.height=ch*ratio; ctx.scale(ratio,ratio); ctx.clearRect(0,0,cw,ch); @@ -298,7 +321,7 @@ async function refresh() { document.getElementById('profileSuggestions').innerHTML = visibleProfileSuggestions.length ? table(visibleProfileSuggestions, [ {label:'Stream', key:'stream_name'}, {label:'Events', key:'events'}, - {label:'Type', render:r => esc(r.detected_log_type || (r.discovery || {}).log_type || '-')}, + {label:'Type', render:r => esc(inferredProfileType(r))}, {label:'Confidence', key:'confidence'}, {label:'Advisor', render:r => esc((r.profile_advisor || {}).status || (advisor.enabled ? advisor.status : 'heuristic'))}, {label:'Profile', render:r => r.profile_exists ? 'exists' : 'new'}, @@ -307,7 +330,7 @@ async function refresh() { {label:'Baseline fields', render:r => esc([...(r.categorical_fields || []), ...(r.numeric_fields || [])].slice(0,8).join(', ') || '-')}, {label:'Detectors', render:r => esc(Object.keys(r.detectors || {}).join(', ') || '-')}, {label:'Common denominators', render:r => esc((r.common_fields || []).slice(0,5).map(item => `${item.field} ${(item.coverage*100).toFixed(0)}%`).join(', ') || '-')}, - {label:'Discovery', render:r => { const d=r.discovery || {}; const selected=d.selected_fields || {}; const top=(d.top_fields || []).slice(0,8).map(item => `${item.field} ${(Number(item.coverage || 0)*100).toFixed(0)}%/${item.unique_values}`).join(', '); const rejected=(d.rejected_fields || []).slice(0,8).map(item => `${item.field}: ${item.reason}`).join('; '); const reasons=(d.reasons || []).join('; '); return `
${esc(d.field_count || 0)} fields analyzed

Selected
Entity: ${esc((selected.entity || []).join(', ') || '-')}
Time: ${esc((selected.time || []).join(', ') || '-')}
Categorical: ${esc((selected.categorical || []).join(', ') || '-')}
Numeric: ${esc((selected.numeric || []).join(', ') || '-')}

Top fields
${esc(top || '-')}

Rejected
${esc(rejected || '-')}

${esc(reasons || '')}

`; }}, + {label:'Discovery', render:r => profileDiscoveryDetails(r)}, {label:'Action', render:r => r.profile_exists ? '' : ``} ], 'profile-suggestions') : '

No missing stream profiles. Enable "show existing profiles" to inspect already configured profiles.

'; const llmText = llm.text ? esc(llm.text).replace(/\\n/g, '
') : esc(llm.error || 'LLM assessment disabled or waiting for first run.');