SIEM Core
Custom formatter
Writing a custom formatter
A formatter is any class implementing Cbox\Siem\Contracts\StreamFormatter. There
is nothing else to register — construct it and call format().
use Cbox\Siem\Contracts\StreamFormatter;
use Cbox\Siem\ValueObjects\SiemEvent;
final class KeyValueFormatter implements StreamFormatter
{
public function format(SiemEvent $event): string
{
// Keep it pure and deterministic: no I/O, no clock, no globals.
return sprintf(
'id=%s action=%s outcome=%s',
$event->id,
$event->action,
$event->outcome->value,
);
}
public function contentType(): string
{
return 'text/plain';
}
public function name(): string
{
return 'keyvalue';
}
}
Rules to honour
- Pure and deterministic. The same event must yield the same bytes. No
network, no filesystem, no reading the clock — the event already carries
occurredAt. - One record, no framing. Return a single record for a single event. Do not append record separators; a batch is your formatter mapped over many events and the transport frames them.
- Escape untrusted values. If your target format has structural delimiters
(like CEF's
|/=/ newline), any event field could contain them. Isolate the escaping in a small, tested helper — mirrorCbox\Siem\Support\CefEscaper— and never emit a raw newline that a downstream framer could read as a record boundary. See Security → escaping.
Testing it with the dogfooded seam
Compose Cbox\Siem\Testing\InteractsWithSiem into your test case for a
deterministic SiemEvent factory (siemEvent(...)) and a shared
FakeStreamSink, so you assert on real output rather than a mock:
use Cbox\Siem\Testing\InteractsWithSiem;
uses(InteractsWithSiem::class);
it('formats an event', function (): void {
$record = (new KeyValueFormatter)->format(
$this->siemEvent(id: 'e1', action: 'ping'),
);
expect($record)->toBe('id=e1 action=ping outcome=success');
});
Prove your formatter against a real expected shape (a spec vector), not a mock: for a structured format, decode the output and assert the field names and values; for a delimited one, add an adversarial test that feeds every metacharacter through and asserts it cannot break the structure.