Stream the audit log to an HTTPS webhook

Administrator·3 minutes to read

What this does for you

Every new entry in your organisation's audit log is posted, within a minute of being written, to an https endpoint you run — your own collector, or any SIEM's generic HTTP input. Each request is signed, so your endpoint can prove it came from Botseon and refuse anything else. Only an owner or an administrator can see or change audit streams.

Before you start

  • Your team is on Enterprise or its two-week Enterprise trial. The audit log and its streams are part of the Enterprise plan: on a Team plan, Organisation → Audit shows The audit log, its exports and the SIEM stream are part of the Enterprise plan. with a link to See Enterprise.
  • An https address on the public internet that answers 2xx once it has stored the entries. It must not redirect (redirects are not followed) and must not resolve to a private or local network.
  • Somewhere to keep the signing secret, which is shown once.

Steps

  1. Open Organisation → Audit and find Audit streams.
  2. Under Destination, choose HTTPS webhook, give it a Name and enter the Address.
  3. Press Add stream, and copy the signing secret it shows. It is not shown again.
  4. On your endpoint, for every request:
    1. read the raw body before parsing it;
    2. compute the hex HMAC-SHA256 of v1: + the x-botseon-timestamp header + : + the body, keyed with the signing secret exactly as shown;
    3. compare it with the x-botseon-signature header in constant time, and answer 401 if it differs or the timestamp is more than five minutes old;
    4. store the rows, then answer 2xx.

Each request has the header x-botseon-event: audit.rows and a JSON body {"event":"audit.rows","organisation":"<id>","rows":[…]} with up to 200 entries. x-botseon-delivery is the same on a retry of the same entries, so use it to drop duplicates.

A worked example

In Node.js:

import { createHmac, timingSafeEqual } from 'node:crypto';

function verified(secret, headers, rawBody) {
  const ts = headers['x-botseon-timestamp'];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const want = createHmac('sha256', secret).update(`v1:${ts}:${rawBody}`).digest('hex');
  const got = String(headers['x-botseon-signature'] ?? '');
  return got.length === want.length && timingSafeEqual(Buffer.from(got), Buffer.from(want));
}

What can go wrong

The stream's row on Organisation → Audit shows the last delivery and, when the newest attempt failed, why and when it is tried again. A failed batch is never skipped: the same entries are sent again after 15 seconds, then 30 seconds, 1 minute, 5 minutes, 15 minutes, and then every hour until the destination takes them.

What the row saysWhat to do
the destination refused the request (HTTP 401)your endpoint's secret is not the one shown when the stream was added: remove the stream, add it again, and store the new secret
the address is refusedthe address resolves to a private or local network
the destination could not be reached / did not answer in timeyour endpoint is down, slower than ten seconds, or answers with a redirect (enter the final address)

See also

Last verified against build 9d01262.