cboxdk / siem
Zero-dependency SIEM log-streaming core for PHP: a normalized security-event value object and the formatters (Splunk HEC, Elastic ECS, ArcSight/syslog CEF, generic JSON) that turn it into what real SIEMs ingest. Framework-agnostic; the delivery/egress layer lives in the Laravel wrapper.
Requires
- php: ^8.4
- ext-json: *
Requires (Dev)
- laravel/pint: ^1.18
- pestphp/pest: ^3.5 || ^4.0
- phpstan/phpstan: ^2.0
README
A zero-runtime-dependency SIEM log-streaming core for PHP 8.4+. You produce one normalized security event; it hands you back exactly the bytes a real SIEM ingests — a Splunk HEC envelope, an Elastic ECS document, an ArcSight/syslog CEF line, a Graylog GELF message, or generic NDJSON. Nothing else: no HTTP, no queue, no credentials, no framework.
composer require cboxdk/siem
The two-tier picture
Log streaming to a SIEM splits cleanly into two jobs, and this package is deliberately only the first one:
- Format — turn a normalized event into each SIEM's wire schema. Pure, deterministic, dependency-free, security-critical (CEF injection lives here). That is this package.
- Deliver — ship those records over the network: SSRF-guarded egress, TLS,
batching, a queue, retries, a dead-letter queue, encrypted secrets. That is
the Laravel wrapper
cboxdk/laravel-siem(separate package), plus alaravel-idaudit binding. Not here.
Keeping the formatting core free of I/O means the security-sensitive part — the escaping that stops log injection — is small, framework-agnostic, and testable in isolation, and the delivery concerns live where a framework can do them properly.
Mental model
your app ──▶ SiemEvent ──▶ StreamFormatter ──▶ formatted record ──▶ StreamSink
(normalize) (per-SIEM schema) (a string) (deliver: wrapper)
SiemEvent— one immutable, transport-neutral value object: a stable id, when it happened, an action, a category, an outcome, a severity, an optional actor and target, an optional source IP and message, and an already-flattenedcontextbag.StreamFormatter—format(SiemEvent): string, one record per event. Ships five implementations. A batch is just the formatter mapped over many events; framing (NDJSON newlines, HEC concatenation, syslog envelopes) is the transport's job.StreamSink— a pure interface for delivery. The core ships no implementation (onlyCbox\Siem\Testing\FakeStreamSinkfor tests); the real sink is the wrapper's.
Quickstart
use Cbox\Siem\Enums\EventCategory; use Cbox\Siem\Enums\Outcome; use Cbox\Siem\Enums\Severity; use Cbox\Siem\Formatters\CefFormatter; use Cbox\Siem\ValueObjects\Party; use Cbox\Siem\ValueObjects\SiemEvent; $event = new SiemEvent( id: 'evt_01HZX', occurredAt: new DateTimeImmutable(), action: 'user-login', category: EventCategory::Authentication, outcome: Outcome::Success, severity: Severity::Medium, actor: new Party('user', '42'), sourceIp: '203.0.113.7', message: 'User 42 signed in', context: ['method' => 'password', 'mfa' => true], ); echo (new CefFormatter)->format($event); // CEF:0|Cbox|SIEM|0.1.0|user-login|User 42 signed in|5|rt=... cat=authentication act=user-login ...
Swap the formatter for any other with no other change:
use Cbox\Siem\Formatters\EcsFormatter; use Cbox\Siem\Formatters\SplunkHecFormatter; use Cbox\Siem\Formatters\GelfFormatter; use Cbox\Siem\Formatters\JsonFormatter; (new EcsFormatter)->format($event); // Elastic Common Schema JSON (new SplunkHecFormatter)->format($event); // Splunk HEC envelope (new GelfFormatter('edge-1'))->format($event); // Graylog GELF 1.1 (new JsonFormatter)->format($event); // generic single-line NDJSON
The formatters, mapped to each SIEM's real schema
| Formatter | Target | Key schema facts |
|---|---|---|
SplunkHecFormatter |
Splunk HTTP Event Collector | {"time": <epoch-seconds.float>, "sourcetype": ..., "event": {...}}; time is seconds, not milliseconds; records concatenate as NDJSON. |
EcsFormatter |
Elastic Common Schema | @timestamp (RFC-3339 UTC), pinned ecs.version, event.{id,action,category[],type[],kind,outcome}, log.level, user.id, source.ip, labels.*, custom cbox.*. |
CefFormatter |
ArcSight / syslog | `CEF:0 |
GelfFormatter |
Graylog (GELF 1.1) | version 1.1, host, short_message, epoch-seconds timestamp, numeric level 0–7, _-prefixed additional fields (_id forbidden). |
JsonFormatter |
generic / NDJSON | deterministic single-line, UTF-8 safe JSON. |
Security posture (honest scope)
This package formats; it does not deliver. So its security surface is exactly
one thing, and it takes it seriously: preventing log/record injection during
formatting. The CEF formatter is the sharp edge — a CEF record is a single
syslog line, so an unescaped |, =, or newline in attacker-influenced data
could forge a header field, an extension key, or a whole second event. All
escaping is isolated in a tested Cbox\Siem\Support\CefEscaper and proven with an
adversarial round-trip test. Newline neutralization is unconditional — there
is no config flag that can turn it off.
Everything downstream of a formatted string — SSRF-safe egress, TLS, auth,
secret storage, retries — is out of scope here by design and belongs to
cboxdk/laravel-siem. See SECURITY.md and
docs/security/_index.md.
Requirements
- PHP 8.4+
ext-json— ships with core PHP.
No runtime package dependencies. No framework. See
docs/requirements.md.
Documentation
Full docs live in docs/: a quickstart,
core concepts (the event model and the
formatters), extension points (writing your
own formatter), and the security posture and escaping
guarantee.
Security reporting
Report vulnerabilities through GitHub Private Vulnerability Reporting — see
SECURITY.md.
License
MIT — see LICENSE.