---
module: aria.ioc-feeds
title: IOC Feeds
tab: Architect
order: 30
audience: dev
---

How the **IOC Feeds** feature is built. Everything for this feature lives inside **`hiveops-aria`** — it does not call any other HiveIQ service. Cross-link [platform.service-ownership].

## Component map

| Layer | Class | Role |
|-------|-------|------|
| Frontend | `frontend/src/components/IocFeeds.svelte` | cards + run-history table; polls `/status` every 30s |
| Frontend API | `frontend/src/lib/api.ts` (`ariaApi`) | `getFeedStatus`, `getFeedRuns`, `refreshAllFeeds`, `refreshFeed` |
| Controller | `controller/IocFeedController.java` | `/api/aria/feeds/*`; spawns background threads for refresh |
| Service | `service/IocFeedService.java` | orchestrates fetch → upsert → run record; owns `@Scheduled` cron |
| Feed clients | `feed/OtxFeedClient`, `feed/MalwareBazaarFeedClient`, `feed/ThreatFoxFeedClient` | call external APIs, map raw indicators → `ThreatIoc` |
| Repos | `IocFeedRunRepository`, `ThreatIocRepository` | run history + IOC catalog |
| Entities | `entity/IocFeedRun`, `entity/ThreatIoc` | `ioc_feed_run`, `threat_ioc` tables |

## Data flow (one feed run)

```
[BCOS_ADMIN] Run Feed / Run All            [cron 0 0 3 * * *]
        │  POST /api/aria/feeds/refresh(/{feed})       │ scheduledRefresh()
        ▼                                              ▼
IocFeedController ── new Thread() ──►  IocFeedService.refreshFeed(feedName)
                    (202 accepted)             │
                                               ├─ 1. countRunning guard (skip if RUNNING)
                                               ├─ 2. save ioc_feed_run row (status=RUNNING, started_at)
                                               ├─ 3. fetchFromFeed → <Feed>Client.fetch()  ──► external HTTPS
                                               ├─ 4. upsertIocs → threat_ioc  (add / update)
                                               └─ 5. run row → SUCCESS|ERROR, counts, completed_at
```

- **Controller offloads to a raw `Thread`** (named `ioc-feed-manual-all` / `ioc-feed-manual-<FEED>`) with an uncaught-exception handler that logs, and returns `202` immediately. There is **no executor/queue and no Kafka** in this path — it's a synchronous fetch running on a plain thread.
- **Upsert** (`ThreatIocRepository.findByActiveTrueAndIocTypeAndValueIgnoreCase`): match on active + `ioc_type` + case-insensitive `value`. Hit → update `description/source_ref/confidence/reported_at/expires_at` (**updated++**); miss → insert (**added++**). Blank values are skipped.

## External sources (feed clients)

| Client | Endpoint | Auth header | Query strategy | IOC types mapped | expires_at |
|--------|----------|-------------|----------------|------------------|-----------|
| OTX | `https://otx.alienvault.com/api/v1` | `X-OTX-API-KEY` | subscribed pulses (8d) + searches `atm+malware`, `jackpotting`, `atm+skimming` | MD5, FILE_PATH, IP, REGISTRY_KEY | +90d |
| MalwareBazaar | `https://mb-api.abuse.ch/api/v1/` | `Auth-Key` | `get_taginfo` per tag: ATM, jackpotting, NCR, Diebold, GreenDispenser, Tyupkin | MD5, FILENAME | +365d |
| ThreatFox | `https://threatfox-api.abuse.ch/api/v1/` | `Auth-Key` | `iocs_by_malware_family` (ATMii, GreenDispenser, Tyupkin, Ploutus, SUCEFUL, Ripper) + `get_iocs` 7d filtered for ATM relevance | MD5, IP (port stripped), FILE_PATH, FILENAME | +180d |

- All three short-circuit to an empty list when their API key is blank (`isConfigured()` false) — that becomes a SUCCESS run with 0/0, not an error.
- Each client swallows per-request errors internally (`log.warn`) and returns whatever it collected; a run only flips to ERROR if the whole `fetchFromFeed`/`upsertIocs` throws.

## DB tables ([platform.data-architecture])

Database **`hiveiq_aria`** (PostgreSQL), Flyway-managed, `ddl-auto=validate`.

| Table | Access here | Notes |
|-------|-------------|-------|
| `ioc_feed_run` | read (status, runs) + write (each run) | added in Flyway **V6**; columns: `feed_name`, `started_at`, `completed_at`, `status`, `iocs_added`, `iocs_updated`, `error_message`; indexes on `feed_name` and `started_at DESC` |
| `threat_ioc` | read (dedup lookup) + write (upsert) | IOC catalog; feed rows carry `source` = `OTX`/`MALWARE_BAZAAR`/`THREAT_FOX` |

- `/status` = for each feed name, `findTopByFeedNameOrderByStartedAtDesc` → latest run summary + `configured` flag.
- `/runs` = `findAllByOrderByStartedAtDesc` as a Spring `Page` (default size 20 from the UI), so the frontend gets `content` + page metadata.

## Kafka ([platform.kafka])

- **This feature produces and consumes nothing.** ARIA's Kafka producers (`hiveops.aria.threats`, `hiveops.aria.sequences`) belong to the event-ingestion / sequence-detection paths, not IOC feeds. The catalog these feeds populate is later read synchronously by the classifier when matching device events — no topic sits between the feed and the catalog.

## Scheduling & config

- `@Scheduled(cron = "${aria.feed.refresh.cron:0 0 3 * * *}")` on `IocFeedService.scheduledRefresh()` → `refreshAll()` (iterates the three feeds sequentially).
- Keys: `aria.feed.otx.api-key`, `aria.feed.malware-bazaar.api-key`, `aria.feed.threat-fox.api-key` (env `ARIA_FEED_*_API_KEY`).
- Note: `aria.feed.enabled` is declared but **unused** in code (see Internal tab / uncertainties).

## Security

- `SecurityConfig`: stateless, `@EnableMethodSecurity`. `/api/aria/feeds/**` is **not** in the `permitAll` list, so it falls under `anyRequest().authenticated()` **plus** each method's `@PreAuthorize("hasRole('BCOS_ADMIN')")`. Cross-link [platform.service-ownership].
