This commit is contained in:
larssand
2026-06-30 16:36:07 +02:00
parent 78f11af302
commit b24b5d48a6

View File

@@ -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 `<details><summary>waiting for discovery data</summary><p>This row was produced before profile discovery metadata was added. Wait for the next monitor cycle after updating, or re-apply/edit the profile.</p><p><b>Observed common fields</b><br>${esc(common || '-')}</p></details>`;
}
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 `<details><summary>${esc(d.field_count)} fields analyzed</summary><p><b>Selected</b><br>Entity: ${esc((selected.entity || []).join(', ') || '-')}<br>Time: ${esc((selected.time || []).join(', ') || '-')}<br>Categorical: ${esc((selected.categorical || []).join(', ') || '-')}<br>Numeric: ${esc((selected.numeric || []).join(', ') || '-')}</p><p><b>Top fields</b><br>${esc(top || '-')}</p><p><b>Rejected</b><br>${esc(rejected || '-')}</p><p>${esc(reasons || '')}</p></details>`;
}
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 `<details><summary>${esc(d.field_count || 0)} fields analyzed</summary><p><b>Selected</b><br>Entity: ${esc((selected.entity || []).join(', ') || '-')}<br>Time: ${esc((selected.time || []).join(', ') || '-')}<br>Categorical: ${esc((selected.categorical || []).join(', ') || '-')}<br>Numeric: ${esc((selected.numeric || []).join(', ') || '-')}</p><p><b>Top fields</b><br>${esc(top || '-')}</p><p><b>Rejected</b><br>${esc(rejected || '-')}</p><p>${esc(reasons || '')}</p></details>`; }},
{label:'Discovery', render:r => profileDiscoveryDetails(r)},
{label:'Action', render:r => r.profile_exists ? '<button type="button" disabled>Applied</button>' : `<button type="button" class="apply-suggested-profile" data-stream-id="${esc(r.stream_id)}">Apply profile</button>`}
], 'profile-suggestions') : '<p class="muted">No missing stream profiles. Enable "show existing profiles" to inspect already configured profiles.</p>';
const llmText = llm.text ? esc(llm.text).replace(/\\n/g, '<br>') : esc(llm.error || 'LLM assessment disabled or waiting for first run.');