Skip to content

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 — mirror Cbox\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.