Files
Mick d0299e0f23 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>
2026-05-22 11:09:43 -04:00

74 lines
2.7 KiB
Python

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,
}