Audit streaming
Stream a Team workspace's audit records to your SIEM, verify each batch, and check that your copy has no gaps.
Page text and source link for your AI assistant. Nothing is sent automatically.
Audit streaming sends every record in a Team workspace's audit log to a system you control: admin activity (member, policy, key, billing and setting changes) and Gateway decisions. Records are sent in the order they were recorded. Each one carries the hash that links it to the record before it, so your copy can show that nothing was removed or changed, including by Foo Guard.
The stream also carries Foo Guard's signed checkpoints, each with a proof that covers only your workspace. See Check a signed checkpoint.
Set it up
Owners and admins open Settings › Audit log › Streaming.
- Choose HTTPS endpoint (signed JSON, works with any SIEM that accepts webhooks) or Splunk HTTP Event Collector.
- Enter a public HTTPS URL on port 443, without redirects, credentials or query parameters. For Splunk, also enter the HEC token.
- Choose where to start: new records only, from a date, or full history. History is sent first, in order, then new records follow.
- Save. For an HTTPS endpoint, copy the signing secret now; it is shown only once.
- Send a test, then start streaming.
Changing the destination pauses streaming until a new test succeeds. Choose Continue where streaming stopped to pick up after the last delivered record; together with the copy at the previous destination, nothing is missing.
What your endpoint receives
Each delivery is one POST containing up to 500 records (about 1 MB). The scheduler runs about every five minutes and sends batches until it catches up.
{
"type": "fooguard.audit-stream/v1",
"delivery": "8f0c2c1e-…",
"workspace": { "id": "5d0e…" },
"records": [
{
"sequence": 48211,
"entryHash": "9c1e…",
"entry": "{\"schemaVersion\":\"fooguard.workspace-evidence/v1\",\"sequence\":48211,\"previousHash\":\"e41a…\",…}",
"summary": { "source": "gateway_decision", "requestId": "…", "decision": "block", "outcome": "not_sent", "mode": "enforce", "tool": "create_pull_request", "rule": "…", "agentId": "…", "policyVersion": 12, "at": "2026-10-30T23:14:02Z" }
}
]
}entryis the stored record exactly as Foo Guard hashed it. Keep it byte for byte.summaryis for searching and alerting. It is not part of the proof. Gateway summaries never include tool arguments, responses or credentials, and are left out if the request has been deleted.- A test delivery has
"test": trueand no records.
For Splunk HEC, each record is one event with sourcetype=fooguard:audit, sent with Authorization: Splunk <token>.
Verify each delivery
HTTPS deliveries carry the same signature headers as Gateway webhooks: X-FooGuard-Delivery, X-FooGuard-Timestamp and X-FooGuard-Signature (v1= plus the HMAC-SHA256 of the timestamp, a period and the raw body, keyed with your signing secret). Check the signature before storing anything, and check that delivery in the body matches the delivery header.
Return a 2xx within 8 seconds once the batch is stored durably. A failed batch is usually retried with the same delivery ID and records, but after repeated failures or a 413 it is split into smaller batches with new IDs, and changing the destination can re-send records. Store records by sequence and ignore ones you already have.
Check that your copy is complete
For every record:
entryHashmust equal the SHA-256 of theentrytext (UTF-8).- The
previousHashinsideentrymust equal theentryHashof the record with the previoussequence. sequencemust increase by exactly one.
A missing sequence number or a hash that doesn't match means a record was lost or altered.
Check a signed checkpoint
Every hour Foo Guard signs a checkpoint over all workspaces' logs and stores it where it can't be changed or deleted. Your stream receives a checkpoints item when your log grew since the last one, and at least once a day otherwise. Each item arrives after the record it covers, in a delivery with an empty records list.
{
"checkpoints": [{
"checkpoint": { "payload": "{\"schemaVersion\":\"fooguard.evidence-checkpoint/v2\",…,\"workspacesRoot\":\"…\"}", "algorithm": "ECDSA_SHA_256", "keyId": "fooguard-audit-1", "signature": "…" },
"workspace": { "organizationId": "5d0e…", "sequence": 48212, "head": "9c1e…" },
"salt": "…",
"index": 517,
"siblings": ["…", "…"]
}]
}The signed payload holds only a Merkle root over every workspace. The tree has at least 1,024 slots, each workspace sits at a random slot with a fresh random salt every hour, and empty slots hold random values, so the proof tells you nothing about other workspaces, how many there are, or when they were active. To check one:
- Verify the signature with the key named by
keyIdfrom `/.well-known/fooguard-audit-keys.json`, and check that the payload'sissuedAtis not before that key'svalidFrom. - Check that
workspace.organizationIdis your workspace ID, and that the payload is aproductionv2 checkpoint. - Recompute the root from your entry, its salt and the proof, and compare it with
workspacesRootin the signed payload. - Check that
headequals theentryHashof recordsequencein your copy. Then every record up to that one is covered by Foo Guard's signature. - Optionally, confirm the SHA-256 of
checkpoint.payloadappears in the public list at `/api/audit-checkpoints`. Everyone sees the same list, so this shows you weren't given a private copy.
import { createHash, createPublicKey, verify } from 'node:crypto';
const sha = (...parts) => createHash('sha256').update(Buffer.concat(parts)).digest();
export function verifyCheckpointProof(item, publicKeyPem, keyId, myWorkspaceId) {
const { checkpoint, workspace, salt, index, siblings } = item;
if (checkpoint.keyId !== keyId || workspace.organizationId !== myWorkspaceId) return false;
const signedOk = verify('sha256', Buffer.from(checkpoint.payload, 'utf8'),
{ key: createPublicKey(publicKeyPem), dsaEncoding: 'der' }, Buffer.from(checkpoint.signature, 'base64'));
if (!signedOk) return false;
const signed = JSON.parse(checkpoint.payload);
if (signed.schemaVersion !== 'fooguard.evidence-checkpoint/v2' || signed.series !== 'production' || signed.keyId !== keyId) return false;
if (siblings.length !== Math.log2(signed.treeSize)) return false;
const leaf = JSON.stringify({ organizationId: workspace.organizationId, sequence: workspace.sequence, head: workspace.head });
let hash = sha(Buffer.from([0]), Buffer.from(salt, 'hex'), Buffer.from(leaf, 'utf8'));
let position = index;
for (const sibling of siblings) {
const other = Buffer.from(sibling, 'hex');
hash = position % 2 === 0 ? sha(Buffer.from([1]), hash, other) : sha(Buffer.from([1]), other, hash);
position >>= 1;
}
return position === 0 && hash.toString('hex') === signed.workspacesRoot;
}For Splunk HEC, each proof is one event with sourcetype=fooguard:checkpoint; the proof is in the event's checkpointProof field.
Failures and recovery
If your endpoint fails, later records wait so order is kept. Retries back off to once every 30 minutes. After 30 minutes of failures, owners and admins are emailed once for that run of failures. After seven days of failures, streaming stops itself and keeps its place; resume it after fixing the endpoint. Nothing is skipped.
Pausing also keeps its place. Removing the destination deletes its secret or token and stops streaming.