mirror of
https://github.com/marcredhat/SIEM-toolkit-patched
synced 2026-06-10 21:31:19 +00:00
Add health score, coverage trends, dependency map, PowerQuery playground, onboarding tracker
Tenant Health Score: - CoverageSnapshot table stores daily health metrics (parser %, MITRE %, firing %) - _compute_health() weighted formula: 40% parser coverage + 35% MITRE + 25% firing (reweighted 55/45 when firing cache empty) - GET /api/coverage/health returns score + delta vs previous snapshot - GET /api/coverage/snapshots returns chronological history for sparklines - POST /api/coverage/snapshot for manual recording - Auto-snapshot recorded at end of every sync-sources call - Overview dashboard: prominent health score card with color coding, component breakdown, delta indicator, and inline SVG sparkline (last 30 points) Rule Dependency Map: - GET /api/coverage/dependency-map flips the coverage map — rule → required sources - Each source flagged healthy/inactive/no_parser; at_risk = any source missing - New section on Threat Coverage tab with at-risk filter toggle PowerQuery Playground: - New query.py router: GET /presets (7 curated queries) + POST /run - New Query nav tab with time-range pills, preset buttons, localStorage history, monospace textarea, auto-column results table, client-side CSV export Onboarding Tracker: - GET /api/coverage/onboarding-status returns per-source pipeline progress across 6 stages: Data Received → Parser File → Parser Active → Source Labeled → Detection Rules → Rules Firing - New section on Onboarding tab with emoji stage dots, progress bars, collapsed completed sources with show/hide toggle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime, timedelta
|
||||
from services import s1_client
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _date_range(hours: int | None = None, days: int | None = None) -> tuple[str, str]:
|
||||
now = datetime.utcnow()
|
||||
if hours:
|
||||
delta = timedelta(hours=hours)
|
||||
else:
|
||||
delta = timedelta(days=days or 1)
|
||||
return (
|
||||
(now - delta).strftime("%Y-%m-%dT%H:%M:%S.000Z"),
|
||||
now.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
|
||||
)
|
||||
|
||||
|
||||
PRESET_QUERIES = [
|
||||
{"label": "Top sources by volume", "query": "| group events=count() by dataSource.name | sort -events | limit 25"},
|
||||
{"label": "Unlabelled events", "query": "!(dataSource.name = *) !(source = 'scalyr') | group events=count() by source | sort -events | limit 25"},
|
||||
{"label": "Events by type", "query": "| group events=count() by dataSource.name, event.type | sort -events | limit 50"},
|
||||
{"label": "Failed logins", "query": "| filter event.type = 'Logon' | filter event.outcome = 'FAILED' | group count() by user.name, src.ip | sort -count() | limit 25"},
|
||||
{"label": "Process executions", "query": "| filter event.type = 'Process Creation' | group count() by src.process.name | sort -count() | limit 25"},
|
||||
{"label": "Network connections by dest", "query": "| filter event.type = 'IP Connect' | group count() by dst.ip | sort -count() | limit 25"},
|
||||
{"label": "Rules firing (30d)", "query": "| filter ruleName != '' | group alerts=count() by ruleName | sort -alerts | limit 50"},
|
||||
]
|
||||
|
||||
|
||||
class QueryRequest(BaseModel):
|
||||
query: str
|
||||
hours: int | None = None
|
||||
days: int | None = None
|
||||
max_count: int = 1000
|
||||
|
||||
|
||||
@router.get("/presets")
|
||||
def get_presets():
|
||||
return {"presets": PRESET_QUERIES}
|
||||
|
||||
|
||||
@router.post("/run")
|
||||
async def run_query(req: QueryRequest):
|
||||
"""Run a PowerQuery against the Singularity Data Lake."""
|
||||
if not req.query.strip():
|
||||
raise HTTPException(400, "Query cannot be empty")
|
||||
if req.max_count > 10_000:
|
||||
req.max_count = 10_000
|
||||
|
||||
from_dt, to_dt = _date_range(hours=req.hours, days=req.days)
|
||||
|
||||
try:
|
||||
result = await s1_client.run_powerquery(req.query, from_dt, to_dt, max_count=req.max_count)
|
||||
except Exception as e:
|
||||
raise HTTPException(502, f"PowerQuery error: {e}")
|
||||
|
||||
err = result.get("error") if isinstance(result, dict) else None
|
||||
if err:
|
||||
raise HTTPException(502, f"PowerQuery error: {err}")
|
||||
|
||||
events = result.get("events", [])
|
||||
columns = sorted({k for row in events for k in row.keys()}) if events else []
|
||||
|
||||
return {
|
||||
"rows": len(events),
|
||||
"columns": columns,
|
||||
"events": events,
|
||||
"from": from_dt,
|
||||
"to": to_dt,
|
||||
"query": req.query,
|
||||
}
|
||||
Reference in New Issue
Block a user