--- module: aria.dashboard title: Threat Dashboard tab: Architect order: 30 audience: bcos --- 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 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_`. `/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].