--- module: aria.precursor-alerts title: Precursor Alerts tab: Architect order: 30 audience: 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 all `threat_event` rows for the device with `detected_at` after `now - window`. - Requires **≥2 recent events** and a **`PHYSICAL_ACCESS`** attack phase, else returns null (no alert). - **JACKPOTTING** if `MALWARE_STAGING` or `MALWARE_EXECUTION` phase 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_TAMPER` or `USB_INSERTION` present. 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 with `phasesDetected[]`, `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.