---
module: aria.threat-events
title: Threat Events
tab: Architect
order: 30
audience: dev
---

How the **Threat Events** feature is built. It is owned end-to-end by **`hiveops-aria`** (the threat-intelligence service, `spring.application.name=hiveops-aria`, DB `hiveiq_aria`). The screen is a thin read view; the interesting work happens in the ingest → classify → store → (optionally) publish pipeline. Cross-link [platform.service-ownership], [platform.data-architecture], [platform.kafka].

## Services involved

- **`hiveops-aria` (owner)** — serves the read API the screen calls, owns ingestion, classification, persistence, and Kafka production for this feature. No other service is required to render Threat Events.
- **Agents / internal callers (producers of data)** — POST batched signals to ARIA's ingest endpoint. They are the *source* of `threat_event` rows.
- **hiveops-incident (downstream consumer, optional)** — a potential consumer of the `hiveops.aria.threats` topic for auto-response; ARIA itself has **no Kafka consumers** for this feature.

## Data flow

```
Agent/service ──POST /api/aria/ingest/events──▶ ThreatEventIngestionController
   (X-Service-Secret header, permitAll)              │
                                                      ▼
                                        ThreatEventIngestionService.ingest()
                                          per item:
                                            ThreatEventClassifier.classify()
                                              → severity, matchedIoc, attackPhase
                                            build ThreatEvent
                                            eventRepository.save()  ──▶ threat_event (hiveiq_aria)
                                            if severity ∈ {CRITICAL, HIGH}:
                                              AriaThreatProducer.publishThreatEvent()
                                                 ──▶ Kafka topic hiveops.aria.threats
                                                     (key = deviceAgentId)
                                            SequenceDetectionService.detectAndAlert(event)
                                              → may emit hiveops.aria.sequences

Browser (BCOS_ADMIN) ──GET /api/aria/events──▶ ThreatEventController.getEvents()
                                                 → threat_event (paged, detectedAt DESC)
```

This is the standard **executor → record-store → Kafka** pattern: the ingestion service is the executor, `threat_event` is the record store, and `hiveops.aria.threats` is the fan-out for downstream reaction. Cross-link [platform.data-architecture].

## Classification

`ThreatEventClassifier.classify(item)` returns a `ClassificationResult(severity, matchedIoc, attackPhase)`:
- **IOC match** — signal is matched against active rows in `threat_ioc`; a hit sets `matched_ioc_id` (the **IOC Match** column).
- **severity** — one of `CRITICAL / HIGH / MEDIUM / LOW / INFO`.
- **attackPhase** — one of `PHYSICAL_ACCESS`, `MALWARE_STAGING`, `MALWARE_EXECUTION`, `PERSISTENCE`, `CLEANUP` (the UI relabels these to Physical Access / Staging / Execution / Persistence / Cleanup).
- `eventType` is parsed from the request; unknown values fall back to `OS_EVENT`.
- Classification happens **once, at ingest**. Rows are not re-classified later.

## DB tables (in `hiveiq_aria`, Flyway-managed, `ddl-auto=validate`)

| Table | Read/Written by this feature |
|-------|------------------------------|
| `threat_event` | **Written** by ingestion (`eventRepository.save`); **read** by the screen (`GET /api/aria/events`, `/stats`). Columns include `device_agent_id`, `device_id`, `signal_key`, `event_type`, `severity`, `matched_ioc_id`, `attack_phase`, `sequence_id`, `institution_key`, `raw_payload` (JSONB), `detected_at`. |
| `threat_ioc` | **Read** during classification to resolve `matched_ioc_id`; surfaced as the IOC Match badge (type + value). |
| `precursor_sequence_alert` | **Written** by `SequenceDetectionService` when an event completes a multi-phase sequence (adjacent feature, not this screen). |

Repository queries used by the read path (`ThreatEventRepository`): `findAllByOrderByDetectedAtDesc`, `findBySeverityOrderByDetectedAtDesc`, `findByDeviceAgentIdOrderByDetectedAtDesc` — all paged, sort `detectedAt DESC`.

## Kafka (produced only)

Cross-link [platform.kafka]. Topics defined in `AriaKafkaConfig` (JSON serializer):

| Topic | Producer | When | Key | Payload |
|-------|----------|------|-----|---------|
| `hiveops.aria.threats` | `AriaThreatProducer.publishThreatEvent` | ingest of a **CRITICAL or HIGH** event only | `deviceAgentId` (or `"unknown"`) | `AriaThreatEvent` (eventId, deviceId, deviceAgentId, institutionKey, signalKey, eventType, severity, description, matchedIocValue/Type, attackPhase, sequenceId, detectedAt, sourceService=`hiveops-aria`) |
| `hiveops.aria.sequences` | `AriaSequenceProducer` (via `SequenceDetectionService`) | a precursor sequence trips | — | sequence alert |

**MEDIUM / LOW / INFO events are stored but never published** (`shouldPublish` = CRITICAL||HIGH). ARIA has **no `@KafkaListener`** for this feature — it is a pure producer here.

## Key API endpoints (this feature)

| Method | Path | Auth | Role |
|--------|------|------|------|
| GET | `/api/aria/events` | JWT | `hasRole('BCOS_ADMIN')` |
| GET | `/api/aria/stats` | JWT | `hasRole('BCOS_ADMIN')` |
| POST | `/api/aria/ingest/events` | `X-Service-Secret` header | `permitAll` (secret checked only if `ARIA_SERVICE_SECRET` non-blank) |

`GET /api/aria/events` params: `severity?`, `deviceAgentId?`, `page` (0), `size` (50). Returns a Spring `Page<ThreatEvent>`.

## Frontend

`hiveops-aria/frontend/src/components/ThreatEvents.svelte` → `ariaApi.getEvents` in `src/lib/api.ts`. Base URL from `window.__APP_CONFIG__.apiUrl` (dev default `http://localhost:8017`, `/aria` suffix trimmed for the gateway var). Client-side 60s auto-refresh; filters map straight to the query params above.

## Design note (verified)

`getEvents` evaluates `severity` before `deviceAgentId` and returns early — so the two filters are **mutually exclusive** at the backend even though the UI can send both. If combined severity+device filtering is a requirement, the controller needs a combined repository query (`findBySeverityAndDeviceAgentId…`). See Internal/Claude tabs.
