Formatters
Formatters
Every formatter implements Cbox\Siem\Contracts\StreamFormatter:
public function format(SiemEvent $event): string; // one record
public function contentType(): string; // MIME type for the transport
public function name(): string; // stable id: cef, ecs, ...
Formatters are pure and deterministic: the same event always yields the same bytes, with no I/O. A batch is the formatter mapped over many events; framing between records is the transport's job.
JsonFormatter — generic / NDJSON
A neutral, deterministic single-line JSON document (fixed key order, UTF-8 safe,
slashes unescaped), so a newline join of records is valid NDJSON. Fields: id,
timestamp (RFC-3339 UTC), action, category, outcome, severity, and the
optional actor, target, source_ip, message, and context.
contentType() = application/json.
EcsFormatter — Elastic Common Schema
Real ECS field names, not approximations:
| ECS field | Source |
|---|---|
@timestamp |
occurredAt, RFC-3339 UTC with millisecond precision and Z |
ecs.version |
pinned schema version (EcsFormatter::ECS_VERSION) |
event.id / event.action |
id / action |
event.category (array) |
category->ecsCategory() |
event.type (array) |
derived: allowed/denied for access decisions, change for config/audit, indicator for threat, else info |
event.kind |
"event" |
event.outcome |
outcome (success/failure/unknown) |
log.level |
severity->logLevel() |
user.id |
actor->id |
source.ip |
sourceIp |
message |
message |
labels.* |
context |
cbox.* |
raw domain category, and actor/target type (+ target id) |
document() is public, so you can embed the ECS array in another envelope or
assert on it directly. contentType() = application/json.
CefFormatter — ArcSight / syslog
CEF:0|Cbox|<product>|<version>|<signatureId>|<name>|<severity>|<extension>
- Header: vendor (
Cboxby default), product and version (constructor args, defaultSIEM/0.1.0),signatureId←action,name←message(oraction), severity ←severity->cef()(0–10). - Extension (
key=value, space-separated):rt(event time in ms since epoch),cat(raw category),act(action),outcome,externalId(id),src(source IP),suser(actor id),actorType,targetType,targetId,msg(message), and each context entry ascbox_<key>.
Every field is escaped through Cbox\Siem\Support\CefEscaper. This is the
security-critical formatter — see Security → escaping.
contentType() = text/plain.
SplunkHecFormatter — Splunk HTTP Event Collector
{"time": 1752582896.789, "sourcetype": "cbox:siem", "event": { ... }}
time— epoch seconds with a millisecond decimal fraction (a float). This is a common footgun: HEC wants seconds, not milliseconds.sourcetype— constructor arg, defaultcbox:siem. An optionalsourceis emitted only when configured.event— the structured event (id, action, category, outcome, severity, and optional actor, target, source_ip, message, context).
Records are single-line, so concatenating them (HEC accepts back-to-back JSON
objects) yields a valid batch. contentType() = application/json.
GelfFormatter — Graylog (GELF 1.1)
| GELF field | Source |
|---|---|
version |
"1.1" |
host |
constructor arg (default cbox-siem; the wrapper sets the real host) |
short_message |
message (or action) |
timestamp |
epoch seconds with a millisecond decimal |
level |
severity->syslogLevel() — a numeric 0–7, not a keyword |
_action, _category, _outcome, _severity, _event_id, … |
normalized fields, _-prefixed |
_<key> |
each context entry |
The GELF-reserved additional field _id is forbidden: a context key id
(which would produce _id) is skipped rather than emitted, as is any context key
that would clobber a standard additional field.
Transport note. GELF is often sent over UDP, but this package targets
audit/security data where silent datagram loss is unacceptable. The Laravel
wrapper delivers GELF over TCP or HTTP only — never UDP. Framing a GELF/TCP
stream (null-byte delimited) is the transport's concern, not the formatter's.
contentType() = application/json.