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.1 KiB
module, title, tab, order, audience
| module | title | tab | order | audience |
|---|---|---|---|---|
| aria.precursor-alerts | Precursor Alerts | Architect | 30 | bcos |
How the Precursor Alerts feature is built. Everything lives in hiveops-aria (port 8095, DB hiveiq_aria). ARIA owns detection end-to-end — it does not read another service's tables to build alerts. See [platform.service-ownership].
Services involved
- hiveops-aria — the only service in the write path. Ingests threat events, classifies them, detects multi-phase sequences, persists alerts, serves this screen, and emits a Kafka event downstream.
- Agents / internal callers — POST threat events into ARIA ingestion (out of scope of this screen but the source of all data).
- Downstream consumers (potential) — anything subscribing to
hiveops.aria.sequences. ARIA itself has no Kafka consumers for this feature; the topic is fire-and-forget.
Data flow
agent/internal → POST /api/aria/ingest/events
→ ThreatEventIngestionService.ingest() [@Transactional]
→ ThreatEventClassifier.classify() (severity + attack_phase + IOC match)
→ save ThreatEvent (threat_event)
→ if severity HIGH/CRITICAL: AriaThreatProducer → topic hiveops.aria.threats
→ SequenceDetectionService.detectAndAlert(event)
→ query recent events for device within window
→ detect JACKPOTTING / SKIMMING / (null)
→ dedup guard (one active alert per device)
→ stamp events with sequence UUID, save
→ save PrecursorSequenceAlert (precursor_sequence_alert)
→ AriaSequenceProducer → topic hiveops.aria.sequences
This is the ingest → executor(detect) → persist → Kafka pattern: the ingestion service is the executor, detection persists the record, then publishes an event to the record-store topic for downstream fan-out. See [platform.kafka], [platform.data-architecture].
Detection logic (SequenceDetectionService)
- Window:
aria.sequence.window.minutes(ARIA_SEQUENCE_WINDOW_MINUTES, default 15). Pulls allthreat_eventrows for the device withdetected_atafternow - window. - Requires ≥2 recent events and a
PHYSICAL_ACCESSattack phase, else returns null (no alert). - JACKPOTTING if
MALWARE_STAGINGorMALWARE_EXECUTIONphase present. Confidence = weighted sum of phases (PHYSICAL_ACCESS.25,MALWARE_STAGING.30,MALWARE_EXECUTION.25,PERSISTENCE.10,CLEANUP.05) + .10 if any CRITICAL event + .05 if any IOC match, capped at 1.0, 3-dp. - SKIMMING if signal
CARD_READER_TAMPERorUSB_INSERTIONpresent. Base .35 + .40 (tamper) + .20 (USB) + .05 (critical), capped 1.0. - Dedup:
existsByDeviceAgentIdAndResolvedAtIsNull— a device with an unresolved alert gets no new alert until the existing one is resolved. - On detect: all correlated events get the same
sequence_id(UUID) written back, then the alert row is saved withphasesDetected[],confidence,institutionKey,sequenceId.
Kafka
| Topic | Direction | Producer | Key | When |
|---|---|---|---|---|
hiveops.aria.sequences |
produced | AriaSequenceProducer.publishSequenceAlert |
deviceAgentId |
on every raised sequence alert |
hiveops.aria.threats |
produced | AriaThreatProducer.publishThreatEvent |
deviceAgentId |
per HIGH/CRITICAL ingested event (feeds context, not alerts) |
No @KafkaListener consumes these in ARIA. Payloads are JSON (AriaSequenceEvent, AriaThreatEvent). AriaSequenceEvent.alertId is a freshly generated UUID (not the DB row id). See [platform.kafka].
Database (hiveiq_aria, Flyway-managed, ddl-auto=validate)
| Table | Role in this feature | Read | Write |
|---|---|---|---|
threat_event |
source signals; also linked to a sequence via sequence_id |
detection query; events panel | sequence_id stamped on correlate |
precursor_sequence_alert |
the alert record shown on this screen | list / resolve | created on detect; resolved_at/resolved_by on resolve |
precursor_sequence_alert columns: device_id, device_agent_id, sequence_type, phases_detected[], confidence (precision 4 scale 3), triggered_at, auto_response_taken, resolved_at, resolved_by, institution_key, sequence_id. See [platform.data-architecture].
API endpoints (PrecursorSequenceAlertController, all hasRole('BCOS_ADMIN'))
| Method | Path | Purpose |
|---|---|---|
| GET | /api/aria/sequences |
paged list; params resolved, sequenceType, deviceAgentId, page, size (default size 25); sorted triggeredAt desc |
| GET | /api/aria/sequences/{id}/events |
threat events sharing the alert's sequence_id |
| POST | /api/aria/sequences/{id}/resolve |
idempotent resolve; stamps resolved_at + resolved_by (JWT email) |
Note the repository query precedence in getSequences: a non-blank deviceAgentId overrides the resolved and sequenceType filters (device query returns all statuses for that device). Only when no device filter is set do the resolved/type branches apply.