---
module: agent.aria-signals
title: ARIA Security Signals — ATM threat-signal collector
tab: Architect
order: 30
audience: dev
---

> **Internal · how it's built.** Confirmed against `hiveops-module-aria-signals` source (`com.hiveops.aria`) 2026-07-01. See [technical.agent] for agent-wide module-system context.

## Maven module

`hiveops-module-aria-signals` (artifact `hiveops-module-aria-signals`, own version `1.0.0`, parented by `hiveops-agent-parent` — currently `4.4.3`). Packaged as a standalone jar (`<finalName>${project.artifactId}</finalName>` → `hiveops-module-aria-signals.jar`). Dependencies are all **`provided`** scope (`hiveops-core`, `jackson-databind`, `log4j-api`, `commons-lang3`) — nothing is shaded into the module JAR; it relies on the agent runtime classpath. No dependency on `hiveops-journal`, `hiveops-images`, or `hiveops-file-collection`.

Registered via ServiceLoader SPI: `src/main/resources/META-INF/services/com.hiveops.core.module.AgentModule` → `com.hiveops.aria.AriaSignalModule`. This is how `ModuleLoader` discovers it from the `modules/` directory at startup.

## Classes and flow

Four runtime classes plus three DTOs:

| Class | Role |
|---|---|
| `AriaSignalModule` | `AgentModule` implementation — reads config, wires collaborators, owns the scheduler and lifecycle |
| `AriaSignalCollector` | `Runnable` — polls the Windows Security Event Log + scans staging dirs, batches, POSTs events |
| `AriaPostureReporter` | `Runnable` — builds and POSTs the device posture snapshot |
| `AriaHttpClient` | Thin `HttpURLConnection` JSON POST client to the ARIA base URL |
| `dto.AriaIngestRequest` | Envelope: `deviceAgentId`, `deviceId`, `institutionKey`, `List<AriaSignalItem> events` |
| `dto.AriaSignalItem` | One signal: `signalKey`, `eventType`, `Map<String,Object> rawPayload`, `detectedAt` (ISO-8601 string) |
| `dto.AriaPostureReportDto` | Posture snapshot (see fields below) |

### Lifecycle (`AriaSignalModule`)

`getName()` = `"aria-signals"`; `getLogPackage()` = `"com.hiveops.aria"` → the framework routes its output to `logs/modules/aria-signals.log`.

- `initialize(ModuleContext)` — reads `aria.signals.enabled`; if false, returns (leaves `ready=false`). Reads `aria.endpoint`; if blank, logs a warning and returns. Otherwise builds one `AriaHttpClient(endpoint, serviceSecret, skipSsl)`, resolves `atmName` (`context.getAtmName()`), `institutionKey` (`agent.institution.key` → `incident.institution.key`), `deviceId` (`aria.device.id`, parsed to `Long` or null), intervals and batch size, then constructs the collector and reporter and sets `ready=true`.
- `isEnabled(context)` returns the `ready` flag (so a mis-configured module self-disables rather than erroring).
- `start()` — creates a **2-thread** daemon `ScheduledExecutorService` and schedules both `Runnable`s with `scheduleAtFixedRate`: the collector at `[0, signalIntervalSec]` and the reporter at `[0, postureIntervalSec]`. So the posture report fires **immediately**; the collector's first run just **anchors** the log (see below).
- `stop()` — `scheduler.shutdownNow()`.
- `isHealthy()` — `scheduler == null || !scheduler.isTerminated()`; consulted by the framework `ModuleWatchdog`.

### Signal collection cycle (`AriaSignalCollector`, poll-based)

The trigger model is **poll**, not log-tail or hook. Each `run()`:

1. **First tick anchors, doesn't collect.** If not yet `anchored`, `anchorSecurityLog()` runs `wevtutil qe Security /rd:true /f:XML /c:1 /q:<query>` (newest first, query with no `EventRecordID` bound), parses the max `EventRecordID`, stores it in `lastSecurityRecordId`, sets `anchored=true`, and returns. This prevents replaying historical events. On non-Windows it just sets `anchored=true` and returns.
2. **Subsequent ticks:** `collectSecurityEvents()` builds a query `*[System[(EventID=1102 or … ) and EventRecordID > <lastSecurityRecordId>]]`, runs `wevtutil qe Security /rd:false /f:XML /c:<batchSize> /q:<query>`, and parses the returned XML with **hand-rolled string scanning** (`parseEvents`/`parseBlock`/`extractTag`/`extractAttr` — no XML parser dependency). Each event becomes an `AriaSignalItem` with `signalKey = <eventId>`, `eventType = "OS_EVENT"`, and `rawPayload = {eventId, recordId, provider?}`. The bookmark `lastSecurityRecordId` advances to the max record seen.
3. **Staging-dir scan** (`scanStagingDirs()`, runs on all OSes but paths are Windows `C:/...`): for each existing directory, any regular file whose name matches `SUSPICIOUS_FILENAMES` (case-insensitive) emits `AriaSignalItem(signalKey="IOC_FILENAME_MATCH", eventType="FILE_SYSTEM", payload={path, filename})`. Separately, if the directory contains **any** file at all, it emits `AriaSignalItem(signalKey="SUSPICIOUS_DIRECTORY", eventType="FILE_SYSTEM", payload={directory})`.
4. If the combined list is non-empty, wrap in `AriaIngestRequest(deviceAgentId, deviceId, institutionKey, events)` and `client.sendEvents(...)`.

Note the bookmark advances **before** the POST, so a failed send does not re-queue those Security events on the next tick.

### Posture cycle (`AriaPostureReporter`)

`buildReport()` populates `AriaPostureReportDto`: `deviceAgentId`/`deviceId`/`institutionKey`; `osVersion` (`os.name os.version`) and `osEolStatus` (`classifyOsEol` → `EOL` for Win 7/XP/Vista/8, else `SUPPORTED`/`UNKNOWN`). On Windows it adds `diskEncryptionEnabled` (BitLocker `manage-bde -status C:` → "protection on"), `auditPolicyCompliant` (`auditpol` → not "no auditing"), `softwareWhitelistEnabled` (`sadmin status` → "solid", fallback AppLocker registry `EnforcementMode 0x1`). If `aria.gold.image.hash` is set, it SHA-256-hashes `aria.image.path` (`sha256:<hex>`) into `currentImageHash` for drift comparison. Each check returns `null` on failure, and `@JsonInclude(NON_NULL)` drops null fields from the JSON.

## How events reach the platform

`AriaHttpClient` posts JSON via `HttpURLConnection` (connect 10s / read 15s) with `Content-Type: application/json; charset=UTF-8` and header `X-Service-Secret: <aria.service.secret>`:

| Method | Path (relative to `aria.endpoint`) | Body |
|---|---|---|
| `sendEvents` | `POST /api/aria/ingest/events` | `AriaIngestRequest` |
| `sendPostureReport` | `POST /api/aria/posture/report` | `AriaPostureReportDto` |

This is a **direct** agent → ARIA backend path. It does **not** traverse agent-proxy/incident and does **not** use `agent.auth.token`, `FileUploadClient`, or the fleet-task pipeline that other modules share. Non-2xx responses are logged as warnings; there is no retry/backoff.

## Platform abstraction

This module deliberately **does not** use the agent's `PlatformService`/`SystemCommands` abstraction. OS branching is done inline with `System.getProperty("os.name")` and raw `ProcessBuilder` calls to `wevtutil`, `manage-bde`, `auditpol`, `sadmin`, `reg`. Command output is read with the JVM `file.encoding` charset. Consequence: all monitoring except the OS/EOL and image-hash posture fields is effectively Windows-only.
