---
module: aria.dashboard
title: Threat Dashboard
tab: Claude
order: 40
audience: dev
---

Terse act-without-guessing reference. Owner service: **`hiveops-aria`** (port **8095**, DB **`hiveiq_aria`**). Frontend: `hiveops-aria/frontend/src/components/Dashboard.svelte`.

## Endpoints (all this view uses)

| Method | Path | Auth | Notes |
|--------|------|------|-------|
| GET | `/api/aria/stats` | `hasRole('BCOS_ADMIN')` | THE only data call. Returns 7 counts. |
| GET | `/auth/api/users/me` | authenticated | header name/role only; not aria; via gateway URL |

Response keys of `/api/aria/stats`: `totalEvents`, `criticalCount`, `highCount`, `activeIocCount`, `activeSequenceCount`, `lowPostureCount`, `eolDeviceCount`.

Card-click navigation is client-side only (no endpoints): Critical→events(sev=CRITICAL), High→events(sev=HIGH), Active Sequences→sequences, Active IOCs→ioc, Low Posture/EOL→posture.

## Count sources (exact)

| Key | Query | Table | Window |
|-----|-------|-------|--------|
| totalEvents | `count()` | `threat_event` | all-time |
| criticalCount | `countBySeverityAndDetectedAtAfter(CRITICAL, now-24h)` | `threat_event` | **24h** |
| highCount | `countBySeverityAndDetectedAtAfter(HIGH, now-24h)` | `threat_event` | **24h** |
| activeIocCount | `findByActiveTrue().size()` | `threat_ioc` | current |
| activeSequenceCount | `countByResolvedAtIsNull()` | `precursor_sequence_alert` | current |
| lowPostureCount | `countByPostureScoreLessThan(50)` | `device_security_posture` | current |
| eolDeviceCount | `countByOsEolStatus(EOL)` | `device_security_posture` | current |

## Tables (owned by hiveops-aria, schema `hiveiq_aria`)

- `threat_event` · `threat_ioc` · `precursor_sequence_alert` · `device_security_posture`
- Dashboard = **read-only**. Zero writes from this view.

## Kafka

- Topics produced by aria (NOT by the dashboard): `hiveops.aria.threats`, `hiveops.aria.sequences`.
- **No `@KafkaListener` in hiveops-aria.** No consumer. Dashboard reads Postgres directly.

## Auth facts

- JWT Bearer, header `Authorization: Bearer <jwt>`. Single `role` claim → `ROLE_<role>`.
- `/stats` requires role **exactly `BCOS_ADMIN`**. `MSP_ADMIN` → 403.
- `permitAll` in SecurityConfig: `/api/aria/ingest/**`, `/api/aria/posture/report`, actuator health/info. `/stats` is NOT public.

## Gotchas

- Critical/High cards are **24h counts**, Total is all-time → "Non-Critical Events" (`total − crit24h − high24h`, computed in browser) mixes windows. Don't treat it as a clean category.
- Dashboard loads once on mount — **no auto-poll, no refresh button**. Stale = reload.
- Total Events + Non-Critical Events cards are non-clickable by design.
- `activeIocCount` loads full active-IOC list then `.size()` (not a count query).
- `frontend/src/lib/api.ts` dev fallback `apiUrl` is `http://localhost:8017`; real port is 8095 via injected `window.__APP_CONFIG__.apiUrl`. Don't hardcode 8017.

## Do NOT

- Do NOT assume `MSP_ADMIN` can load this — it's `BCOS_ADMIN` only.
- Do NOT expect Kafka to drive the dashboard — it's a synchronous DB read.
- Do NOT add write/mutation logic here — mutations live in IOC Management / Precursor Alerts views.
- Do NOT read `criticalCount`/`highCount` as all-time totals — they are 24h.
- Do NOT invent per-card endpoints — only `/api/aria/stats` backs this page.
- Do NOT route stats through incident/journal/devices — hiveops-aria owns all of it.

Related: [aria.dashboard.internal], [aria.dashboard.architect], [technical.aria].
