diff --git a/app/db/conn.py b/app/db/conn.py index 51f62ed..30e0e8b 100644 --- a/app/db/conn.py +++ b/app/db/conn.py @@ -117,10 +117,11 @@ def get_conn(db_path: Path | None = None) -> Generator[DbConn, None, None]: else: if db_path is None: raise ValueError("db_path is required for SQLite backend") - raw = sqlite3.connect(str(db_path), timeout=30.0) + raw = sqlite3.connect(str(db_path), timeout=90.0) raw.row_factory = sqlite3.Row try: raw.execute("PRAGMA journal_mode=WAL") + raw.execute("PRAGMA busy_timeout=90000") raw.execute("PRAGMA foreign_keys=ON") yield DbConn(raw, BACKEND) finally: diff --git a/web/src/views/DashboardView.vue b/web/src/views/DashboardView.vue index 3d6a73a..3f3175f 100644 --- a/web/src/views/DashboardView.vue +++ b/web/src/views/DashboardView.vue @@ -259,7 +259,7 @@ async function loadWatchStatus() { async function loadAlertCount() { try { - const res = await fetch(`${BASE}/turnstone/api/anomaly/detections?unacked_only=true&limit=1000`) + const res = await fetch(`${BASE}/api/anomaly/detections?unacked_only=true&limit=1000`) if (res.ok) unackedAlerts.value = (await res.json()).total ?? 0 } catch { /* non-critical — scorer may be disabled */ } finally { alertsLoading.value = false } diff --git a/web/src/views/SecurityAlertsView.vue b/web/src/views/SecurityAlertsView.vue index 5f71189..46cf19b 100644 --- a/web/src/views/SecurityAlertsView.vue +++ b/web/src/views/SecurityAlertsView.vue @@ -35,12 +35,14 @@ :class="[ 'text-xs px-2 py-1 rounded border font-mono', cybersecStatus.enabled - ? 'border-surface-border text-text-dim' + ? cybersecStatus.running + ? 'border-accent text-accent animate-pulse' + : 'border-surface-border text-text-dim' : 'border-surface-border text-text-dim opacity-40' ]" :title="cybersecStatus.enabled ? `cybersec: ${cybersecStatus.model}` : 'TURNSTONE_CYBERSEC_MODEL not set'" > - {{ cybersecStatus.enabled ? 'cybersec on' : 'cybersec off' }} + {{ cybersecStatus.running ? 'cybersec scoring…' : cybersecStatus.enabled ? 'cybersec on' : 'cybersec off' }} + + @@ -64,15 +75,25 @@ -
- Total scored: {{ scorerStatus.total_scored ?? '—' }} - Total detections: {{ scorerStatus.total_detections ?? '—' }} - - Last run: {{ formatTs(scorerStatus.last_run_at) }} - - - Last error: {{ scorerStatus.last_error }} - +
+ +
@@ -162,7 +183,7 @@ - - {{ det.anomaly_label }} - +
+ + {{ det.anomaly_label }} + + cybersec +
@@ -302,6 +329,7 @@ interface Detection { acknowledged: number | boolean acknowledged_at: string | null notes: string + scorer: string } interface ScorerStatus { @@ -318,14 +346,30 @@ interface ScorerStatus { total_detections: number } +interface CybersecStatus { + enabled: boolean + running: boolean + model: string | null + threshold: number + device: string + last_run_at: string | null + last_duration_s: number | null + last_scored: number + last_detections: number + last_error: string | null + total_scored: number + total_detections: number +} + // ── State ──────────────────────────────────────────────────────────────────── -const detections = ref([]) -const scorerStatus = ref(null) -const cybersecStatus = ref | null>(null) -const loading = ref(true) -const triggerLoading = ref(false) -const ackLoading = ref(false) +const detections = ref([]) +const scorerStatus = ref(null) +const cybersecStatus = ref(null) +const loading = ref(true) +const triggerLoading = ref(false) +const cybersecTriggerLoading = ref(false) +const ackLoading = ref(false) const ackError = ref(null) const ackNotes = ref('') const drawer = ref(null) @@ -371,7 +415,7 @@ async function loadDetections() { if (labelFilter.value) params.set('label', labelFilter.value) if (scorerFilter.value) params.set('scorer', scorerFilter.value) try { - const res = await fetch(`${BASE}/turnstone/api/anomaly/detections?${params}`) + const res = await fetch(`${BASE}/api/anomaly/detections?${params}`) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() detections.value = (data.detections ?? []).map((d: Detection) => ({ @@ -388,12 +432,11 @@ async function loadDetections() { async function loadScorerStatus() { try { const [anomalyRes, cybersecRes] = await Promise.all([ - fetch(`${BASE}/turnstone/api/anomaly/status`), - fetch(`${BASE}/turnstone/api/cybersec/status`), + fetch(`${BASE}/api/anomaly/status`), + fetch(`${BASE}/api/cybersec/status`), ]) if (anomalyRes.ok) { - const data = await anomalyRes.json() - scorerStatus.value = { ...data.state, ...data.config } + scorerStatus.value = await anomalyRes.json() } if (cybersecRes.ok) { const data = await cybersecRes.json() @@ -414,14 +457,23 @@ onMounted(() => { async function runScorer() { triggerLoading.value = true try { - await fetch(`${BASE}/turnstone/api/anomaly/run`, { method: 'POST' }) - // reload status after a short delay so the running flag has time to flip + await fetch(`${BASE}/api/anomaly/run`, { method: 'POST' }) setTimeout(() => { loadScorerStatus(); loadDetections() }, 2000) } finally { triggerLoading.value = false } } +async function runCybersec() { + cybersecTriggerLoading.value = true + try { + await fetch(`${BASE}/api/cybersec/run`, { method: 'POST' }) + setTimeout(() => { loadScorerStatus(); loadDetections() }, 2000) + } finally { + cybersecTriggerLoading.value = false + } +} + function openDrawer(det: Detection) { ackNotes.value = det.notes ?? '' ackError.value = null @@ -435,7 +487,7 @@ async function acknowledge(det: Detection) { const params = new URLSearchParams() if (ackNotes.value.trim()) params.set('notes', ackNotes.value.trim()) const res = await fetch( - `${BASE}/turnstone/api/anomaly/detections/${det.id}/acknowledge?${params}`, + `${BASE}/api/anomaly/detections/${det.id}/acknowledge?${params}`, { method: 'POST' } ) if (!res.ok) throw new Error(`HTTP ${res.status}`)