SIEM output¶
Emitting every integrity finding and every signed operator action as CEF or LEEF over syslog, for Splunk, QRadar, and generic structured-log ingesters.
Enterprise edition
SIEM output is part of the Etminan Enterprise edition. The siem module
is compiled only under the enterprise Cargo feature; in the Standard build
alarm::emit_siem is a no-op and nothing is emitted. See
Editions for the full matrix.
SIEM output is a structured addition for a SOC — one event per finding and per signed operator action — alongside, never instead of, the notification channels. It is off by default: not every deployment runs a SIEM, so nothing is emitted unless it's explicitly switched on.
Consistent with the rest of Etminan: no in-process HTTP client
SIEM output goes over syslog — a local socket or UDP — never an in-process HTTP client. Anything that needs HTTP (Slack, PagerDuty) is an external notify plugin. CEF/LEEF over syslog is the SOC's native ingestion path, so it needs no plugin at all.
Turning it on¶
One environment variable in /etc/etminan-verifier/verifier.env picks the format and switches it on:
ETMINAN_SIEM_FORMAT=cef # cef | leef ; unset/empty/other = OFF
ETMINAN_SIEM_SYSLOG_ADDR=siem.internal:514 # optional: UDP to a remote collector
# unset -> local /dev/log; your syslog daemon forwards it
cef— ArcSight Common Event Format, parsed by Splunk (and ArcSight).leef— IBM QRadar's Log Event Extended Format, parsed natively by QRadar.json— Etminan's own flat, single-line (newline-delimited) schema for generic structured-log ingesters (Elasticsearch, Loki, Vector, Fluent Bit, a custom forwarder). This project defines and owns the shape; it is deliberately not modelled on any third-party tool's input.
An unrecognized value logs a warning and stays off — it never silently guesses a format.
Transport and severity¶
Events default to the local syslog datagram socket /dev/log (your syslog daemon forwards them to the central SIEM — the usual enterprise topology), or go as UDP to ETMINAN_SIEM_SYSLOG_ADDR=host:port. The facility is local0. Emission is strictly best-effort — a broken or absent syslog target must never break a run, so send errors are swallowed.
| Event severity | syslog severity | CEF numeric (0–10) |
|---|---|---|
critical |
err (3) |
10 |
warning |
warning (4) |
5 |
| action / other | info (6) |
3 |
The event model¶
Everything Etminan emits is one of two categories, split on the cat field so your SIEM can route them to different data models.
flowchart LR
A[run/check finding] -->|alarm::fire → emit_findings| S{ETMINAN_SIEM_FORMAT}
B[signed operator action] -->|audit_log::append → emit_audit| S
S -->|cef| C[CEF line over syslog]
S -->|leef| D[LEEF 2.0 line over syslog]
C --> T[(local0 / /dev/log or UDP)]
D --> T
T --> F[Splunk / QRadar / Elastic / Loki]
| Event | cat |
When |
|---|---|---|
| finding | finding |
An integrity check result — one per finding from a run/check cycle, fed only the findings new this cycle (transition-based, so no per-cycle re-fire). |
| operator-action | operator-action |
A signed trust-changing action, taken from the hash-chained audit log. The internal security_finding mirror action is skipped here to avoid double-reporting a finding already emitted. |
Format strings¶
The <version> header field carries the running etminan-verifier version at emit time.
CEF header
LEEF 2.0 header (x09 declares the TAB attribute delimiter)
Field dictionary¶
Field names are stable. Signature and payload blobs are deliberately never placed on the wire — they live in the hash-chained audit log, not the SIEM event.
| Field (CEF / LEEF) | finding | operator-action |
|---|---|---|
sigId / eventId |
the finding kind (pcr-mismatch, template-hash-mismatch, unexpected-pcr-selection, sla-exceeded, pending-overflow, plugin-failure, check-error, audit-chain-invalid, integrity-summary-rejected, …) |
the audit action (host_enrolled, baseline_approved, baseline_rejected, baseline_exclusion_created/_revoked, operator_key_authorized/_revoked/_rotated, tls_cert_rotated, ak_rotated, profile_assigned, notify_policy_set, …) |
cat |
finding |
operator-action |
dhost / dst |
monitored host id | resource acted on (host id, or a key for registry actions) |
cs1 (cs1Label=kind) / kind |
the finding kind | — |
suser / usrName |
— | the signing operator key (hex) |
act |
— | the audit action |
msg |
the finding text | compact k:v, k:v detail summary (blobs dropped) |
sev (LEEF) / CEF numeric |
LEEF sev is the raw word (critical/warning/action); CEF is the numeric mapping above |
same |
Sample events¶
CEF — finding
CEF:0|Etminan|etminan-verifier|<version>|pcr-mismatch|Host integrity finding|10|dhost=web-01 cat=finding cs1Label=kind cs1=pcr-mismatch msg=quoted PCR10 digest does not match the replayed IMA measurement log
CEF — operator action
CEF:0|Etminan|etminan-verifier|<version>|baseline_approved|Signed operator action|3|suser=3a1f9c2b…e7 cat=operator-action act=baseline_approved dhost=web-01 msg=path:/usr/local/bin/app, reason:scheduled deploy 4821
LEEF — finding (attributes are TAB-separated)
LEEF:2.0|Etminan|etminan-verifier|<version>|pcr-mismatch|x09|cat=finding dst=web-01 kind=pcr-mismatch sev=critical msg=quoted PCR10 digest does not match the replayed IMA measurement log
Injection-safe by construction
Finding text can come straight off an IMA log on a possibly-compromised host. CEF/LEEF header and value fields escape the delimiters and newlines and strip any remaining control byte (ESC/BEL/backspace) so an attacker-controlled field can't inject a forged second syslog line or terminal escape sequences into a SOC's raw view.
Shipped integrations¶
Both live in integrations/ in the source distribution.
Neither has been run against the product it targets
We do not operate a Splunk or QRadar instance, so nothing here has been installed on one. What is checked, on every build: the event list is generated from the verifier's own emitting code, the Splunk add-on's extraction patterns are compiled and run against real events this verifier produces, and the QRadar event table is compared against that same list — CI fails if any of them disagree. The first deployment with a real instance is where this stops being a draft. Tell us what it took.
The event list¶
signature_id (CEF) and eventId (LEEF) both carry the same value: a finding
kind or an audit action. There are 59 of them. They are listed with names
and descriptions in integrations/qradar/etminan-events.csv, which is generated
from alarm::kind, audit_log::actions and rbac::Action rather than kept by
hand — a new finding kind that nobody adds to it fails the build.
The list is the Enterprise superset. A Standard build never emits the witness and dual-control events; a QID that never fires costs nothing.
Splunk¶
integrations/splunk/TA-etminan/ is an add-on: sourcetypes etminan:cef and
etminan:leef, field extraction for both, and CIM tagging.
Point an input at the verifier's syslog stream with the matching sourcetype.
TA-etminan/samples/ holds four events in each format for Settings → Add Data
→ Upload, so you can see the extractions before wiring up a live input.
Findings are tagged alert; operator actions are tagged both change and
audit, because a signed operator action is both at once. The fields land as
signature_id, etminan_category, dest, kind, user, action, severity
and message. CEF carries severity as a 0-10 number and LEEF as a word; the
add-on maps the number back onto the word, so a search does not have to know
which format it is reading.
QRadar¶
Send LEEF. QRadar parses it natively from the header, and then the only work
left is the QID map. CEF also works but needs
integrations/qradar/etminan-lsx.xml, a Log Source Extension that exists for
sites standardised on CEF across every product.
Import integrations/qradar/etminan-events.csv with qidmap_cli.sh on the
console, or paste it into the DSM Editor. Its family column is not part of a
QID entry — it tells you which of the three groups a row came from, so findings,
operator actions and access decisions can take different low-level categories.
Import one severity for everything
The CSV deliberately has no severity column, and an earlier version of this
page was wrong to suggest a table of them. A static severity per event type
cannot be correct here: audit-witness-unreachable and check-error are
each raised as critical in some circumstances and warning in others,
decided where the event is constructed rather than declared with the type.
Give every QID the same default and let the event's own severity — the CEF
number, the LEEF sev attribute — drive the offense magnitude. That value
is on every event and it is the authoritative one.
Deployment checklist¶
- Turn on the producer — set
ETMINAN_SIEM_FORMAT(andETMINAN_SIEM_SYSLOG_ADDRfor a remote collector) in the verifier's unit environment, and confirm events reach the SIEM. - Splunk — install
TA-etminan, load the sample files through Add Data to confirm the extractions, then set the sourcetype on the live input. - QRadar — add a LEEF Log Source, import the event table as QIDs with one uniform severity, and check that an offense's magnitude follows the event's own severity rather than the QID default.
Where it plugs into the alarm path¶
SIEM emission is one leg of the verifier's shared channel fan-out. After a run produces findings and the admin notification policy is applied, alarm::dispatch_all calls both the notify channels and emit_siem. The two are independent: the SIEM stream and the audit log always receive findings even when notify plugins are disabled, and the SIEM leg is transition-based (only findings new this cycle), so a SOC gets one structured event per new finding rather than a re-fire every cycle.
Related¶
- Notification channels — the alarm channels this runs alongside; also carries the master notify switch and per-kind suppression that the SIEM leg honours.
- Change-source correlation — the other integration category.
- Editions — what's in Standard vs Enterprise.