---
module: aria.precursor-alerts
title: Precursor Alerts
tab: Architect
order: 30
audience: dev
---

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.
