# Send audit events and drift to your tools

> Page: https://docs.plotwork.dev/siem-delivery. Index of every page: https://docs.plotwork.dev/llms.txt

On Enterprise, an organisation can send every event of its audit log, and every drift run its CI reports, to the tools it already runs: Datadog, Dynatrace, Splunk, Microsoft Sentinel, or an HTTPS webhook of its own. Plotwork sends each one as it happens, in batches, and tries again for a day when a tool is down.

## TL;DR

| Question | Answer |
|---|---|
| Which plan? | Enterprise. On another plan the Integrations tab says so, nothing is sent, and a destination saved earlier can still be removed. |
| Who sets it up? | The owner and the admins, signed in, on the organisation's Integrations tab in the portal. |
| Where can it go? | Datadog (logs, and drift as metrics), Dynatrace (logs, and drift as metrics), Splunk (HTTP Event Collector), Microsoft Sentinel (the Azure Monitor Logs Ingestion API), or a webhook signed with HMAC-SHA256. Up to five destinations. |
| What is sent? | Metadata only: the same fields as the audit log's export and the drift dashboard. Never a diagram's source, a key's value or a token. |
| How fast? | About five seconds after an event, in batches of up to 100. |
| What if my tool is down? | Plotwork tries again after 30 seconds, then waits twice as long each time, up to an hour, for a day. After a day the events are dropped and counted. |
| How are secrets kept? | Sealed with AES-256-GCM as soon as they are saved, and never shown again, not even to the owner. Leave the field empty when you edit to keep the saved one. |
| Can Plotwork reach my private network? | No. Plotwork sends only to HTTPS addresses on public names. |

## 1. Add a destination

1. Open the organisation in the portal and choose the **Integrations** tab.
2. Choose the tool, give the destination a name, and choose what it receives: the audit log, the drift results, or both.
3. Fill in the tool's fields from the section below, then save.
4. Press **Send test event**. Plotwork sends one event of type `test` at once and says whether the tool took it, or why not, in one sentence such as "Datadog answered HTTP 403."

Each destination then shows, in plain sentences, when it last took a batch, its last error, how many events wait to be sent, how many were given up on, and when it tries next.

## 2. Datadog

Audit events and drift runs go to the Logs intake, `https://http-intake.logs.<site>/api/v2/logs`. Drift runs also go to the metrics API, `https://api.<site>/api/v2/series`.

| Field | What to enter |
|---|---|
| Site | The site your organisation signs in to, from the table below |
| API key | An API key from Organization Settings, API Keys. Not an application key. It is sent as the `DD-API-KEY` header. |

| Site | Domain |
|---|---|
| US1 | `datadoghq.com` |
| US3 | `us3.datadoghq.com` |
| US5 | `us5.datadoghq.com` |
| EU1 | `datadoghq.eu` |
| AP1 | `ap1.datadoghq.com` |
| US1-FED (`gov`) | `ddog-gov.com` |

Each log has `ddsource` and `service` set to `plotwork`, a `plotwork_type` tag (`audit_event`, `drift_run` or `test`), a one-line summary as its message, and the whole event under the `plotwork` attribute.

For each diagram a drift run checked, Plotwork sends two gauges tagged `repo:<repository>` and `source:<name>`:

| Metric | Value |
|---|---|
| `plotwork.drift.stale` | 1 when the diagram drifted from its files or could not be made, 0 when it is in step or was written |
| `plotwork.drift.checked` | 1 |

A point carries the time it is sent, because Datadog refuses points more than an hour old and a retried batch can be older.

## 3. Dynatrace

Audit events and drift runs go to Log Monitoring API v2, `https://<environment id>.live.dynatrace.com/api/v2/logs/ingest`. Drift runs also go to Metrics API v2, `/api/v2/metrics/ingest`, as line protocol.

| Field | What to enter |
|---|---|
| Environment id | The first part of your environment's address, such as `abc12345` in `abc12345.live.dynatrace.com` |
| Access token | A token with the **Ingest logs** (`logs.ingest`) and **Ingest metrics** (`metrics.ingest`) scopes. It is sent as `Authorization: Api-Token <token>`. |

Each log record has the event as JSON in `content`, its time in `timestamp`, `log.source` set to `plotwork`, and the attributes `plotwork.type`, `plotwork.org_id`, and `plotwork.action` or `plotwork.repository`. The drift metrics are `plotwork.drift.stale` and `plotwork.drift.checked`, gauges with the dimensions `repo` and `source`, as for Datadog, stamped by Dynatrace when they arrive.

## 4. Splunk

Events go to the HTTP Event Collector.

| Field | What to enter |
|---|---|
| Address | The collector's event endpoint, such as `https://http-inputs-<stack>.splunkcloud.com/services/collector/event` on Splunk Cloud, or `https://splunk.example.com:8088/services/collector/event` on a server you run. HTTPS on port 443 or 8088. |
| Index | The index to write to, or empty for the token's default |
| Token | An HTTP Event Collector token. It is sent as `Authorization: Splunk <token>`. Turn indexer acknowledgement off for this token. |

Each event has `host` set to `plotwork.dev`, `source` set to `plotwork`, a `sourcetype` of `plotwork:audit_event`, `plotwork:drift_run` or `plotwork:test`, its time in seconds, and the whole event as `event`.

## 5. Microsoft Sentinel

Events go to a Log Analytics workspace through the Azure Monitor Logs Ingestion API, with a token Plotwork asks Microsoft Entra ID for with the client credentials you give. Plotwork keeps the token in memory until five minutes before it expires, and never stores it.

Set it up in Azure once:

1. In Microsoft Entra ID, register an application and add a client secret. Note the tenant id, the application (client) id and the secret's value.
2. In the workspace that Sentinel uses, create a custom table from a data collection rule (Tables, Create, New custom log (DCR-based)), named for example `PlotworkAudit_CL`, with the columns below. Azure creates the rule and its stream, `Custom-PlotworkAudit_CL`.
3. Give the application the **Monitoring Metrics Publisher** role on the data collection rule.
4. From the rule's JSON view, copy its `immutableId`. From its data collection endpoint (or the rule's own logs ingestion endpoint), copy the logs ingestion address.

| Column | Type | Holds |
|---|---|---|
| `TimeGenerated` | datetime | When the event happened |
| `RecordType` | string | `audit_event`, `drift_run` or `test` |
| `OrgId` | string | The organisation's id |
| `Summary` | string | A one-line summary, such as `orgs.member.role by ada@acme.com on dev@acme.com` |
| `Data` | dynamic | The whole event |

| Field | What to enter |
|---|---|
| Tenant | The tenant id or its primary domain |
| Client id | The application (client) id |
| Client secret | The secret's value |
| Endpoint | The logs ingestion address, on `*.ingest.monitor.azure.com` |
| Rule id | The rule's immutable id, `dcr-` and 32 hexadecimal characters |
| Stream | The stream, such as `Custom-PlotworkAudit_CL` |

## 6. A webhook

Plotwork posts each batch to your HTTPS address as a JSON array of events.

| Header | Value |
|---|---|
| `Content-Type` | `application/json` |
| `Plotwork-Signature` | `t=<unix seconds>,v1=<hex>`: HMAC-SHA256 under your signing secret of the timestamp, a full stop, and the raw body |
| `Plotwork-Delivery` | The id of the batch's first event, so a retry of the same batch carries the same value |
| `User-Agent` | `Plotwork-Delivery/1` and a link to this page |

| Field | What to enter |
|---|---|
| Address | Your endpoint. HTTPS on port 443, on a public name. |
| Signing secret | At least 32 characters. **Generate** in the portal fills in a random one; copy it to your receiver before you save, because it is not shown again. |

Answer with a 2xx status within 10 seconds. Any other status, a redirect, or no answer is a failure, and the batch is sent again later. Check the signature over the body exactly as it arrived, before parsing it, and refuse a timestamp more than five minutes from your clock, so a captured request cannot be replayed. An event can arrive twice, so use its `id` to skip one you have seen.

In Node:

```js
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyPlotworkSignature(secret, header, rawBody, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map((part) => part.trim().split('=', 2)))
  const timestamp = Number(parts.t)
  if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false
  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
  const given = String(parts.v1 ?? '')
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected))
}
```

In Python:

```python
import hashlib
import hmac
import time

def verify_plotwork_signature(secret: str, header: str, raw_body: bytes, tolerance_seconds: int = 300) -> bool:
    parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
    try:
        timestamp = int(parts.get("t", ""))
    except ValueError:
        return False
    if abs(time.time() - timestamp) > tolerance_seconds:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

## 7. What is sent

Every event has a `type`, a `version` (1), an `id`, the organisation's `orgId`, and `at`, an ISO 8601 time in UTC.

| Type | Fields |
|---|---|
| `audit_event` | The fields of [`plotwork.audit-event.json`](https://docs.plotwork.dev/schemas/plotwork.audit-event.json), the shape the audit log's export uses: `action`, `actor` (`kind`, `id`, `name`), `target` (`type`, `id`, `name`), `metadata`, `ip` (the network only), `userAgent` and `requestId`. The [audit log guide](https://docs.plotwork.dev/audit-log.md) lists every action. |
| `drift_run` | What the GitHub Action reported: `repository`, `ref`, `sha`, `event`, `runUrl`, `pullRequestUrl`; `counts` with `sources`, `inStep`, `drifted`, `written`, `failed` and `errors`; and `sources`, each with `name`, `out`, `status`, `added`, `removed`, `changed`, `errors` and `owners`. The [drift dashboard guide](https://docs.plotwork.dev/drift-dashboard.md) explains each. |
| `test` | `message`, from **Send test event** |

Nothing else is sent: never a diagram's source, a file's content, the names of what changed, a key's value or a token.

## 8. How delivery works

| Question | Answer |
|---|---|
| When is an event sent? | About five seconds after the first event of a burst, so a burst goes as one batch. |
| How big is a batch? | Up to 100 events and 512 KB. Up to 10 batches go out at a time, then the rest a second later. |
| In what order? | Oldest first, for each destination. |
| What counts as a failure? | Any answer that is not 2xx, a redirect, no answer within 10 seconds, or an address that is not public HTTPS. The events stay in the queue. |
| How often is a failure tried again? | After 30 seconds, then 1, 2, 4 and 8 minutes and so on, up to once an hour, each a fifth earlier or later at random. |
| When does Plotwork give up? | An event still unsent a day after it happened is dropped and counted as given up on. So is the oldest event when 10,000 wait for one destination. |
| Can an event arrive twice? | Yes. When a tool takes a batch but its answer is lost, the batch is sent again. Use each event's `id`. |
| What does the status say when something fails? | One sentence of Plotwork's own, such as "Splunk answered HTTP 401." or "The webhook did not answer within 10 seconds." Never what your tool answered. |
| What happens when I remove a destination? | Its sealed secret is deleted with it, and so is everything waiting for it. |
| What does "Paused by Plotwork support" mean? | Plotwork support stopped sending to the destination, usually because it kept failing. Events keep waiting for it and are given up on after a day, as when it fails. `status.pausedAt` says when. Ask support why, or to resume it; nothing you set up is changed. |
| What can Plotwork support see? | Each destination's kind, name, host and delivery status, to answer a question about it. Never its secret, its full address or the events. |

## 9. The API

The Integrations tab uses these routes. They take the portal's session, the owner's or an admin's, and are not in the OpenAPI reference.

| Route | Does |
|---|---|
| `GET /orgs/{org}/integrations` | Lists the destinations: the fields that are not secret, `secretSet` in place of the secret, and each one's `status`, with `available` saying whether the plan sends |
| `POST /orgs/{org}/integrations` | Adds one. The body has `type`, `name`, `streams` (`audit`, `drift`, or both), the type's fields and its secret field (`apiKey`, `token`, `clientSecret` or `signingSecret`). |
| `PUT /orgs/{org}/integrations/{id}` | Changes one. The type cannot change; leave the secret out to keep the saved one. |
| `DELETE /orgs/{org}/integrations/{id}` | Removes one and what waits for it |
| `POST /orgs/{org}/integrations/{id}/test` | Sends a test event now and answers `{ "ok": true }` or `{ "ok": false, "error": "<one sentence>" }` |

| Error code | Status | Means |
|---|---|---|
| `integrations.invalid` | 400 | A field is wrong. `details.reason` is `address` (not a public HTTPS address on an allowed port), `secret_required`, `field` or `type_change`, and `details.field` names the field. |
| `plan.siem_delivery_not_included` | 403 | The organisation's plan does not send to its tools |
| `orgs.forbidden` | 403 | Only the owner and the admins can set it up |
| `integrations.limit` | 409 | The organisation has five destinations already |
| `integrations.unavailable` | 503 | Delivery is not set up on this server |

Adding, changing and removing a destination is recorded in the audit log as `orgs.integration.create`, `orgs.integration.update` and `orgs.integration.delete`, without the secret.

## See also

| Page | Why |
|---|---|
| [Audit log](https://docs.plotwork.dev/audit-log.md) | Every action, the export, and polling it with a key |
| [The drift dashboard](https://docs.plotwork.dev/drift-dashboard.md) | What a drift run reports |
| [Single sign-on](https://docs.plotwork.dev/single-sign-on.md) | The sign-in events the log records |
| [Plans and billing](https://docs.plotwork.dev/plans-and-billing.md) | What Enterprise includes |
| [Privacy policy](https://docs.plotwork.dev/privacy-policy.md) | What is stored to send, and for how long |
