120 lines
31 KiB
HTML
120 lines
31 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Greenfield SIEM</title>
|
||
<style>
|
||
:root{--bg:#0b1020;--side:#0f172a;--panel:#111a2e;--panel2:#16213a;--line:#263653;--text:#e8eefb;--muted:#91a2bd;--brand:#5b8cff;--brand2:#7aa2ff;--green:#37c99b;--yellow:#f1b84b;--orange:#f17c4a;--red:#f05d6f;--purple:#ad7cff;--shadow:0 18px 50px rgba(0,0,0,.22)}*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.45 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}button,input,select,textarea{font:inherit}.app{min-height:100%;display:grid;grid-template-columns:250px 1fr}.sidebar{background:linear-gradient(180deg,#0e172b,#0b1325);border-right:1px solid var(--line);padding:22px 16px;position:sticky;top:0;height:100vh}.brand{display:flex;gap:11px;align-items:center;padding:0 8px 24px}.brandmark{width:34px;height:34px;border-radius:10px;background:linear-gradient(135deg,var(--brand),var(--purple));box-shadow:0 8px 26px #5b8cff44}.brand h1{font-size:16px;margin:0}.brand small{display:block;color:var(--muted);font-size:11px;margin-top:2px}.nav{display:grid;gap:5px}.nav button{border:0;background:transparent;color:var(--muted);padding:10px 12px;text-align:left;border-radius:9px;cursor:pointer;display:flex;align-items:center;gap:10px}.nav button:hover{background:#17223a;color:var(--text)}.nav button.active{background:#1b2a4a;color:#fff;box-shadow:inset 3px 0 var(--brand)}.nav .ico{width:22px;text-align:center;color:var(--brand2)}.sidebarfoot{position:absolute;left:16px;right:16px;bottom:18px;padding:12px;border:1px solid var(--line);border-radius:10px;background:#121d33;color:var(--muted);font-size:12px}.main{min-width:0}.topbar{height:70px;padding:0 26px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;background:#0c1323cc;backdrop-filter:blur(10px);position:sticky;top:0;z-index:20}.title h2{margin:0;font-size:18px}.title span{color:var(--muted);font-size:12px}.actions{display:flex;gap:8px;align-items:center}.btn{border:1px solid var(--line);color:var(--text);background:#152039;padding:8px 12px;border-radius:8px;cursor:pointer;text-decoration:none}.btn:hover{border-color:#45638f}.btn.primary{background:var(--brand);border-color:var(--brand);color:#fff}.btn.danger{background:#371b28;border-color:#613044;color:#ffabb6}.content{padding:24px 26px 50px;max-width:1900px;margin:0 auto}.view{display:none}.view.active{display:block}.kpis{display:grid;grid-template-columns:repeat(5,minmax(150px,1fr));gap:12px}.kpi,.panel{background:linear-gradient(180deg,var(--panel),#10192c);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.kpi{padding:16px}.kpi .label{color:var(--muted);font-size:12px}.kpi .value{font-size:27px;font-weight:750;margin-top:7px;letter-spacing:-.03em}.kpi .hint{font-size:11px;color:var(--muted);margin-top:4px}.grid2{display:grid;grid-template-columns:1.35fr 1fr;gap:14px;margin-top:14px}.grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-top:14px}.panel{padding:16px;min-width:0}.panelhead{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.panelhead h3{font-size:14px;margin:0}.panelhead span{color:var(--muted);font-size:11px}.spark{height:200px;width:100%;display:block}.barlist{display:grid;gap:9px}.barrow{display:grid;grid-template-columns:minmax(80px,1.2fr) 3fr 80px;gap:10px;align-items:center}.barlabel{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bartrack{height:8px;background:#202d47;border-radius:10px;overflow:hidden}.barfill{height:100%;background:linear-gradient(90deg,var(--brand),var(--purple));border-radius:10px}.barvalue{text-align:right;color:var(--muted);font-variant-numeric:tabular-nums}.filters{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}.field{background:#0e1729;border:1px solid var(--line);color:var(--text);border-radius:8px;padding:8px 10px;min-width:130px}.field::placeholder{color:#62728c}.tablewrap{overflow:auto;max-height:68vh;border:1px solid var(--line);border-radius:10px}table{border-collapse:collapse;width:100%;font-size:12px}th,td{padding:10px 11px;border-bottom:1px solid #20304c;text-align:left;vertical-align:top}th{position:sticky;top:0;background:#14213a;color:#a9b7cb;font-weight:600;z-index:2}tbody tr{background:#101a2e}tbody tr:nth-child(even){background:#0e182a}tbody tr.clickable:hover{background:#182744;cursor:pointer}.muted{color:var(--muted)}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.pill{display:inline-flex;align-items:center;gap:5px;padding:3px 7px;border-radius:999px;background:#20304b;color:#cbd7e9;font-size:11px;white-space:nowrap}.sev{font-weight:700;text-transform:uppercase;font-size:10px;letter-spacing:.04em}.sev-critical{color:#ff7c8b}.sev-high{color:#ff9a6b}.sev-medium{color:#ffd06b}.sev-low{color:#7bc4ff}.sev-info{color:#9fb0c8}.dot{width:7px;height:7px;border-radius:50%;display:inline-block;background:var(--green)}.switch{position:relative;width:38px;height:21px;display:inline-block}.switch input{display:none}.switch span{position:absolute;inset:0;border-radius:20px;background:#33435f;cursor:pointer}.switch span:after{content:"";position:absolute;width:15px;height:15px;left:3px;top:3px;background:white;border-radius:50%;transition:.15s}.switch input:checked+span{background:var(--brand)}.switch input:checked+span:after{transform:translateX(17px)}.rulesets{display:grid;grid-template-columns:repeat(4,minmax(210px,1fr));gap:10px;margin-bottom:14px}.ruleset{padding:13px;border:1px solid var(--line);background:#101b30;border-radius:10px}.ruleset h4{margin:0 0 3px;font-size:13px}.ruleset p{color:var(--muted);font-size:11px;height:33px;overflow:hidden;margin:5px 0 9px}.rulesetfoot{display:flex;justify-content:space-between;align-items:center}.tag{display:inline-block;font-size:10px;padding:2px 5px;border-radius:5px;background:#202e49;color:#b9c8df;margin:1px}.empty{padding:35px;text-align:center;color:var(--muted)}.alert{padding:10px 12px;border-radius:8px;margin-bottom:12px;background:#3a1d29;color:#ffb8c1;border:1px solid #643244}.success{background:#123326;color:#9ae3c4;border-color:#225841}.drawer{position:fixed;right:0;top:0;bottom:0;width:min(660px,92vw);background:#0d1729;border-left:1px solid var(--line);z-index:50;box-shadow:-20px 0 50px #0008;transform:translateX(105%);transition:.2s;padding:22px;overflow:auto}.drawer.open{transform:none}.drawer h3{margin:0}.drawergrid{display:grid;grid-template-columns:145px 1fr;gap:8px;margin-top:18px;font-size:12px}.drawergrid .k{color:var(--muted)}pre{white-space:pre-wrap;word-break:break-word;background:#08101f;border:1px solid var(--line);padding:12px;border-radius:8px;color:#cbd8ec}.modalback{position:fixed;inset:0;background:#0009;z-index:60;display:none;align-items:center;justify-content:center;padding:20px}.modalback.open{display:flex}.modal{width:min(760px,96vw);max-height:92vh;overflow:auto;background:#101a2d;border:1px solid var(--line);border-radius:14px;box-shadow:0 30px 80px #000b;padding:20px}.modal h3{margin-top:0}.formgrid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.formgrid .wide{grid-column:1/-1}.formgrid label{font-size:11px;color:var(--muted);display:block;margin-bottom:4px}.formgrid input,.formgrid select,.formgrid textarea{width:100%;background:#0a1425;border:1px solid var(--line);color:var(--text);padding:9px;border-radius:8px}.formgrid textarea{min-height:75px}.modalactions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}.sectionintro{display:flex;justify-content:space-between;gap:20px;align-items:end;margin-bottom:14px}.sectionintro h3{margin:0 0 3px;font-size:17px}.sectionintro p{margin:0;color:var(--muted);font-size:12px}.status-open{color:#ff9a6b}.status-investigating{color:#ffd06b}.status-closed{color:#7be2bb}.status-false_positive{color:#90a0ba}@media(max-width:1200px){.app{grid-template-columns:78px 1fr}.sidebar{padding:20px 9px}.brand h1,.brand small,.nav .label,.sidebarfoot{display:none}.brand{justify-content:center}.nav button{justify-content:center}.kpis{grid-template-columns:repeat(3,1fr)}.rulesets{grid-template-columns:1fr 1fr}}@media(max-width:800px){.app{display:block}.sidebar{height:auto;position:static;border-right:0;border-bottom:1px solid var(--line)}.brand,.sidebarfoot{display:none}.nav{grid-template-columns:repeat(6,1fr)}.nav button{padding:10px 4px}.main{width:100%}.content{padding:16px}.topbar{padding:0 15px}.kpis{grid-template-columns:1fr 1fr}.grid2,.grid3{grid-template-columns:1fr}.rulesets{grid-template-columns:1fr}.formgrid{grid-template-columns:1fr}.formgrid .wide{grid-column:auto}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app">
|
||
<aside class="sidebar">
|
||
<div class="brand"><div class="brandmark"></div><div><h1>Greenfield SIEM</h1><small>Tenant {{.Tenant}}</small></div></div>
|
||
<nav class="nav">
|
||
<button class="active" data-view="dashboard"><span class="ico">◆</span><span class="label">Übersicht</span></button>
|
||
<button data-view="events"><span class="ico">≡</span><span class="label">Events</span></button>
|
||
<button data-view="detections"><span class="ico">⚠</span><span class="label">Detections</span></button>
|
||
<button data-view="rules"><span class="ico">⌘</span><span class="label">Rule-Sets</span></button>
|
||
<button data-view="suppressions"><span class="ico">⊘</span><span class="label">Suppressions</span></button>
|
||
<button data-view="agents"><span class="ico">◉</span><span class="label">Agents</span></button>
|
||
</nav>
|
||
<div class="sidebarfoot"><div><span class="dot"></span> Pipeline verbunden</div><div style="margin-top:5px">ClickHouse + Redpanda + PostgreSQL</div></div>
|
||
</aside>
|
||
<main class="main">
|
||
<header class="topbar"><div class="title"><h2 id="viewTitle">Security Overview</h2><span id="viewSub">Ereignisse, Detections und Pipeline-Zustand</span></div><div class="actions"><a id="grafanaLink" class="btn" target="_blank" rel="noopener">Grafana ↗</a><button class="btn" onclick="refreshCurrent()">Aktualisieren</button></div></header>
|
||
<div class="content"><div id="notice"></div>
|
||
<section id="view-dashboard" class="view active">
|
||
<div class="kpis">
|
||
<div class="kpi"><div class="label">Events / 24h</div><div class="value" id="kEvents">–</div><div class="hint">dedupliziert</div></div>
|
||
<div class="kpi"><div class="label">Aktive Hosts / 24h</div><div class="value" id="kHosts">–</div><div class="hint">mit Event-Aktivität</div></div>
|
||
<div class="kpi"><div class="label">High / Critical offen</div><div class="value" id="kHigh">–</div><div class="hint">offen + investigating</div></div>
|
||
<div class="kpi"><div class="label">Aktive Regeln</div><div class="value" id="kRules">–</div><div class="hint" id="kRuleSets">– Rule-Sets</div></div>
|
||
<div class="kpi"><div class="label">Pipeline</div><div class="value" id="kPipeline" style="color:var(--green)">OK</div><div class="hint">API → Queue → ClickHouse</div></div>
|
||
</div>
|
||
<div class="grid2"><div class="panel"><div class="panelhead"><h3>Event-Volumen</h3><span>24 Stunden / 5-Minuten-Buckets</span></div><svg id="eventSpark" class="spark" viewBox="0 0 900 200" preserveAspectRatio="none"></svg></div><div class="panel"><div class="panelhead"><h3>Top Hosts</h3><span>Events / 24h</span></div><div id="topHosts" class="barlist"></div></div></div>
|
||
<div class="grid2"><div class="panel"><div class="panelhead"><h3>Top Event-IDs</h3><span>Events / 24h</span></div><div id="topCodes" class="barlist"></div></div><div class="panel"><div class="panelhead"><h3>Authentifizierung</h3><span>4624 / 4625 / 4740 / 4771 / 4776</span></div><div id="authBars" class="barlist"></div></div></div>
|
||
</section>
|
||
|
||
<section id="view-events" class="view">
|
||
<div class="sectionintro"><div><h3>Event Investigation</h3><p>Drill-down direkt auf die kanonischen ClickHouse-Events.</p></div><button class="btn primary" onclick="loadEvents()">Suchen</button></div>
|
||
<div class="panel"><div class="filters"><input class="field" id="eQuery" placeholder="Message / Command / Process"><input class="field" id="eHost" placeholder="Host"><input class="field" id="eUser" placeholder="User"><input class="field" id="eIP" placeholder="Source-IP"><input class="field" id="eCode" placeholder="Event-ID"><input class="field" id="eChannel" placeholder="Channel"><select class="field" id="eHours"><option value="1">1 Stunde</option><option value="24" selected>24 Stunden</option><option value="168">7 Tage</option><option value="720">30 Tage</option><option value="2160">90 Tage</option></select></div><div class="tablewrap"><table><thead><tr><th>Zeit</th><th>Host</th><th>Event</th><th>User</th><th>Source</th><th>Workstation</th><th>Prozess / Nachricht</th></tr></thead><tbody id="eventRows"></tbody></table></div></div>
|
||
</section>
|
||
|
||
<section id="view-detections" class="view">
|
||
<div class="sectionintro"><div><h3>Detections</h3><p>Ergebnisse der Rule-Engine mit Status, MITRE-Kontext und Evidence-Zeitfenster.</p></div></div>
|
||
<div class="panel"><div class="tablewrap"><table><thead><tr><th>Zuletzt</th><th>Severity</th><th>Rule</th><th>Entität</th><th>Zusammenfassung</th><th>MITRE</th><th>Status</th></tr></thead><tbody id="detectionRows"></tbody></table></div></div>
|
||
</section>
|
||
|
||
<section id="view-rules" class="view">
|
||
<div class="sectionintro"><div><h3>Rule-Sets</h3><p>Versionierte Built-ins plus eigene Regeln. Built-ins bleiben updatefähig; Aktivierungszustände werden in PostgreSQL erhalten.</p></div><button class="btn primary" onclick="openRuleModal()">+ Eigene Regel</button></div>
|
||
<div id="ruleSets" class="rulesets"></div>
|
||
<div class="panel"><div class="filters"><input class="field" id="ruleSearch" placeholder="Regel suchen" oninput="renderRules()"><select class="field" id="ruleSeverity" onchange="renderRules()"><option value="">Alle Severities</option><option>critical</option><option>high</option><option>medium</option><option>low</option><option>info</option></select></div><div class="tablewrap"><table><thead><tr><th>On</th><th>Severity</th><th>Rule</th><th>Set</th><th>Typ</th><th>Event-IDs</th><th>Fenster</th><th>MITRE</th></tr></thead><tbody id="ruleRows"></tbody></table></div></div>
|
||
</section>
|
||
|
||
<section id="view-suppressions" class="view">
|
||
<div class="sectionintro"><div><h3>Suppressions</h3><p>Gezielte Ausnahmen nach Rule, Host, Benutzer oder Source-IP. Wildcards mit <span class="mono">*</span>.</p></div><button class="btn primary" onclick="openSuppressionModal()">+ Suppression</button></div>
|
||
<div class="panel"><div class="tablewrap"><table><thead><tr><th>Rule</th><th>Host</th><th>User</th><th>Source-IP</th><th>Bis</th><th>Grund</th><th></th></tr></thead><tbody id="suppRows"></tbody></table></div></div>
|
||
</section>
|
||
|
||
<section id="view-agents" class="view">
|
||
<div class="sectionintro"><div><h3>Agents</h3><p>Enrollment, letzter Kontakt und Aktivierungsstatus.</p></div></div>
|
||
<div class="panel"><div class="tablewrap"><table><thead><tr><th>On</th><th>Host</th><th>IP</th><th>First seen</th><th>Last seen</th><th>Zustand</th></tr></thead><tbody id="agentRows"></tbody></table></div></div>
|
||
</section>
|
||
</div></main></div>
|
||
|
||
<div id="drawer" class="drawer"><div style="display:flex;justify-content:space-between"><h3>Event Details</h3><button class="btn" onclick="closeDrawer()">Schließen</button></div><div id="drawerBody"></div></div>
|
||
<div id="ruleModal" class="modalback"><div class="modal"><h3>Eigene Detection-Regel</h3><div class="formgrid">
|
||
<div><label>ID</label><input id="rID" placeholder="custom-my-rule"></div><div><label>Titel</label><input id="rTitle" placeholder="Meine Detection"></div>
|
||
<div><label>Severity</label><select id="rSeverity"><option>medium</option><option>high</option><option>critical</option><option>low</option><option>info</option></select></div><div><label>Score (0-100)</label><input id="rScore" type="number" min="0" max="100" value="60"></div>
|
||
<div><label>Typ</label><select id="rKind"><option value="event">event</option><option value="threshold">threshold</option><option value="distinct">distinct</option></select></div><div><label>MITRE (CSV)</label><input id="rMitre" placeholder="T1110,T1078"></div>
|
||
<div><label>Event-IDs (CSV)</label><input id="rCodes" placeholder="4625,4771"></div><div><label>Channels (CSV)</label><input id="rChannels" placeholder="Security"></div>
|
||
<div><label>Window Sekunden</label><input id="rWindow" type="number" value="300"></div><div><label>Threshold</label><input id="rThreshold" type="number" value="1"></div>
|
||
<div><label>Group by (CSV)</label><input id="rGroup" value="host" placeholder="host,user,source_ip"></div><div><label>Suppress Sekunden</label><input id="rSuppress" type="number" value="900"></div>
|
||
<div><label>Condition Field</label><input id="rCondField" placeholder="message"></div><div><label>Operator</label><select id="rCondOp"><option value="contains">contains</option><option value="equals">equals</option><option value="not_equals">not_equals</option><option value="regex">regex</option><option value="exists">exists</option><option value="not_exists">not_exists</option><option value="in">in</option></select></div>
|
||
<div class="wide"><label>Condition Value</label><input id="rCondValue" placeholder="optional"></div>
|
||
<div><label>Distinct Field (nur distinct)</label><input id="rDistinctField" placeholder="target_user"></div><div><label>Distinct Threshold</label><input id="rDistinctThreshold" type="number" value="10"></div>
|
||
<div class="wide"><label>Summary Template</label><input id="rSummary" value="Regel auf {host} ausgelöst ({count} Events)" placeholder="{host} {user} {source_ip} {count} {distinct}"></div>
|
||
<div class="wide"><label>Tags (CSV)</label><input id="rTags" placeholder="authentication,windows,custom"></div>
|
||
<div class="wide"><label>Beschreibung</label><textarea id="rDesc"></textarea></div>
|
||
</div><div class="modalactions"><button class="btn" onclick="closeModal('ruleModal')">Abbrechen</button><button class="btn primary" onclick="saveRule()">Speichern</button></div></div></div>
|
||
<div id="suppModal" class="modalback"><div class="modal"><h3>Suppression erstellen</h3><div class="formgrid"><div><label>Rule-ID</label><input id="sRule" placeholder="leer = alle"></div><div><label>Host Pattern</label><input id="sHost" placeholder="CLIENT-*"></div><div><label>User Pattern</label><input id="sUser" placeholder="svc_*"></div><div><label>Source-IP Pattern</label><input id="sIP" placeholder="10.10.*"></div><div><label>Ablauf</label><input id="sExpires" type="datetime-local"></div><div><label>Grund</label><input id="sReason" placeholder="Wartungsfenster"></div></div><div class="modalactions"><button class="btn" onclick="closeModal('suppModal')">Abbrechen</button><button class="btn primary" onclick="saveSuppression()">Speichern</button></div></div></div>
|
||
|
||
<script>
|
||
const $=id=>document.getElementById(id), esc=s=>String(s??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]));
|
||
let rulesCache=[], currentView='dashboard';
|
||
const titles={dashboard:['Security Overview','Ereignisse, Detections und Pipeline-Zustand'],events:['Event Investigation','Suche und Drill-down'],detections:['Detections','Priorisieren, untersuchen und schließen'],rules:['Rule-Sets','Detection Content verwalten'],suppressions:['Suppressions','False Positives kontrolliert ausnehmen'],agents:['Agents','Collector-Zustand und Enrollment']};
|
||
const explicitGrafana='{{.GrafanaURL}}', grafanaPort='{{.GrafanaPort}}'; $('grafanaLink').href=explicitGrafana||('http://'+location.hostname+':'+grafanaPort+'/');
|
||
async function api(url,opt){const r=await fetch(url,opt);let d={};try{d=await r.json()}catch{}if(!r.ok)throw new Error(d.error||r.statusText);return d}
|
||
function notice(msg,type='alert'){ $('notice').innerHTML=`<div class="${type}">${esc(msg)}</div>`;setTimeout(()=>{if($('notice').textContent===msg)$('notice').innerHTML=''},5000)}
|
||
function fmt(n){return Number(n||0).toLocaleString('de-DE')} function dt(s){if(!s)return '';const d=new Date(s);return isNaN(d)?esc(s):d.toLocaleString('de-DE')}
|
||
function sev(s){return `<span class="sev sev-${esc(s)}">${esc(s)}</span>`}
|
||
function switcher(on,call){return `<label class="switch"><input type="checkbox" ${on?'checked':''} onchange="${call}"><span></span></label>`}
|
||
function setView(v){currentView=v;document.querySelectorAll('.view').forEach(x=>x.classList.remove('active'));$('view-'+v).classList.add('active');document.querySelectorAll('.nav button').forEach(x=>x.classList.toggle('active',x.dataset.view===v));$('viewTitle').textContent=titles[v][0];$('viewSub').textContent=titles[v][1];refreshCurrent()}
|
||
document.querySelectorAll('.nav button').forEach(b=>b.onclick=()=>setView(b.dataset.view));
|
||
function refreshCurrent(){({dashboard:loadDashboard,events:loadEvents,detections:loadDetections,rules:loadRules,suppressions:loadSuppressions,agents:loadAgents}[currentView]||(()=>{}))()}
|
||
async function loadDashboard(){try{const [s,a]=await Promise.all([api('/api/summary'),api('/api/analytics?hours=24')]);$('kEvents').textContent=fmt(s.events_24h);$('kHosts').textContent=fmt(s.active_hosts);$('kHigh').textContent=fmt((s.detections?.high||0)+(s.detections?.critical||0));$('kRules').textContent=fmt(s.rules_enabled);$('kRuleSets').textContent=fmt(s.rule_sets_enabled)+' Rule-Sets';$('kPipeline').textContent='OK';drawSpark(a.series||[]);bars('topHosts',a.top_hosts||[],'host_name','events');bars('topCodes',a.top_event_codes||[],'event_code','events',x=>'Event '+x);bars('authBars',a.authentication||[],'event_code','events',authName)}catch(e){$('kPipeline').textContent='FEHLER';$('kPipeline').style.color='var(--red)';notice(e.message)}}
|
||
function authName(x){return ({4624:'4624 Successful Logon',4625:'4625 Failed Logon',4740:'4740 Lockout',4771:'4771 Kerberos Failure',4776:'4776 NTLM'}[x]||('Event '+x))}
|
||
function bars(id,rows,key,val,labelFn){const max=Math.max(1,...rows.map(x=>Number(x[val]||0)));$(id).innerHTML=rows.length?rows.map(x=>`<div class="barrow"><div class="barlabel">${esc(labelFn?labelFn(x[key]):x[key])}</div><div class="bartrack"><div class="barfill" style="width:${Math.max(2,Number(x[val]||0)/max*100)}%"></div></div><div class="barvalue">${fmt(x[val])}</div></div>`).join(''):'<div class="empty">Keine Daten</div>'}
|
||
function drawSpark(rows){const svg=$('eventSpark');if(!rows.length){svg.innerHTML='<text x="20" y="100" fill="#91a2bd">Keine Daten</text>';return}const vals=rows.map(x=>Number(x.events||0)),max=Math.max(1,...vals),w=900,h=190;const pts=vals.map((v,i)=>`${i/(Math.max(1,vals.length-1))*w},${h-v/max*(h-20)}`).join(' ');svg.innerHTML=`<defs><linearGradient id="g" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="#5b8cff" stop-opacity=".38"/><stop offset="1" stop-color="#5b8cff" stop-opacity="0"/></linearGradient></defs><polygon points="0,${h} ${pts} ${w},${h}" fill="url(#g)"/><polyline points="${pts}" fill="none" stroke="#7aa2ff" stroke-width="3" vector-effect="non-scaling-stroke"/>`}
|
||
async function loadEvents(){try{const q=new URLSearchParams({hours:$('eHours').value,limit:'500'});[['query','eQuery'],['host','eHost'],['user','eUser'],['ip','eIP'],['event_code','eCode'],['channel','eChannel']].forEach(([k,id])=>{const v=$(id).value.trim();if(v)q.set(k,v)});const d=await api('/api/events?'+q);window._events=d;$('eventRows').innerHTML=d.length?d.map((x,i)=>`<tr class="clickable" onclick="showEvent(${i})"><td>${dt(x.event_time)}</td><td><b>${esc(x.host_name)}</b><br><span class="muted">${esc(x.provider)}</span></td><td><b>${esc(x.event_code)}</b><br><span class="pill">${esc(x.channel)}</span></td><td>${esc(x.target_user||x.user_name||x.subject_user)}</td><td>${esc(x.source_ip)}</td><td>${esc(x.workstation)}</td><td>${esc((x.process_path||x.command_line||x.message||'').slice(0,180))}</td></tr>`).join(''):'<tr><td colspan="7" class="empty">Keine Events im gewählten Zeitraum</td></tr>'}catch(e){notice(e.message)}}
|
||
function showEvent(i){const x=window._events[i]||{};const attrs=typeof x.attributes==='object'?JSON.stringify(x.attributes,null,2):String(x.attributes||'');$('drawerBody').innerHTML=`<div class="drawergrid">${[['Zeit',dt(x.event_time)],['Host',x.host_name],['Provider',x.provider],['Channel',x.channel],['Event-ID',x.event_code],['Action',(x.category||'')+' / '+(x.action||'')+' / '+(x.outcome||'')],['User',x.target_user||x.user_name||x.subject_user],['Source-IP',x.source_ip],['Destination-IP',x.destination_ip],['Workstation',x.workstation],['Logon Type',x.logon_type],['Auth Package',x.authentication_package],['Status',x.status_code],['Failure Reason',x.failure_reason],['Process',x.process_path],['Parent',x.parent_process_path],['Raw Object',x.raw_object_key]].map(([k,v])=>`<div class="k">${k}</div><div>${esc(v)}</div>`).join('')}</div><h4>Command line</h4><pre>${esc(x.command_line)}</pre><h4>Message</h4><pre>${esc(x.message)}</pre><h4>Restattribute</h4><pre>${esc(attrs)}</pre>`;$('drawer').classList.add('open')}
|
||
function closeDrawer(){$('drawer').classList.remove('open')}
|
||
async function loadDetections(){try{const d=await api('/api/detections?limit=700');$('detectionRows').innerHTML=d.length?d.map(x=>`<tr><td>${dt(x.last_seen)}<br><span class="muted">${fmt(x.count)} Treffer</span></td><td>${sev(x.severity)}</td><td><b>${esc(x.rule_name)}</b><br><span class="mono muted">${esc(x.rule_id)}</span></td><td>${esc(x.hostname)}<br>${esc(x.user_name)}<br><span class="muted">${esc(x.source_ip||x.workstation)}</span></td><td>${esc(x.summary)}</td><td>${(x.mitre||[]).map(t=>`<span class="tag">${esc(t)}</span>`).join('')}</td><td><select class="field status-${esc(x.status)}" onchange="setDetectionStatus(${Number(x.id)},this.value)">${['open','investigating','closed','false_positive'].map(s=>`<option ${s===x.status?'selected':''}>${s}</option>`).join('')}</select></td></tr>`).join(''):'<tr><td colspan="7" class="empty">Keine Detections</td></tr>'}catch(e){notice(e.message)}}
|
||
async function setDetectionStatus(id,status){try{await api('/api/detections/status',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,status})});loadDashboard()}catch(e){notice(e.message)}}
|
||
async function loadRules(){try{const [sets,rules]=await Promise.all([api('/api/rule-sets'),api('/api/rules')]);rulesCache=rules;$('ruleSets').innerHTML=sets.map(x=>`<div class="ruleset"><h4>${esc(x.name)}</h4><div class="muted mono">${esc(x.id)} · v${esc(x.version)}</div><p>${esc(x.description)}</p><div class="rulesetfoot"><span>${fmt(x.enabled_rules)}/${fmt(x.rule_count)} Regeln</span>${switcher(x.enabled,`toggleRuleSet('${js(x.id)}',this.checked)`)}</div></div>`).join('');renderRules()}catch(e){notice(e.message)}}
|
||
function renderRules(){const q=$('ruleSearch').value.toLowerCase(),sv=$('ruleSeverity').value;const r=rulesCache.filter(x=>(!q||(x.title+' '+x.id+' '+x.rule_set_name).toLowerCase().includes(q))&&(!sv||x.severity===sv));$('ruleRows').innerHTML=r.length?r.map(x=>`<tr><td>${switcher(x.enabled,`toggleRule('${js(x.id)}',this.checked)`)}</td><td>${sev(x.severity)}</td><td><b>${esc(x.title)}</b><br><span class="mono muted">${esc(x.id)}</span><br><span class="muted">${esc(x.description)}</span></td><td>${esc(x.rule_set_name)} ${x.source==='custom'?'<span class="tag">custom</span>':''}</td><td><span class="pill">${esc(x.kind)}</span></td><td>${(x.event_codes||[]).map(v=>`<span class="tag">${esc(v)}</span>`).join('')}</td><td>${fmt(x.window_seconds)}s / ≥${fmt(x.threshold||1)}</td><td>${(x.mitre||[]).map(v=>`<span class="tag">${esc(v)}</span>`).join('')}</td></tr>`).join(''):'<tr><td colspan="8" class="empty">Keine Regeln</td></tr>'}
|
||
async function toggleRule(id,enabled){try{await api('/api/rules/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,enabled})});await loadRules()}catch(e){notice(e.message)}}
|
||
async function toggleRuleSet(id,enabled){try{await api('/api/rule-sets/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,enabled})});await loadRules()}catch(e){notice(e.message)}}
|
||
function openRuleModal(){$('ruleModal').classList.add('open')} function openSuppressionModal(){$('suppModal').classList.add('open')} function closeModal(id){$(id).classList.remove('open')}
|
||
function csv(v){return v.split(',').map(x=>x.trim()).filter(Boolean)} function js(v){return String(v).replace(/\\/g,'\\\\').replace(/'/g,"\\'")}
|
||
async function saveRule(){try{const cond=$('rCondField').value.trim()? [{field:$('rCondField').value.trim(),operator:$('rCondOp').value,value:$('rCondValue').value}]:[];const data={id:$('rID').value.trim(),title:$('rTitle').value.trim(),description:$('rDesc').value.trim(),severity:$('rSeverity').value,score:Number($('rScore').value||0),enabled:true,kind:$('rKind').value,channels:csv($('rChannels').value),event_codes:csv($('rCodes').value).map(Number).filter(Number.isFinite),conditions:cond,group_by:csv($('rGroup').value),threshold:Number($('rThreshold').value||1),distinct_field:$('rDistinctField').value.trim(),distinct_threshold:Number($('rDistinctThreshold').value||0),window_seconds:Number($('rWindow').value||300),suppress_seconds:Number($('rSuppress').value||0),summary:$('rSummary').value.trim(),tags:csv($('rTags').value).length?csv($('rTags').value):['custom'],mitre:csv($('rMitre').value)};await api('/api/rules/save',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});closeModal('ruleModal');notice('Regel gespeichert','success');loadRules()}catch(e){notice(e.message)}}
|
||
async function loadSuppressions(){try{const d=await api('/api/suppressions');$('suppRows').innerHTML=d.length?d.map(x=>`<tr><td class="mono">${esc(x.rule_id||'*')}</td><td>${esc(x.host_pattern||'*')}</td><td>${esc(x.user_pattern||'*')}</td><td>${esc(x.source_ip_pattern||'*')}</td><td>${x.expires_at?dt(x.expires_at):'unbegrenzt'}</td><td>${esc(x.reason)}</td><td><button class="btn danger" onclick="deleteSuppression(${Number(x.id)})">Löschen</button></td></tr>`).join(''):'<tr><td colspan="7" class="empty">Keine Suppressions</td></tr>'}catch(e){notice(e.message)}}
|
||
async function saveSuppression(){try{const exp=$('sExpires').value?new Date($('sExpires').value).toISOString():null;await api('/api/suppressions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({rule_id:$('sRule').value.trim(),host_pattern:$('sHost').value.trim(),user_pattern:$('sUser').value.trim(),source_ip_pattern:$('sIP').value.trim(),reason:$('sReason').value.trim(),enabled:true,expires_at:exp})});closeModal('suppModal');notice('Suppression erstellt','success');loadSuppressions()}catch(e){notice(e.message)}}
|
||
async function deleteSuppression(id){if(!confirm('Suppression wirklich löschen?'))return;try{await api('/api/suppressions/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})});loadSuppressions()}catch(e){notice(e.message)}}
|
||
async function loadAgents(){try{const d=await api('/api/agents');const now=Date.now();$('agentRows').innerHTML=d.length?d.map(x=>{const online=x.enabled&&(now-new Date(x.last_seen).getTime()<15*60e3);return `<tr><td>${switcher(x.enabled,`toggleAgent('${js(x.id)}',this.checked)`)}</td><td><b>${esc(x.hostname)}</b><br><span class="mono muted">${esc(x.id)}</span></td><td>${esc(x.last_ip)}</td><td>${dt(x.first_seen)}</td><td>${dt(x.last_seen)}</td><td><span class="pill"><span class="dot" style="background:${online?'var(--green)':'var(--red)'}"></span>${online?'online':'offline'}</span></td></tr>`}).join(''):'<tr><td colspan="6" class="empty">Keine Agents</td></tr>'}catch(e){notice(e.message)}}
|
||
async function toggleAgent(id,enabled){try{await api('/api/agents/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,enabled})});loadAgents()}catch(e){notice(e.message)}}
|
||
loadDashboard();setInterval(()=>{if(currentView==='dashboard')loadDashboard()},30000);
|
||
</script>
|
||
</body></html>
|