---
module: aria.dashboard
title: Threat Dashboard
tab: Architect
order: 30
audience: dev
---

How the ARIA Threat Dashboard is built. The dashboard is a **thin read-only aggregation view** — it does not own data, run detection, or consume Kafka. It renders one snapshot from `hiveops-aria`'s tables. All the interesting machinery (ingestion, IOC matching, sequence detection) runs upstream and only *populates* the tables this view reads.

## Services involved

- **`hiveops-aria`** (Spring Boot, `spring.application.name=hiveops-aria`, port **8095**, DB `hiveiq_aria`) — owns everything on this screen. Serves the stats, owns all four tables. See [platform.service-ownership].
- **`hiveops-auth`** — only for the header's `GET /auth/api/users/me` name/role lookup; not part of the stats path.
- No other service participates in the *dashboard read path*. (Ingestion into the tables comes from agents via the ARIA ingest endpoints; auto-response — disabled by default — would reach out to `hiveops-incident`.)

## Data flow for THIS feature

```
Browser (Dashboard.svelte, onMount)
  → GET /api/aria/stats            [JWT Bearer, ROLE_BCOS_ADMIN]
    → ThreatEventController.getStats()
       ├─ eventRepository.count()                                   → threat_event
       ├─ eventRepository.countBySeverityAndDetectedAtAfter(CRIT,-24h) → threat_event
       ├─ eventRepository.countBySeverityAndDetectedAtAfter(HIGH,-24h) → threat_event
       ├─ iocRepository.findByActiveTrue().size()                  → threat_ioc
       ├─ sequenceAlertRepository.countByResolvedAtIsNull()        → precursor_sequence_alert
       ├─ postureRepository.countByPostureScoreLessThan(50)        → device_security_posture
       └─ postureRepository.countByOsEolStatus(EOL)                → device_security_posture
    ← Map<String,Object> of 7 counts
  → renders 8 cards (Non-Critical Events computed client-side)
```

Clicking a card is pure **client-side navigation** (Svelte `createEventDispatcher` → `App.svelte handleDashboardNav`) to the Events / Sequences / IOC / Posture views. No stats-specific endpoint backs the click.

## Tables read (never written by this view)

| Table | Read for | Written by (upstream) |
|-------|----------|------------------------|
| `threat_event` | Total / Critical / High counts | event ingestion service (`POST /api/aria/ingest/events`) |
| `threat_ioc` | Active IOC count | Flyway seed (FBI FLASH) + IOC feed refresh + manual CRUD |
| `precursor_sequence_alert` | Active (unresolved) sequence count | sequence-detection service |
| `device_security_posture` | Low-posture + EOL device counts | posture report ingestion (`POST /api/aria/posture/report`) |

The dashboard issues **zero writes**. See [platform.data-architecture].

## Kafka role (indirect only)

The dashboard has **no Kafka consumer and no producer** — it is a synchronous DB read. Kafka appears one hop upstream, populating the tables it later reads:

| Topic | Direction (in aria) | Producer | Note |
|-------|--------------------|----------|------|
| `hiveops.aria.threats` | produced | `AriaThreatProducer` from `ThreatEventIngestionService` | fan-out of ingested threat events; not consumed within aria |
| `hiveops.aria.sequences` | produced | `AriaSequenceProducer` from `SequenceDetectionService` | fan-out when a precursor sequence trips |

There are **no `@KafkaListener`s** in `hiveops-aria`. This means the executor→Kafka→record-store pattern used elsewhere on the platform does **not** drive this dashboard: writes land directly in Postgres via the ingest/detection services, and the dashboard reads Postgres. Kafka here is purely an outbound notification channel for other consumers. See [platform.kafka].

## Key API endpoint

| Method | Path | Auth | Returns |
|--------|------|------|---------|
| GET | `/api/aria/stats` | `hasRole('BCOS_ADMIN')` | `{ totalEvents, criticalCount, highCount, activeIocCount, activeSequenceCount, lowPostureCount, eolDeviceCount }` |

Auth chain: `SecurityConfig` (stateless, `@EnableMethodSecurity`) → `JwtAuthenticationFilter` validates the Bearer JWT via shared `JwtTokenValidator`, maps the single `role` claim to `ROLE_<role>`. `/stats` is **not** in the `permitAll` list, so it requires an authenticated BCOS_ADMIN.

## Design notes / semantics

- **Windowing is mixed on purpose (or by oversight):** `criticalCount`/`highCount` are **24h** rolling counts (`...DetectedAtAfter(now-24h)`), while `totalEvents` and everything else are **point-in-time totals**. The client's "Non-Critical Events = Total − Critical − High" therefore subtracts a 24h slice from an all-time total. Worth confirming this is intended before building anything else on those numbers.
- **No caching / no auto-poll:** the stat map is recomputed on every `GET /stats`, and the SPA calls it exactly once on mount. Live dashboards would need a poll loop or push.
- **Counting shape:** `activeIocCount` materializes the active-IOC list and takes `.size()` rather than a `count(*)` query — fine at current scale, a candidate for a dedicated count query later.

Related: [aria.dashboard.internal], [technical.aria], [platform.data-architecture], [platform.kafka], [platform.service-ownership].
