---
module: aria.dashboard
title: Threat Dashboard
tab: Internal
order: 20
audience: internal
---

Ops/support view for the ARIA Threat Dashboard — the landing screen of the ARIA (`hiveops-aria`) SPA. The whole page is driven by **one call**: `GET /api/aria/stats`. If a card is wrong or blank, that endpoint (or its underlying tables) is where to look — there is no per-card fetch.

## Who can see it

- **Role required: `BCOS_ADMIN` only.** `/api/aria/stats` (and `/api/aria/events`) is gated by `@PreAuthorize("hasRole('BCOS_ADMIN')")`.
- This is **not** the usual `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` pattern. An `MSP_ADMIN` JWT gets **403** and the dashboard shows the "Could not load ARIA stats" toast with empty cards.
- Auth is a stateless JWT Bearer token. The filter reads the single `role` claim and maps it to `ROLE_<role>`, so the token's `role` must be exactly `BCOS_ADMIN`.
- The sidebar/header also calls `GET /auth/api/users/me` (via the gateway URL) purely to show the user's name/role — a failure there is silent and does not block the dashboard.

## What each card actually counts

| Card | Source (repo query) | Window |
|------|--------------------|--------|
| Total Events | `threat_event` row count | **all-time** |
| Critical | `threat_event` where severity=CRITICAL | **last 24h only** |
| High | `threat_event` where severity=HIGH | **last 24h only** |
| Active Sequences | `precursor_sequence_alert` where `resolved_at IS NULL` | current |
| Active IOCs | `threat_ioc` where `active = true` | current |
| Low Posture | `device_security_posture` where `posture_score < 50` | current |
| EOL Devices | `device_security_posture` where `os_eol_status = EOL` | current |
| Non-Critical Events | `Total − Critical − High` (computed in the browser) | mixed |

> **Gotcha — Critical/High are 24-hour rolling counts, not all-time.** The card label just says "Critical"/"High" (matching the customer Overview), but the backend uses `countBySeverityAndDetectedAtAfter(..., now-24h)`. "Non-Critical Events" subtracts 24h counts from an all-time total, so it is effectively "all events minus the last 24h of critical/high" — not a clean category. If someone asks "why does Critical show 0 when there are old critical events?", this is why.

## First things to check when it misbehaves

1. **All cards blank + "Load Failed" toast** → the `/api/aria/stats` call failed. Almost always a **403 (wrong role — not BCOS_ADMIN)** or a **401 (missing/expired JWT)**. Check the browser network tab for the status on `/api/aria/stats`.
2. **Page loads but numbers are all 0** → the tables are genuinely empty. ARIA data arrives only via event ingestion (`POST /api/aria/ingest/**`) and posture reports (`POST /api/aria/posture/report`). No ingestion = no events, IOCs still seeded from Flyway.
3. **Critical/High are 0 but you expect events** → confirm the events are within the last 24h (`detected_at`). Older events count toward Total but not Critical/High.
4. **Active IOCs unexpectedly low/zero** → `threat_ioc` seed (FBI FLASH, Flyway V2) may not have run, or IOCs were deactivated. Check `active = true` rows.
5. **502/timeout on the SPA** → `hiveops-aria` service (port 8095) is down or the gateway/nginx route to it is stale. Verify `curl localhost:8095/actuator/health`.
6. **A card doesn't navigate** → Total Events and Non-Critical Events are **intentionally not clickable**; the rest dispatch client-side navigation to Events/Sequences/IOC/Posture views (no extra API calls on click).

## Admin-only actions reachable from here

The dashboard itself is **read-only** (no buttons, no refresh trigger — it loads once on mount). The clickable cards jump to admin views that do have actions:

- **Active IOCs → IOC Management** — create/update/deactivate IOCs (`POST/PUT/DELETE /api/aria/ioc`).
- **Active Sequences → Precursor Alerts** — resolve a sequence (`POST /api/aria/sequences/{id}/resolve`).
- **Low Posture / EOL → Device Posture** — read-only posture inspection.

## Common failure modes → fix

| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| 403 on `/api/aria/stats` | JWT role ≠ BCOS_ADMIN | Re-issue token with `role: BCOS_ADMIN` |
| 401 on `/api/aria/stats` | Expired/absent Bearer token | Re-login to the SPA |
| All zeros | No ingested events/postures yet | Confirm agents are posting to `/api/aria/ingest/**` |
| Stats slow | `activeIocCount` loads all active IOC rows into memory to size the list | Cosmetic; only bites with very large IOC tables |
| Card counts stale | Dashboard loads once per page open — no auto-poll | Reload the page |

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