---
module: aria.ioc-management
title: IOC Management
tab: Architect
order: 30
audience: dev
---

How the IOC watchlist is built and how it plugs into ARIA's threat pipeline. Cross-link [platform.service-ownership], [platform.data-architecture], [platform.kafka].

## Ownership

- **Single service: `hiveops-aria`** (`spring.application.name=hiveops-aria`, port **8095**, DB `hiveiq_aria`). The IOC catalog, its CRUD API, the external-feed importer, and the classifier that consumes it all live in the same service. No cross-service call is involved in IOC management itself.
- Frontend: `hiveops-aria/frontend` — `IocManagement.svelte`, calling `ariaApi` (`src/lib/api.ts`) at base `/api/aria`.

## Components for this feature

| Layer | Class | Role |
|-------|-------|------|
| Controller | `ThreatIocController` (`/api/aria/ioc`) | CRUD over the watchlist. |
| Repository | `ThreatIocRepository` (Spring Data JPA) | `findByActiveTrue`, `findByActiveTrueAndIocType`, `findByActiveTrueAndIocTypeAndValueIgnoreCase`, `findActiveExecutableIocs`. |
| Entity | `ThreatIoc` → table `threat_ioc` | The IOC record. |
| Feed importer | `IocFeedService` (+ `ioc_feed_run`) | Populates/refreshes IOCs from external feeds. |
| Consumer | `ThreatEventClassifier` | Reads active IOCs to match incoming device events. |

## Data model

Table **`threat_ioc`** (Flyway-managed, `db/migration` V1–V8; `ddl-auto=validate`):

| Column | Notes |
|--------|-------|
| `id` | PK, identity. |
| `ioc_type` | enum `FILENAME, MD5, REGISTRY_KEY, DIRECTORY, SERVICE_NAME, IP, FILE_PATH`. Immutable after create. |
| `value` | the thing being watched for (required). |
| `description` | free text. |
| `source` | enum `FBI_FLASH, FS_ISAC, MANUAL, OTX, MALWARE_BAZAAR, THREAT_FOX`. CRUD-created rows are always `MANUAL`. |
| `source_ref` | e.g. FBI FLASH number. Widened to TEXT in V8. |
| `confidence` | enum `HIGH, MEDIUM, LOW`. |
| `reported_at` | added V7; when the source reported it. |
| `added_at` | insert time (server default `Instant.now()`). |
| `expires_at` | optional expiry. |
| `active` | soft-delete flag; deactivate flips this to false. |

> Note: V1 defined Postgres enum types (`ioc_type`, `ioc_source`, `confidence_level`) but V3 (`convert_enums_to_varchar`) converted them to VARCHAR; the entity still declares `columnDefinition = "ioc_type"` etc.

## Data flow

**Write path (managing the watchlist):**

```
IocManagement.svelte ──HTTP──▶ ThreatIocController ──▶ ThreatIocRepository ──▶ threat_ioc
                                                          (INSERT / UPDATE / active=false)
```

- No Kafka on the IOC CRUD path — it is synchronous JPA.
- Parallel write path: `IocFeedService` runs on a schedule (cron `aria.feed.refresh.cron`, default `0 0 3 * * *`) pulling OTX / MalwareBazaar / ThreatFox, upserting into `threat_ioc` (dedup via `findByActiveTrueAndIocTypeAndValueIgnoreCase`) and recording each run in `ioc_feed_run`.

**Read path (how the watchlist is used):** the catalog is the matching table for ARIA's detection pipeline.

```
agent event ─▶ POST /api/aria/ingest/events ─▶ ThreatEventClassifier
        │            (permitAll + X-Service-Secret)
        │
        ├─ classifier looks up active IOCs by type+value (case-insensitive):
        │     FILENAME / SERVICE_NAME / DIRECTORY / FILE_PATH / REGISTRY_KEY / IP
        │
        ├─ match ─▶ threat_event (matched_ioc_id, severity, attack_phase) ─▶ Kafka hiveops.aria.threats
        │
        └─ SequenceDetectionService correlates phases within a window
                                   ─▶ precursor_sequence_alert ─▶ Kafka hiveops.aria.sequences
```

- IOC matching is **exact value, case-insensitive**, per `findByActiveTrueAndIocTypeAndValueIgnoreCase`. No wildcard/substring matching. `findActiveExecutableIocs` narrows to `FILENAME, MD5, FILE_PATH`.
- Matching applies only to **newly ingested** events; editing the watchlist is not retroactive.

## Kafka (context, not on the CRUD path)

Both topics are produced by ARIA (`AriaKafkaConfig`, JSON), downstream of an IOC match — see [platform.kafka]:

| Topic | Producer | Trigger |
|-------|----------|---------|
| `hiveops.aria.threats` | `AriaThreatProducer` | classified/matched threat event (key = deviceAgentId). |
| `hiveops.aria.sequences` | `AriaSequenceProducer` | a precursor sequence trips. |

ARIA has **no `@KafkaListener`** — it only produces. Auto-response fleet actions are off by default (`aria.autoresponse.shutdown.enabled=false`); when enabled they call `hiveops-incident`.

## Key API endpoints (this feature)

| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| GET | `/api/aria/ioc?type=&activeOnly=` | `BCOS_ADMIN` | List (default `activeOnly=true`). |
| GET | `/api/aria/ioc/active?type=` | authenticated | Active IOCs, no role gate. |
| POST | `/api/aria/ioc` | `BCOS_ADMIN` | Create (`source` forced MANUAL). |
| PUT | `/api/aria/ioc/{id}` | `BCOS_ADMIN` | Update value/description/confidence/expires only. |
| DELETE | `/api/aria/ioc/{id}` | `BCOS_ADMIN` | Soft-deactivate (`active=false`). |

## Design notes / caveats

- **Soft-delete by design** — HTTP DELETE flips `active`, preserving audit/history; the classifier only reads `active = true`.
- **BCOS_ADMIN-only** across the board here — divergent from the platform-wide `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`. See [platform.service-ownership].
- **PUT drops `sourceRef`** — the update handler never persists `request.getSourceRef()` (create does). Flagged as a real bug; noted in uncertainties.
