Files
siem-backend/web/index.html
jbergner e9d9583f28
Some checks failed
release-tag / release-image (push) Failing after 1m8s
Funktionsrollback
2026-07-24 07:17:38 +02:00

120 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[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>