The 162 Architect/Claude docs were tagged 'audience: dev'. The 'dev' tier resolves to isInternal() = ROLE_MSP_ADMIN || ROLE_BCOS_ADMIN, so every MSP_ADMIN could read full internal architecture: source paths, DB tables, service ports, NGINX routes. Verified live on bcos.dev as msp_a@msp1.bcos.dev. Retag them to 'audience: bcos' (isBcos, BCOS_ADMIN only). Content-only change; the 'dev' tier mapping is deliberately left alone so the 15 Internal, 35 Overview and 66 Testing docs stay MSP-visible. Also add platform.guide-access documenting the audience tiers and the per-role visibility matrix, so the access model is queryable from the Guide instead of re-derived from source each time. Verified: MSP module count unchanged (116 across 17 apps); no module loses all its tabs. Guide is bcos.dev only, not deployed to production. Closes #16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.2 KiB
module, title, tab, order, audience
| module | title | tab | order | audience |
|---|---|---|---|---|
| aria.dashboard | Threat Dashboard | Architect | 30 | 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, DBhiveiq_aria) — owns everything on this screen. Serves the stats, owns all four tables. See [platform.service-ownership].hiveops-auth— only for the header'sGET /auth/api/users/mename/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 @KafkaListeners 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/highCountare 24h rolling counts (...DetectedAtAfter(now-24h)), whiletotalEventsand 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:
activeIocCountmaterializes the active-IOC list and takes.size()rather than acount(*)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].