SIEM events
SIEM events
Cbox\Siem\ValueObjects\SiemEvent is the single, transport-neutral shape your
application produces. It is final readonly — immutable by construction — so a
built event cannot be mutated on its way to a formatter.
Fields
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string |
yes | Stable, unique. Becomes ECS event.id, CEF externalId, JSON id. |
occurredAt |
DateTimeImmutable |
yes | Any timezone; always normalized to UTC on the wire. |
action |
string |
yes | Short stable slug: user-login, role-granted. |
category |
EventCategory |
yes | Normalized category (see below). |
outcome |
Outcome |
yes | Success / Failure / Unknown. |
severity |
Severity |
no (default Info) |
Drives CEF severity, ECS log.level, GELF level. |
actor |
Party|null |
no | Who did it — a type + id pair. |
target |
Party|null |
no | What it was done to — a type + id pair. |
sourceIp |
string|null |
no | Originating address. |
message |
string|null |
no | Human-readable description. |
context |
array<string, scalar|null> |
no | Already-flattened extra detail. |
Cbox\Siem\ValueObjects\Party is a tiny type + id value object used for both
the actor and the target. Party::of('service', 'billing') reads a little nicer
than the constructor.
The context flattening contract
context is an already-flattened bag: keys are strings, values are scalars
(string, int, float, bool) or null. The core does not flatten nested
arrays or objects for you, and it will not guess how they should be represented —
because each SIEM disagrees. Elastic wants dotted keyword fields; CEF wants
key=value; Splunk will take nested JSON. Flatten before you build the event:
// Instead of ['http' => ['method' => 'POST', 'status' => 201]] ...
context: ['http.method' => 'POST', 'http.status' => 201],
Where each formatter puts context: ECS uses labels.*, the JSON formatter a
context object, Splunk HEC a context object inside event, GELF _-prefixed
additional fields, and CEF cbox_<key> extension keys.
Enums
EventCategory
authentication, authorization, iam, configuration, session, threat,
audit. The stored value is emitted verbatim by CEF and JSON. For ECS,
ecsCategory() maps each case onto a valid ECS event.category keyword
(authorization → iam, audit → configuration, …). Because ECS has no
audit category, the exact domain value is always preserved under ECS
cbox.category, so nothing is lost when the mapping broadens.
Outcome
success / failure / unknown — exactly the ECS event.outcome vocabulary,
so the mapping is lossless everywhere. Use Unknown only when the outcome is
genuinely unknown, never as a placeholder.
Severity
info, low, medium, high, critical, with three deterministic mappings so
formatters never have to guess:
| Severity | cef() (0–10) |
logLevel() (ECS) |
syslogLevel() (GELF, 0–7) |
|---|---|---|---|
| Info | 1 | informational | 6 |
| Low | 3 | notice | 5 |
| Medium | 5 | warning | 4 |
| High | 7 | error | 3 |
| Critical | 10 | critical | 2 |
A severity lives on the event (rather than being inferred by a formatter) because CEF requires a header severity and GELF a numeric level — deriving them from outcome would be guesswork.