Autarch/web/templates/log_correlator.html

474 lines
20 KiB
HTML
Raw Normal View History

{% extends "base.html" %}
{% block title %}AUTARCH — Log Correlator{% endblock %}
{% block content %}
<div class="page-header">
<h1>Log Correlator</h1>
<p style="margin:0;font-size:0.85rem;color:var(--text-secondary)">
Ingest, correlate, and alert on security events from multiple log sources.
</p>
</div>
<!-- Tab Bar -->
<div class="tab-bar">
<button class="tab active" data-tab-group="logs" data-tab="ingest" onclick="showTab('logs','ingest')">Ingest</button>
<button class="tab" data-tab-group="logs" data-tab="alerts" onclick="showTab('logs','alerts')">Alerts</button>
<button class="tab" data-tab-group="logs" data-tab="rules" onclick="showTab('logs','rules')">Rules</button>
<button class="tab" data-tab-group="logs" data-tab="stats" onclick="showTab('logs','stats')">Stats</button>
</div>
<!-- ==================== INGEST TAB ==================== -->
<div class="tab-content active" data-tab-group="logs" data-tab="ingest">
<div class="section">
<h2>Ingest from File</h2>
<div class="input-row">
<input type="text" id="log-ingest-path" placeholder="Log file path (e.g. /var/log/auth.log)">
<button id="btn-ingest-file" class="btn btn-primary" onclick="logIngestFile()">Ingest File</button>
</div>
<pre class="output-panel" id="log-ingest-file-output" style="min-height:0"></pre>
</div>
<div class="section">
<h2>Paste Log Data</h2>
<div class="form-group">
<label>Paste raw log entries</label>
<textarea id="log-ingest-paste" rows="6" style="width:100%;background:var(--bg-input);border:1px solid var(--border);border-radius:var(--radius);color:var(--text-primary);padding:10px;font-family:monospace;font-size:0.85rem;resize:vertical" placeholder="Paste log lines here..."></textarea>
</div>
<div class="tool-actions">
<button id="btn-ingest-paste" class="btn btn-primary" onclick="logIngestPaste()">Ingest Pasted Data</button>
</div>
<pre class="output-panel" id="log-ingest-paste-output" style="min-height:0"></pre>
</div>
<div class="section">
<h2>Sources</h2>
<div class="tool-actions" style="margin-bottom:12px">
<button class="btn btn-small" onclick="logLoadSources()">Refresh</button>
</div>
<table class="data-table">
<thead><tr><th>Source</th><th>Type</th><th>Entries</th><th>Last Ingested</th></tr></thead>
<tbody id="log-sources-table">
<tr><td colspan="4" class="empty-state">No log sources ingested yet.</td></tr>
</tbody>
</table>
</div>
<div class="section">
<h2>Search Logs</h2>
<div class="input-row">
<input type="text" id="log-search-query" placeholder="Search pattern (regex supported)" onkeydown="if(event.key==='Enter')logSearch()">
<button id="btn-log-search" class="btn btn-primary" onclick="logSearch()">Search</button>
</div>
<span id="log-search-count" style="font-size:0.8rem;color:var(--text-muted)"></span>
<table class="data-table" style="margin-top:8px">
<thead><tr><th>Timestamp</th><th>Source</th><th>Log Entry</th></tr></thead>
<tbody id="log-search-results">
<tr><td colspan="3" class="empty-state">Enter a query and click Search.</td></tr>
</tbody>
</table>
</div>
<div class="section">
<div class="tool-actions">
<button class="btn btn-danger btn-small" onclick="logClearAll()">Clear All Logs</button>
</div>
</div>
</div>
<!-- ==================== ALERTS TAB ==================== -->
<div class="tab-content" data-tab-group="logs" data-tab="alerts">
<div class="section">
<h2>Security Alerts</h2>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
<span style="font-size:0.85rem;color:var(--text-secondary)">Severity:</span>
<button class="btn btn-small log-sev-btn active" data-severity="all" onclick="logFilterAlerts('all',this)">All</button>
<button class="btn btn-small log-sev-btn" data-severity="critical" onclick="logFilterAlerts('critical',this)" style="border-color:var(--danger);color:var(--danger)">Critical</button>
<button class="btn btn-small log-sev-btn" data-severity="high" onclick="logFilterAlerts('high',this)" style="border-color:#f97316;color:#f97316">High</button>
<button class="btn btn-small log-sev-btn" data-severity="medium" onclick="logFilterAlerts('medium',this)" style="border-color:var(--warning);color:var(--warning)">Medium</button>
<button class="btn btn-small log-sev-btn" data-severity="low" onclick="logFilterAlerts('low',this)" style="border-color:var(--accent);color:var(--accent)">Low</button>
<button class="btn btn-small" style="margin-left:auto" onclick="logRefreshAlerts()">Refresh</button>
</div>
<table class="data-table">
<thead><tr><th>Timestamp</th><th>Rule</th><th>Severity</th><th>Source</th><th>Log Entry</th></tr></thead>
<tbody id="log-alerts-table">
<tr><td colspan="5" class="empty-state">No alerts triggered yet.</td></tr>
</tbody>
</table>
<div class="tool-actions" style="margin-top:12px">
<button class="btn btn-danger btn-small" onclick="logClearAlerts()">Clear Alerts</button>
</div>
</div>
</div>
<!-- ==================== RULES TAB ==================== -->
<div class="tab-content" data-tab-group="logs" data-tab="rules">
<div class="section">
<h2>Detection Rules</h2>
<div class="tool-actions" style="margin-bottom:12px">
<button class="btn btn-small" onclick="logLoadRules()">Refresh</button>
</div>
<table class="data-table">
<thead><tr><th>ID</th><th>Name</th><th>Pattern</th><th>Severity</th><th>Type</th><th>Action</th></tr></thead>
<tbody id="log-rules-table">
<tr><td colspan="6" class="empty-state">No rules loaded.</td></tr>
</tbody>
</table>
</div>
<div class="section">
<h2>Add Custom Rule</h2>
<div class="form-row" style="margin-bottom:8px">
<div class="form-group">
<label>Rule ID</label>
<input type="text" id="log-rule-id" placeholder="e.g. CUSTOM-001">
</div>
<div class="form-group">
<label>Name</label>
<input type="text" id="log-rule-name" placeholder="e.g. SSH Root Login">
</div>
</div>
<div class="form-row" style="margin-bottom:8px">
<div class="form-group" style="flex:2">
<label>Pattern (regex)</label>
<input type="text" id="log-rule-pattern" placeholder="e.g. Failed password for root">
</div>
<div class="form-group">
<label>Severity</label>
<select id="log-rule-severity">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
</div>
<div class="form-row" style="margin-bottom:12px">
<div class="form-group">
<label>Threshold (count)</label>
<input type="number" id="log-rule-threshold" value="1" min="1" max="10000">
</div>
<div class="form-group">
<label>Window (seconds)</label>
<input type="number" id="log-rule-window" value="300" min="1" max="86400">
</div>
</div>
<div class="tool-actions">
<button id="btn-add-rule" class="btn btn-primary" onclick="logAddRule()">Add Rule</button>
</div>
<pre class="output-panel" id="log-rule-output" style="min-height:0"></pre>
</div>
</div>
<!-- ==================== STATS TAB ==================== -->
<div class="tab-content" data-tab-group="logs" data-tab="stats">
<div class="section">
<h2>Overview</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Total Logs</div>
<div class="stat-value" id="log-stat-total">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Alerts</div>
<div class="stat-value" id="log-stat-alerts">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Sources</div>
<div class="stat-value" id="log-stat-sources">--</div>
</div>
<div class="stat-card">
<div class="stat-label">Active Rules</div>
<div class="stat-value" id="log-stat-rules">--</div>
</div>
</div>
<div class="tool-actions" style="margin-bottom:12px">
<button class="btn btn-small" onclick="logLoadStats()">Refresh Stats</button>
</div>
</div>
<div class="section">
<h2>Alerts by Severity</h2>
<table class="data-table" style="max-width:400px">
<tbody id="log-stats-severity">
<tr><td>Critical</td><td id="log-sev-critical">--</td></tr>
<tr><td>High</td><td id="log-sev-high">--</td></tr>
<tr><td>Medium</td><td id="log-sev-medium">--</td></tr>
<tr><td>Low</td><td id="log-sev-low">--</td></tr>
</tbody>
</table>
</div>
<div class="section">
<h2>Top Triggered Rules</h2>
<table class="data-table">
<thead><tr><th>Rule</th><th>Count</th><th>Last Triggered</th></tr></thead>
<tbody id="log-stats-top-rules">
<tr><td colspan="3" class="empty-state">No data yet.</td></tr>
</tbody>
</table>
</div>
<div class="section">
<h2>Timeline (Hourly Alert Counts)</h2>
<div id="log-stats-timeline" style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;min-height:120px">
<div id="log-timeline-bars" style="display:flex;align-items:flex-end;gap:2px;height:100px"></div>
<div id="log-timeline-labels" style="display:flex;gap:2px;font-size:0.65rem;color:var(--text-muted);margin-top:4px"></div>
<p id="log-timeline-empty" class="empty-state" style="margin:0;padding:24px 0">No timeline data yet. Ingest logs and trigger rules to populate.</p>
</div>
</div>
</div>
<script>
function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;'); }
var _currentSevFilter = 'all';
/* ── Ingest ── */
function logIngestFile() {
var path = document.getElementById('log-ingest-path').value.trim();
if (!path) return;
var btn = document.getElementById('btn-ingest-file');
setLoading(btn, true);
postJSON('/logs/ingest/file', {path: path}).then(function(data) {
setLoading(btn, false);
renderOutput('log-ingest-file-output', data.message || data.error || 'Done');
if (data.success) logLoadSources();
}).catch(function() { setLoading(btn, false); });
}
function logIngestPaste() {
var text = document.getElementById('log-ingest-paste').value.trim();
if (!text) return;
var btn = document.getElementById('btn-ingest-paste');
setLoading(btn, true);
postJSON('/logs/ingest/paste', {data: text}).then(function(data) {
setLoading(btn, false);
renderOutput('log-ingest-paste-output', data.message || data.error || 'Done');
if (data.success) logLoadSources();
}).catch(function() { setLoading(btn, false); });
}
function logLoadSources() {
fetchJSON('/logs/sources').then(function(data) {
var tb = document.getElementById('log-sources-table');
var sources = data.sources || [];
if (!sources.length) {
tb.innerHTML = '<tr><td colspan="4" class="empty-state">No log sources ingested yet.</td></tr>';
return;
}
var html = '';
sources.forEach(function(s) {
html += '<tr><td>' + esc(s.name) + '</td><td>' + esc(s.type) + '</td>'
+ '<td>' + (s.entries || 0) + '</td><td>' + esc(s.last_ingested || '--') + '</td></tr>';
});
tb.innerHTML = html;
});
}
function logSearch() {
var query = document.getElementById('log-search-query').value.trim();
if (!query) return;
var btn = document.getElementById('btn-log-search');
setLoading(btn, true);
postJSON('/logs/search', {query: query}).then(function(data) {
setLoading(btn, false);
var results = data.results || [];
document.getElementById('log-search-count').textContent = results.length + ' results';
var tb = document.getElementById('log-search-results');
if (!results.length) {
tb.innerHTML = '<tr><td colspan="3" class="empty-state">No matches found.</td></tr>';
return;
}
var html = '';
results.forEach(function(r) {
html += '<tr><td style="white-space:nowrap;font-size:0.8rem">' + esc(r.timestamp || '--') + '</td>'
+ '<td>' + esc(r.source || '--') + '</td>'
+ '<td style="font-family:monospace;font-size:0.8rem">' + esc(r.entry) + '</td></tr>';
});
tb.innerHTML = html;
}).catch(function() { setLoading(btn, false); });
}
function logClearAll() {
if (!confirm('Clear all ingested logs? This cannot be undone.')) return;
postJSON('/logs/clear', {}).then(function(data) {
if (data.success) {
logLoadSources();
document.getElementById('log-search-results').innerHTML = '<tr><td colspan="3" class="empty-state">No matches found.</td></tr>';
document.getElementById('log-search-count').textContent = '';
}
});
}
/* ── Alerts ── */
function logRefreshAlerts() {
logFilterAlerts(_currentSevFilter);
}
function logFilterAlerts(severity, btnEl) {
_currentSevFilter = severity;
document.querySelectorAll('.log-sev-btn').forEach(function(b) {
b.classList.toggle('active', b.dataset.severity === severity);
});
fetchJSON('/logs/alerts?severity=' + severity).then(function(data) {
var tb = document.getElementById('log-alerts-table');
var alerts = data.alerts || [];
if (!alerts.length) {
tb.innerHTML = '<tr><td colspan="5" class="empty-state">No alerts' + (severity !== 'all' ? ' with severity "' + severity + '"' : '') + '.</td></tr>';
return;
}
var html = '';
alerts.forEach(function(a) {
var sev = (a.severity || 'low').toLowerCase();
var badgeClass = sev === 'critical' ? 'badge-fail'
: sev === 'high' ? 'badge-high'
: sev === 'medium' ? 'badge-medium' : 'badge-low';
html += '<tr><td style="white-space:nowrap;font-size:0.8rem">' + esc(a.timestamp || '--') + '</td>'
+ '<td>' + esc(a.rule || '--') + '</td>'
+ '<td><span class="badge ' + badgeClass + '">' + esc(a.severity) + '</span></td>'
+ '<td>' + esc(a.source || '--') + '</td>'
+ '<td style="font-family:monospace;font-size:0.8rem;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(a.entry || '') + '</td></tr>';
});
tb.innerHTML = html;
});
}
function logClearAlerts() {
if (!confirm('Clear all alerts?')) return;
postJSON('/logs/alerts/clear', {}).then(function(data) {
if (data.success) logRefreshAlerts();
});
}
/* ── Rules ── */
function logLoadRules() {
fetchJSON('/logs/rules').then(function(data) {
var tb = document.getElementById('log-rules-table');
var rules = data.rules || [];
if (!rules.length) {
tb.innerHTML = '<tr><td colspan="6" class="empty-state">No rules loaded.</td></tr>';
return;
}
var html = '';
rules.forEach(function(r) {
var sev = (r.severity || 'low').toLowerCase();
var badgeClass = sev === 'critical' ? 'badge-fail'
: sev === 'high' ? 'badge-high'
: sev === 'medium' ? 'badge-medium' : 'badge-low';
var typeBadge = r.builtin ? '<span class="badge badge-info">Built-in</span>' : '<span class="badge badge-medium">Custom</span>';
var deleteBtn = r.builtin ? '' : '<button class="btn btn-danger btn-small" onclick="logDeleteRule(\'' + esc(r.id) + '\')">Delete</button>';
html += '<tr><td style="font-family:monospace;font-size:0.85rem">' + esc(r.id) + '</td>'
+ '<td>' + esc(r.name) + '</td>'
+ '<td style="font-family:monospace;font-size:0.8rem">' + esc(r.pattern) + '</td>'
+ '<td><span class="badge ' + badgeClass + '">' + esc(r.severity) + '</span></td>'
+ '<td>' + typeBadge + '</td>'
+ '<td>' + deleteBtn + '</td></tr>';
});
tb.innerHTML = html;
});
}
function logAddRule() {
var rule = {
id: document.getElementById('log-rule-id').value.trim(),
name: document.getElementById('log-rule-name').value.trim(),
pattern: document.getElementById('log-rule-pattern').value.trim(),
severity: document.getElementById('log-rule-severity').value,
threshold: parseInt(document.getElementById('log-rule-threshold').value) || 1,
window: parseInt(document.getElementById('log-rule-window').value) || 300
};
if (!rule.id || !rule.name || !rule.pattern) {
renderOutput('log-rule-output', 'Rule ID, Name, and Pattern are required.');
return;
}
var btn = document.getElementById('btn-add-rule');
setLoading(btn, true);
postJSON('/logs/rules/add', rule).then(function(data) {
setLoading(btn, false);
renderOutput('log-rule-output', data.message || data.error || 'Done');
if (data.success) {
logLoadRules();
document.getElementById('log-rule-id').value = '';
document.getElementById('log-rule-name').value = '';
document.getElementById('log-rule-pattern').value = '';
}
}).catch(function() { setLoading(btn, false); });
}
function logDeleteRule(id) {
if (!confirm('Delete rule "' + id + '"?')) return;
postJSON('/logs/rules/delete', {id: id}).then(function(data) {
if (data.success) logLoadRules();
});
}
/* ── Stats ── */
function logLoadStats() {
fetchJSON('/logs/stats').then(function(data) {
document.getElementById('log-stat-total').textContent = data.total_logs || 0;
document.getElementById('log-stat-alerts').textContent = data.total_alerts || 0;
document.getElementById('log-stat-sources').textContent = data.total_sources || 0;
document.getElementById('log-stat-rules').textContent = data.total_rules || 0;
var bySev = data.alerts_by_severity || {};
document.getElementById('log-sev-critical').textContent = bySev.critical || 0;
document.getElementById('log-sev-high').textContent = bySev.high || 0;
document.getElementById('log-sev-medium').textContent = bySev.medium || 0;
document.getElementById('log-sev-low').textContent = bySev.low || 0;
var topRules = data.top_rules || [];
var trTb = document.getElementById('log-stats-top-rules');
if (!topRules.length) {
trTb.innerHTML = '<tr><td colspan="3" class="empty-state">No data yet.</td></tr>';
} else {
var html = '';
topRules.forEach(function(r) {
html += '<tr><td>' + esc(r.name) + '</td><td>' + r.count + '</td><td>' + esc(r.last_triggered || '--') + '</td></tr>';
});
trTb.innerHTML = html;
}
/* Timeline bars */
var timeline = data.timeline || [];
var emptyMsg = document.getElementById('log-timeline-empty');
var barsEl = document.getElementById('log-timeline-bars');
var labelsEl = document.getElementById('log-timeline-labels');
if (!timeline.length) {
emptyMsg.style.display = 'block';
barsEl.innerHTML = '';
labelsEl.innerHTML = '';
} else {
emptyMsg.style.display = 'none';
var maxVal = Math.max.apply(null, timeline.map(function(t) { return t.count; })) || 1;
var barsHtml = '';
var labelsHtml = '';
timeline.forEach(function(t) {
var pct = Math.max(2, Math.round((t.count / maxVal) * 100));
var color = t.count > maxVal * 0.7 ? 'var(--danger)' : t.count > maxVal * 0.4 ? 'var(--warning)' : 'var(--accent)';
barsHtml += '<div style="flex:1;height:' + pct + '%;background:' + color + ';border-radius:2px 2px 0 0;min-width:4px" title="' + esc(t.hour) + ': ' + t.count + ' alerts"></div>';
labelsHtml += '<div style="flex:1;text-align:center;min-width:4px">' + esc(t.hour || '') + '</div>';
});
barsEl.innerHTML = barsHtml;
labelsEl.innerHTML = labelsHtml;
}
});
}
/* ── Init ── */
document.addEventListener('DOMContentLoaded', function() {
logLoadSources();
logLoadRules();
logRefreshAlerts();
logLoadStats();
});
</script>
{% endblock %}