|
|
|
|
@@ -201,7 +201,7 @@ function graylogEvidenceLink(query, configuration, label='Open in Graylog') {
|
|
|
|
|
return ` <a href="${esc(href)}" target="_blank" rel="noreferrer">${esc(label)}</a>`;
|
|
|
|
|
}
|
|
|
|
|
const tableSort = {};
|
|
|
|
|
const uiCache = {correlations: [], fieldRows: [], correlationHitboxes: [], selectedCorrelationKey: ''};
|
|
|
|
|
const uiCache = {correlations: [], fieldRows: [], correlationHitboxes: [], selectedCorrelationKey: '', entityLabels: {}};
|
|
|
|
|
window.availableStreams = [];
|
|
|
|
|
window.streamSelection = {};
|
|
|
|
|
window.currentCorrelations = [];
|
|
|
|
|
@@ -258,7 +258,7 @@ function compactRelatedActivity(rows) {
|
|
|
|
|
const groups = new Map();
|
|
|
|
|
for (const row of rows || []) {
|
|
|
|
|
const key = `${row.entity || row.source_ip || '-'}|${row.stream || '-'}|${row.type || '-'}`;
|
|
|
|
|
const group = groups.get(key) || {entity: row.entity || row.source_ip || '-', stream: row.stream || '-', type: row.type || '-', count: 0, security: 0, actions: new Set(), destinations: new Set(), services: new Set(), first: row.timestamp || '', last: row.timestamp || '', samples: []};
|
|
|
|
|
const group = groups.get(key) || {entity: row.entity || row.source_ip || '-', entity_label: row.entity_label || entityDisplay(row.entity || row.source_ip, row), stream: row.stream || '-', type: row.type || '-', count: 0, security: 0, actions: new Set(), destinations: new Set(), services: new Set(), first: row.timestamp || '', last: row.timestamp || '', samples: []};
|
|
|
|
|
group.count += 1;
|
|
|
|
|
if (row.severity && !['-','info','notice','low'].includes(String(row.severity).toLowerCase())) group.security += 1;
|
|
|
|
|
if (row.action && row.action !== '-') group.actions.add(row.action);
|
|
|
|
|
@@ -274,6 +274,42 @@ function compactRelatedActivity(rows) {
|
|
|
|
|
function correlationKey(item) {
|
|
|
|
|
return String(item.entity || item.source_ip || '');
|
|
|
|
|
}
|
|
|
|
|
function correlationLabel(item) {
|
|
|
|
|
return String(item.entity_label || item.entity_display || item.entity || item.source_ip || '-');
|
|
|
|
|
}
|
|
|
|
|
function correlationShortLabel(item) {
|
|
|
|
|
const label = correlationLabel(item);
|
|
|
|
|
const key = correlationKey(item);
|
|
|
|
|
return label && label !== key ? label : key || '-';
|
|
|
|
|
}
|
|
|
|
|
function rememberEntityLabel(key, label, detail='') {
|
|
|
|
|
key = String(key || '');
|
|
|
|
|
label = String(label || key || '');
|
|
|
|
|
if (!key || !label) return;
|
|
|
|
|
uiCache.entityLabels[key] = {label, detail: String(detail || '')};
|
|
|
|
|
}
|
|
|
|
|
function entityDisplay(value, row={}) {
|
|
|
|
|
const key = String(row.entity || row.source_ip || row.src_ip || row.subject || value || '');
|
|
|
|
|
const explicit = row.entity_label || row.entity_display;
|
|
|
|
|
if (explicit) return String(row.entity_label || row.entity_display);
|
|
|
|
|
return (uiCache.entityLabels[key] || {}).label || key || String(value || '-');
|
|
|
|
|
}
|
|
|
|
|
function entityCell(row, key='entity') {
|
|
|
|
|
const value = row[key] || row.entity || row.source_ip || row.src_ip || row.subject || '';
|
|
|
|
|
const label = entityDisplay(value, row);
|
|
|
|
|
return esc(label || '-');
|
|
|
|
|
}
|
|
|
|
|
function refreshEntityLabels(correlations, context) {
|
|
|
|
|
for (const item of correlations || []) {
|
|
|
|
|
rememberEntityLabel(correlationKey(item), correlationShortLabel(item), item.entity_detail || item.source_ip || '');
|
|
|
|
|
}
|
|
|
|
|
for (const item of (context.source_profiles || [])) {
|
|
|
|
|
rememberEntityLabel(item.entity, item.entity_label || item.entity_display || item.entity, item.entity_detail || '');
|
|
|
|
|
}
|
|
|
|
|
for (const item of (context.security_event_samples || [])) {
|
|
|
|
|
rememberEntityLabel(item.entity, item.entity_label || item.entity_display || item.entity, item.entity_detail || '');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function renderCorrelationExplorer(correlations, configuration) {
|
|
|
|
|
const target = document.getElementById('correlationExplorer');
|
|
|
|
|
const rows = [...(correlations || [])].sort((a,b)=>(Number(b.security_events)||0)-(Number(a.security_events)||0) || (Number(b.events)||0)-(Number(a.events)||0)).slice(0,10);
|
|
|
|
|
@@ -287,14 +323,17 @@ function renderCorrelationExplorer(correlations, configuration) {
|
|
|
|
|
const selected = rows.find(item => correlationKey(item) === uiCache.selectedCorrelationKey) || rows[0];
|
|
|
|
|
const chips = rows.map(item => {
|
|
|
|
|
const key = correlationKey(item);
|
|
|
|
|
const label = `${key || '-'} (${Number(item.security_events || 0)}/${Number(item.events || 0)})`;
|
|
|
|
|
const label = `${correlationShortLabel(item)} (${Number(item.security_events || 0)}/${Number(item.events || 0)})`;
|
|
|
|
|
return `<button type="button" class="entity-chip ${key === uiCache.selectedCorrelationKey ? 'active' : ''}" data-correlation-key="${esc(key)}">${esc(label)}</button>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
const samples = (selected.samples || []).slice(0,8).map(item => {
|
|
|
|
|
const line = `${item.timestamp || ''} | ${item.stream || ''} | ${item.action || ''} | ${item.destination || ''} | ${item.service || ''} | ${item.context || item.message || ''}`;
|
|
|
|
|
return `<div class="evidence-item"><div>${esc(line)}</div>${item.graylog_query ? `<code>${esc(item.graylog_query)}</code>${graylogEvidenceLink(item.graylog_query, configuration)}` : ''}</div>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
target.innerHTML = `<div class="entity-picker">${chips}</div><div><strong>${esc(correlationKey(selected) || '-')}</strong><div class="muted">${esc((selected.streams || []).join(', ') || 'single stream')} · ${esc(selected.events || 0)} events · ${esc(selected.security_events || 0)} security events</div></div><div class="evidence-list">${samples || '<p class="muted">No sample evidence for this entity.</p>'}</div>`;
|
|
|
|
|
const selectedKey = correlationKey(selected);
|
|
|
|
|
const selectedLabel = correlationShortLabel(selected);
|
|
|
|
|
const keyNote = selectedLabel !== selectedKey ? ` · key ${selectedKey}` : '';
|
|
|
|
|
target.innerHTML = `<div class="entity-picker">${chips}</div><div><strong>${esc(selectedLabel || '-')}</strong><div class="muted">${esc((selected.streams || []).join(', ') || 'single stream')} · ${esc(selected.events || 0)} events · ${esc(selected.security_events || 0)} security events${esc(keyNote)}</div></div><div class="evidence-list">${samples || '<p class="muted">No sample evidence for this entity.</p>'}</div>`;
|
|
|
|
|
target.querySelectorAll('.entity-chip').forEach(button => button.addEventListener('click', () => {
|
|
|
|
|
uiCache.selectedCorrelationKey = button.dataset.correlationKey || '';
|
|
|
|
|
renderCorrelationExplorer(window.currentCorrelations || [], window.currentConfiguration || {});
|
|
|
|
|
@@ -424,7 +463,7 @@ function drawCorrelationGraph(correlations) {
|
|
|
|
|
ctx.fillText(short(label,24), x, y+r+14);
|
|
|
|
|
if (sub) { ctx.font='11px Arial'; ctx.fillStyle='#91abc4'; ctx.fillText(short(sub,28), x, y+r+28); }
|
|
|
|
|
};
|
|
|
|
|
drawNode(center.x, center.y, 24, Number(selected.security_events||0) ? '#d95f5f' : '#2389cc', correlationKey(selected), `${Number(selected.events)||0} events / ${Number(selected.security_events)||0} security`, correlationKey(selected));
|
|
|
|
|
drawNode(center.x, center.y, 24, Number(selected.security_events||0) ? '#d95f5f' : '#2389cc', correlationShortLabel(selected), `${Number(selected.events)||0} events / ${Number(selected.security_events)||0} security`, correlationKey(selected));
|
|
|
|
|
nodePositions.forEach(node => {
|
|
|
|
|
const color=node.kind==='stream' ? '#238b5d' : '#6f55c8';
|
|
|
|
|
const count=samples.filter(sample => sample.stream===node.name || sample.destination===node.name || sample.service===node.name || sample.type===node.name).length;
|
|
|
|
|
@@ -439,8 +478,8 @@ function drawCorrelationGraph(correlations) {
|
|
|
|
|
const key=correlationKey(item);
|
|
|
|
|
const y=sideY+22+index*23;
|
|
|
|
|
ctx.fillStyle=key===correlationKey(selected)?'#1ea9ff':'#91abc4';
|
|
|
|
|
ctx.fillText(`${short(key,18)} ${Number(item.security_events||0)}/${Number(item.events||0)}`, sideX, y);
|
|
|
|
|
uiCache.correlationHitboxes.push({key,x:sideX-4,y:y-14,w:150,h:20});
|
|
|
|
|
ctx.fillText(`${short(correlationShortLabel(item),24)} ${Number(item.security_events||0)}/${Number(item.events||0)}`, sideX, y);
|
|
|
|
|
uiCache.correlationHitboxes.push({key,x:sideX-4,y:y-14,w:210,h:20});
|
|
|
|
|
});
|
|
|
|
|
ctx.textAlign='left'; ctx.fillStyle='#91abc4'; ctx.font='12px Arial'; ctx.fillText('Selected entity is centered. Green nodes are streams; purple nodes are destinations/services. Click a listed entity or center node evidence chip to change focus.', 8, ch-14);
|
|
|
|
|
}
|
|
|
|
|
@@ -465,6 +504,7 @@ async function refresh() {
|
|
|
|
|
const configuration = data.configuration || {};
|
|
|
|
|
const cache = data.status_cache || {};
|
|
|
|
|
const streamCoverage = data.stream_coverage || [];
|
|
|
|
|
const context = data.event_context || {};
|
|
|
|
|
const enabledStreams = streamCoverage.filter(item => item.enabled);
|
|
|
|
|
const streamsMissingProfile = enabledStreams.filter(item => !item.profile_ready).length;
|
|
|
|
|
const enabledWithNoRawEvents = enabledStreams.filter(item => Number(item.events_fetched || 0) === 0);
|
|
|
|
|
@@ -477,6 +517,7 @@ async function refresh() {
|
|
|
|
|
const correlationsCached = rawCorrelations.length === 0 && uiCache.correlations.length > 0;
|
|
|
|
|
const correlations = rawCorrelations.length ? rawCorrelations : uiCache.correlations;
|
|
|
|
|
if (rawCorrelations.length) uiCache.correlations = rawCorrelations;
|
|
|
|
|
refreshEntityLabels(correlations, context);
|
|
|
|
|
window.currentCorrelations = correlations;
|
|
|
|
|
window.currentConfiguration = configuration;
|
|
|
|
|
drawTrend(data.history || []);
|
|
|
|
|
@@ -552,7 +593,7 @@ async function refresh() {
|
|
|
|
|
const llmText = llm.text ? esc(llm.text).replace(/\\n/g, '<br>') : esc(llm.error || 'LLM assessment disabled or waiting for first run.');
|
|
|
|
|
document.getElementById('llmAssessment').innerHTML = `<div>Status: <code>${esc(llm.status || 'unknown')}</code></div><p>${llmText}</p>`;
|
|
|
|
|
document.getElementById('anomalies').innerHTML = table(data.anomalies || [], [
|
|
|
|
|
{label:'Source', key:'subject'},
|
|
|
|
|
{label:'Source', key:'subject', render:r => entityCell(r, 'subject')},
|
|
|
|
|
{label:'Score', key:'score'},
|
|
|
|
|
{label:'Severity', render:r => `<span class="sev-${esc(r.severity)}">${esc(r.severity)}</span>`},
|
|
|
|
|
{label:'Confidence', key:'confidence'},
|
|
|
|
|
@@ -564,7 +605,7 @@ async function refresh() {
|
|
|
|
|
{label:'Reasons', render:r => esc((r.reasons || []).join('; '))}
|
|
|
|
|
], 'anomalies');
|
|
|
|
|
document.getElementById('recommendations').innerHTML = table(data.recommendations || [], [
|
|
|
|
|
{label:'Subject', key:'subject'},
|
|
|
|
|
{label:'Subject', key:'subject', render:r => entityCell(r, 'subject')},
|
|
|
|
|
{label:'Score', key:'score'},
|
|
|
|
|
{label:'Severity', render:r => `<span class="sev-${esc(r.severity)}">${esc(r.severity)}</span>`},
|
|
|
|
|
{label:'Title', key:'title'},
|
|
|
|
|
@@ -573,7 +614,7 @@ async function refresh() {
|
|
|
|
|
{label:'Services', render:r => esc((r.related_services || []).join(', '))}
|
|
|
|
|
], 'recommendations');
|
|
|
|
|
document.getElementById('incidents').innerHTML = table(data.incidents || [], [
|
|
|
|
|
{label:'Entity', key:'entity', render:r => esc(`${r.entity} (${r.entity_type || 'entity'})`)},
|
|
|
|
|
{label:'Entity', key:'entity', render:r => esc(`${entityDisplay(r.entity, r)} (${r.entity_type || 'entity'})`)},
|
|
|
|
|
{label:'Score', key:'score'},
|
|
|
|
|
{label:'Severity', render:r => `<span class="sev-${esc(r.severity)}">${esc(r.severity)}</span>`},
|
|
|
|
|
{label:'State', render:r => esc(r.lifecycle_status || 'open')},
|
|
|
|
|
@@ -589,15 +630,15 @@ async function refresh() {
|
|
|
|
|
refresh();
|
|
|
|
|
}));
|
|
|
|
|
document.getElementById('blocks').innerHTML = table(data.block_candidates || [], [
|
|
|
|
|
{label:'Source', key:'src_ip'},
|
|
|
|
|
{label:'Source', key:'src_ip', render:r => entityCell(r, 'src_ip')},
|
|
|
|
|
{label:'Score', key:'score'},
|
|
|
|
|
{label:'Reasons', render:r => esc((r.reasons || []).join('; '))}
|
|
|
|
|
], 'blocks');
|
|
|
|
|
const reputationRows = Object.entries(data.reputation || {}).map(([ip, intel]) => ({ip, ...intel}));
|
|
|
|
|
const relatedRows = correlations.flatMap(correlation => (correlation.samples || []).map(sample => ({entity: correlation.entity || correlation.source_ip, source_ip: correlation.source_ip, ...sample})));
|
|
|
|
|
const relatedRows = correlations.flatMap(correlation => (correlation.samples || []).map(sample => ({entity: correlation.entity || correlation.source_ip, entity_label: correlation.entity_label || correlation.entity_display || correlation.entity || correlation.source_ip, source_ip: correlation.source_ip, ...sample})));
|
|
|
|
|
const streamTitles = Object.fromEntries((configuration.graylog_streams || []).map(item => [item.id, item.title || item.id]));
|
|
|
|
|
document.getElementById('triageQueue').innerHTML = table(data.triage_queue || [], [
|
|
|
|
|
{label:'Entity', key:'entity'},
|
|
|
|
|
{label:'Entity', key:'entity', render:r => entityCell(r)},
|
|
|
|
|
{label:'Score', key:'score'},
|
|
|
|
|
{label:'Severity', render:r => `<span class="sev-${esc(r.severity)}">${esc(r.severity)}</span>`},
|
|
|
|
|
{label:'State', key:'state'},
|
|
|
|
|
@@ -621,7 +662,7 @@ async function refresh() {
|
|
|
|
|
document.getElementById('findingSummary').textContent = `${fieldRows.length} shown from ${sourceFieldRows.length} raw deviations. Reviewed and low-score findings are hidden by default.`;
|
|
|
|
|
if (rawFieldRows.length) uiCache.fieldRows = rawFieldRows;
|
|
|
|
|
document.getElementById('fieldDeviations').innerHTML = `${fieldRowsCached ? '<p class="muted">No current field deviations in this poll; showing cached findings from the last non-empty poll.</p>' : ''}` + table(fieldRows, [
|
|
|
|
|
{label:'Entity', key:'entity'}, {label:'Stream', key:'stream_title'}, {label:'Detector', key:'detector'}, {label:'Field', key:'field'}, {label:'Score', key:'score'}, {label:'Baseline', render:r => esc(`${r.confidence || '-'}; ${r.baseline_age_days ?? 0}d; ${r.baseline_samples || 0} samples`)}, {label:'Review', render:r => esc(r.feedback || 'unreviewed')}, {label:'Evidence', render:r => { const weighted = r.weight && r.weight !== 1 ? `; weighted ${r.base_score ?? r.score} x ${r.weight}` : ''; const summary=esc(`${r.reason}; current ${r.current ?? '-'} vs baseline ${r.baseline ?? '-'}; scope ${r.baseline_scope ?? '-'}${weighted}; 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}`)}${item.graylog_query ? `<br><code>${esc(item.graylog_query)}</code>${graylogEvidenceLink(item.graylog_query, configuration)}` : ''}`).join('<br>'); const id=`deviation:${r.entity}:${r.stream_id}:${r.field}:${r.value || ''}`; return events ? `<details data-detail-id="${esc(id)}"><summary>${summary}</summary><p>${events}</p></details>` : summary; }}, {label:'Review action', render:r => `<div class="review-actions"><button class="feedback" data-status="expected" data-entity="${esc(r.entity)}" data-stream="${esc(r.stream_id)}" data-field="${esc(r.field)}" data-value="${esc(r.value || '')}">Expected</button><button class="feedback" data-status="false_positive" data-entity="${esc(r.entity)}" data-stream="${esc(r.stream_id)}" data-field="${esc(r.field)}" data-value="${esc(r.value || '')}">False positive</button><button class="feedback" data-status="confirmed" data-entity="${esc(r.entity)}" data-stream="${esc(r.stream_id)}" data-field="${esc(r.field)}" data-value="${esc(r.value || '')}">Confirm</button></div>`}
|
|
|
|
|
{label:'Entity', key:'entity', render:r => entityCell(r)}, {label:'Stream', key:'stream_title'}, {label:'Detector', key:'detector'}, {label:'Field', key:'field'}, {label:'Score', key:'score'}, {label:'Baseline', render:r => esc(`${r.confidence || '-'}; ${r.baseline_age_days ?? 0}d; ${r.baseline_samples || 0} samples`)}, {label:'Review', render:r => esc(r.feedback || 'unreviewed')}, {label:'Evidence', render:r => { const weighted = r.weight && r.weight !== 1 ? `; weighted ${r.base_score ?? r.score} x ${r.weight}` : ''; const summary=esc(`${r.reason}; current ${r.current ?? '-'} vs baseline ${r.baseline ?? '-'}; scope ${r.baseline_scope ?? '-'}${weighted}; 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}`)}${item.graylog_query ? `<br><code>${esc(item.graylog_query)}</code>${graylogEvidenceLink(item.graylog_query, configuration)}` : ''}`).join('<br>'); const id=`deviation:${r.entity}:${r.stream_id}:${r.field}:${r.value || ''}`; return events ? `<details data-detail-id="${esc(id)}"><summary>${summary}</summary><p>${events}</p></details>` : summary; }}, {label:'Review action', render:r => `<div class="review-actions"><button class="feedback" data-status="expected" data-entity="${esc(r.entity)}" data-stream="${esc(r.stream_id)}" data-field="${esc(r.field)}" data-value="${esc(r.value || '')}">Expected</button><button class="feedback" data-status="false_positive" data-entity="${esc(r.entity)}" data-stream="${esc(r.stream_id)}" data-field="${esc(r.field)}" data-value="${esc(r.value || '')}">False positive</button><button class="feedback" data-status="confirmed" data-entity="${esc(r.entity)}" data-stream="${esc(r.stream_id)}" data-field="${esc(r.field)}" data-value="${esc(r.value || '')}">Confirm</button></div>`}
|
|
|
|
|
], 'field-deviations');
|
|
|
|
|
document.querySelectorAll('.feedback').forEach(button => button.addEventListener('click', async () => {
|
|
|
|
|
const note = prompt('Review note (optional):') || '';
|
|
|
|
|
@@ -632,7 +673,7 @@ async function refresh() {
|
|
|
|
|
}));
|
|
|
|
|
const relatedGroups = compactRelatedActivity(relatedRows);
|
|
|
|
|
document.getElementById('relatedActivity').innerHTML = `<p class="muted">${relatedGroups.length} grouped rows shown from ${relatedRows.length} raw related events.</p>` + table(relatedGroups, [
|
|
|
|
|
{label:'Entity', key:'entity'},
|
|
|
|
|
{label:'Entity', key:'entity', render:r => entityCell(r)},
|
|
|
|
|
{label:'Stream', key:'stream'},
|
|
|
|
|
{label:'Type', key:'type'},
|
|
|
|
|
{label:'Events', key:'count'},
|
|
|
|
|
@@ -657,24 +698,23 @@ async function refresh() {
|
|
|
|
|
{label:'Detail', key:'detail'}
|
|
|
|
|
], 'policies');
|
|
|
|
|
const d = data.diagnostics || {};
|
|
|
|
|
const context = data.event_context || {};
|
|
|
|
|
const quality = data.data_quality || {};
|
|
|
|
|
const profileNames = Object.fromEntries((data.stream_profiles || []).map(item => [item.stream_id, item.name || item.stream_id]));
|
|
|
|
|
const profileReadiness = (data.profile_readiness || []).map(item => ({...item, profile_name: item.profile_name || profileNames[item.stream_id] || item.stream_id, stream_title: item.stream_name || item.stream_title || streamTitles[item.stream_id] || item.stream_id}));
|
|
|
|
|
document.getElementById('diagnostics').innerHTML =
|
|
|
|
|
'<h3>Stream Coverage</h3>' + table(streamCoverage, [{label:'Stream', key:'stream_name'}, {label:'Enabled', key:'enabled', render:r => r.enabled ? 'yes' : 'no'}, {label:'Profile', render:r => esc(r.profile || 'missing')}, {label:'Entity Field', key:'entity_field'}, {label:'Tracked Fields', key:'tracked_fields'}, {label:'Ready Fields', key:'readiness'}, {label:'Raw Events', key:'events_fetched'}, {label:'Aggregate Events', key:'aggregate_events'}, {label:'Aggregate', key:'aggregate_status'}, {label:'Aggregate Schema', key:'aggregate_schema_properties'}, {label:'Latest Event', key:'latest_event_time'}, {label:'Health', render:r => esc(`${r.health || ''}${r.health_detail ? ': ' + r.health_detail : ''}`)}, {label:'Aggregate Error', render:r => esc(r.aggregate_error || '-')}, {label:'Raw Error', render:r => esc(r.raw_error || '-')}], 'stream-coverage') +
|
|
|
|
|
'<h3>Cross-Source Correlations</h3>' + table(correlations, [{label:'Entity', key:'entity', render:r => esc(`${r.entity || r.source_ip} (${r.entity_type || 'ip'})`)}, {label:'Streams', render:r => esc((r.streams || []).join(', '))}, {label:'Events', key:'events'}, {label:'Security Events', key:'security_events'}], 'correlations') +
|
|
|
|
|
'<h3>Entities</h3>' + table(context.source_profiles || [], [{label:'Entity', key:'entity'}, {label:'Events', key:'events'}, {label:'UTM', key:'utm_events'}, {label:'Deny', key:'deny_or_threat_actions'}, {label:'Destinations', key:'distinct_destinations'}, {label:'Actions', render:r => esc((r.top_actions || []).join(', '))}], 'entities') +
|
|
|
|
|
'<h3>Cross-Source Correlations</h3>' + table(correlations, [{label:'Entity', key:'entity', render:r => esc(`${correlationShortLabel(r)} (${r.entity_type || 'ip'})`)}, {label:'Streams', render:r => esc((r.streams || []).join(', '))}, {label:'Events', key:'events'}, {label:'Security Events', key:'security_events'}], 'correlations') +
|
|
|
|
|
'<h3>Entities</h3>' + table(context.source_profiles || [], [{label:'Entity', key:'entity', render:r => entityCell(r)}, {label:'Events', key:'events'}, {label:'UTM', key:'utm_events'}, {label:'Deny', key:'deny_or_threat_actions'}, {label:'Destinations', key:'distinct_destinations'}, {label:'Actions', render:r => esc((r.top_actions || []).join(', '))}], 'entities') +
|
|
|
|
|
'<h3>Profile Baseline Readiness</h3>' + table(profileReadiness, [{label:'Profile', key:'profile_name'}, {label:'Stream', key:'stream_title'}, {label:'Field', key:'field'}, {label:'Buckets', key:'buckets'}, {label:'Age days', key:'age_days'}, {label:'Training days', key:'training_days'}, {label:'Ready', key:'ready', render:r => r.ready ? 'ready' : 'learning'}], 'profile-readiness') +
|
|
|
|
|
'<h3>Data Quality</h3>' + table([quality], [{label:'Events', key:'events'}, {label:'Timestamp coverage', render:r => `${r.timestamp_coverage || 0}%`}, {label:'Source coverage', render:r => `${r.source_coverage || 0}%`}, {label:'Truncated streams', render:r => esc((r.truncated_streams || []).join(', ') || 'none')}]) +
|
|
|
|
|
'<h3>Security Event Samples</h3>' + table(context.security_event_samples || [], [{label:'Entity', key:'entity'}, {label:'Type', key:'type'}, {label:'Action', key:'action'}, {label:'Severity', key:'severity'}, {label:'Destination', key:'dst'}, {label:'Service', key:'service'}]) +
|
|
|
|
|
'<h3>Top Sources</h3>' + table(d.top_source_ips || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) +
|
|
|
|
|
'<h3>Security Event Samples</h3>' + table(context.security_event_samples || [], [{label:'Entity', key:'entity', render:r => entityCell(r)}, {label:'Type', key:'type'}, {label:'Action', key:'action'}, {label:'Severity', key:'severity'}, {label:'Destination', key:'dst'}, {label:'Service', key:'service'}]) +
|
|
|
|
|
'<h3>Top Sources</h3>' + table(d.top_source_ips || [], [{label:'Value', key:'value', render:r => entityCell({entity:r.value})}, {label:'Count', key:'count'}]) +
|
|
|
|
|
'<h3>Top Destinations</h3>' + table(d.top_destination_ips || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) +
|
|
|
|
|
'<h3>Top Policy IDs</h3>' + table(d.top_policy_ids || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) +
|
|
|
|
|
'<h3>Top Destination Ports</h3>' + table(d.top_destination_ports || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) +
|
|
|
|
|
'<h3>Top Source Ports</h3>' + table(d.top_source_ports || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) +
|
|
|
|
|
'<h3>Top Services</h3>' + table(d.top_services || [], [{label:'Value', key:'value'}, {label:'Count', key:'count'}]) +
|
|
|
|
|
'<h3>Local-in Failures</h3>' + table(d.local_in_failures || [], [{label:'Source', key:'src_ip'}, {label:'Service', key:'service'}, {label:'Policy', key:'policy'}, {label:'Count', key:'count'}]);
|
|
|
|
|
'<h3>Local-in Failures</h3>' + table(d.local_in_failures || [], [{label:'Source', key:'src_ip', render:r => entityCell(r, 'src_ip')}, {label:'Service', key:'service'}, {label:'Policy', key:'policy'}, {label:'Count', key:'count'}]);
|
|
|
|
|
document.querySelectorAll('details[data-detail-id]').forEach(item => { if (openDetails.has(item.dataset.detailId)) item.open = true; });
|
|
|
|
|
document.querySelectorAll('[data-sort-table]').forEach(button => button.addEventListener('click', () => { const current=tableSort[button.dataset.sortTable]; tableSort[button.dataset.sortTable]={key:button.dataset.sortKey,direction:current && current.key===button.dataset.sortKey ? -current.direction : 1}; refresh(); }));
|
|
|
|
|
document.querySelectorAll('.apply-suggested-profile').forEach(button => button.addEventListener('click', () => applySuggestedProfile(button.dataset.streamId)));
|
|
|
|
|
|