Stream AgentValet to your SIEM
SIEM streaming sends your organisation’s governance events to your SIEM as they happen, so your security team sees agent activity next to identity, endpoint and cloud logs. Every event is in the OCSF 1.3 schema, and every batch is signed or authenticated, so your SIEM can trust where it came from.
Enterprise. SIEM streaming is included on the Enterprise plan. Organisation admins set it up in Settings → SIEM streaming. On other plans the page shows the upgrade option; see Pricing & plans.
Two destination types are available today:
- Microsoft Sentinel, through the Azure Monitor Logs Ingestion API, into a table in your own workspace.
- Webhook, an HTTPS endpoint you run (a collector, a SIEM’s HTTP input, or a small function), with every batch signed with HMAC-SHA256.
Splunk HEC, syslog/CEF and Shared Signals (CAEP) are not available yet.
What is sent
Every audit record AgentValet writes for your organisation becomes one OCSF event:
| OCSF class | class_uid | AgentValet events |
|---|---|---|
| API Activity | 6003 | Governed platform calls: allowed, denied and held for approval |
| Authorize Session | 3003 | Approval decisions and replays, AuthZEN and TRQP evaluations |
| Account Change | 3001 | Agents registered, issued as children, suspended, revoked, deleted, re-keyed; platforms connected |
| Entity Management | 3004 | Grants, delegates, connections, organisation settings and SIEM destinations changed |
| Authentication | 3002 | Agent binds and passkey use |
| Detection Finding | 2004 | Circuit breaker trips, anomaly suspensions, lost policy coverage |
| Base Event | 0 | Anything not in a class above |
Each event carries OCSF metadata (product AgentValet, the event uid, the request’s correlation_uid), the actor (the agent and its organisation), the platform and operation, status, severity_id and time, plus an unmapped block with AgentValet ids: the policy, approval, grant (permission_id), parent agent and child issuance record.
Never sent: credentials, tokens or keys, request or response bodies, endpoint URLs (they can carry query strings), the agent’s free-text reason, upstream error text, email addresses, and IP addresses or other forensic fields. Every outbound field comes from an allow-list, and our test suite fails if any of these appear in a payload.
Filters
Each destination chooses:
- Event classes: all of them, or only the ones you pick.
- Minimum severity: from everything up to Critical only. Denials are Medium; circuit breaker trips and suspensions are High.
- Routine allowed calls: off by default. Every allowed platform call is an event, and most SIEMs bill by the gigabyte. With it off you still get denials, held requests, approvals and lifecycle changes, subject to the two filters above.
How delivery works
- Events are queued in the same transaction as the audit record, and sent within about a minute, in batches.
- A failed batch is retried with exponential backoff (30 seconds, doubling, up to an hour, with jitter), honouring
Retry-After. After the destination’s maximum attempts (8 by default) it is marked dead. Errors that retrying cannot fix, such as a refused signature or a missing table, go to dead straight away. - Retry dead on the destination puts its dead deliveries back in the queue, for example after you fix a receiver.
- Delivery is at least once. If a batch is resent after a timeout or a partial failure, your SIEM may receive an event twice: deduplicate on the event
uid(theEventUidcolumn in Sentinel). - The delivery log on the settings page shows the latest 100 deliveries and their errors. Delivered records are kept 7 days, others 30.
- Send test event sends one event to that destination only, ignoring its filters, so you can check a receiver end to end.
Microsoft Sentinel
AgentValet writes to a custom table, AgentValet_CL, in the Log Analytics workspace your Sentinel uses. Everything is created in your subscription by a template; AgentValet only holds an app registration’s client id and secret, and can only write to that one data collection rule.
1. Create an app registration
In the Entra admin centre, or with the Azure CLI:
az ad app create --display-name "AgentValet SIEM streaming"
# note the appId (the client id), then:
az ad sp create --id <appId>
az ad sp show --id <appId> --query id -o tsv # the service principal object id, for step 2
az ad app credential reset --id <appId> --display-name agentvalet --years 1
# note the "password": this is the client secret, shown onceYou need the Directory (tenant) ID, the Application (client) ID, the client secret’s Value, and the service principal’s object id.
2. Deploy the template
Download the template as Bicep or ARM JSON and deploy it into the resource group that holds your Sentinel workspace:
az deployment group create \
--resource-group <workspace-resource-group> \
--template-file agentvalet-sentinel.bicep \
--parameters workspaceName=<workspace-name> servicePrincipalObjectId=<object-id-from-step-1>It creates:
- the custom table
AgentValet_CL(Analytics plan, your workspace’s retention); - a data collection rule of kind
Direct, with its own logs ingestion endpoint (no data collection endpoint needed); - the Monitoring Metrics Publisher role for your app on that rule only.
Assigning that role needs Owner, User Access Administrator or Role Based Access Control Administrator on the resource group. If you only have Contributor, add assignRole=false to the parameters, and have someone with one of those roles grant it afterwards:
az role assignment create --assignee-object-id <object-id-from-step-1> --assignee-principal-type ServicePrincipal --role "Monitoring Metrics Publisher" --scope <dataCollectionRuleId output>The deployment prints logsIngestionEndpoint (for example https://dcr-agentvalet-ab12-australiaeast.logs.z1.ingest.monitor.azure.com), dcrImmutableId, streamName and dataCollectionRuleId.
3. Add the destination
In Settings → SIEM streaming, choose Add destination, then Microsoft Sentinel, and enter the tenant id, client id, client secret, and the three outputs. When you save, AgentValet asks Microsoft Entra for a token with those credentials, so a wrong tenant, client id or secret is refused straight away. The client secret is stored encrypted and never shown again.
4. Send a test event
Choose Send test event. A new role assignment can take up to 30 minutes to take effect, and the first data in a new table can take up to 15 minutes to appear. Until the role is active, deliveries show HTTP 403 and are retried up to the destination’s maximum attempts (about an hour at the default of 8), then marked dead; use Retry dead once the role is active. Then query:
AgentValet_CL
| where TimeGenerated > ago(1h)
| project TimeGenerated, ClassName, Severity, Status, AgentId, PlatformId, Operation, Reason, EventUid
| order by TimeGenerated descColumns
The key fields are flat for KQL, and the whole OCSF event is in Ocsf (dynamic):
| Column | Type | From |
|---|---|---|
TimeGenerated | datetime | time |
EventUid | string | metadata.uid (deduplicate on this) |
ClassUid, ClassName, CategoryUid | int, string, int | the OCSF class |
ActivityId, ActivityName, TypeUid | int, string, long | the OCSF activity |
SeverityId, Severity | int, string | severity_id |
StatusId, Status, Message | int, string, string | the outcome |
AgentId, ParentAgentId, OrgId, OwnerId | string | who acted |
PlatformId, Operation, Result, Reason | string | what was attempted and the decision |
CorrelationUid, PolicyId, ApprovalId, PermissionId | string | AgentValet ids to join on |
OcsfVersion | string | metadata.version |
Ocsf | dynamic | the whole event |
AgentValet’s tests check every record against the column list in this template, so the two cannot drift apart. Azure public cloud only for now.
Troubleshooting
HTTP 403in the delivery log: the role assignment has not propagated yet (wait up to 30 minutes), or it was removed. Deliveries are retried until the destination’s maximum attempts; after that, use Retry dead.entra: invalid_client AADSTS7000215: the client secret is wrong or expired. Edit the destination and paste a new one.HTTP 404(or403): the DCR immutable id or stream name may not match the deployed rule. Compare them with the template outputs.HTTP 400: the table or rule was changed by hand and no longer matches the template. Redeploy the template.
Webhook
AgentValet POSTs each batch to your HTTPS URL (public, port 443, no query string):
{ "schema": "ocsf", "schema_version": "1.3.0", "count": 2, "events": [ { "class_uid": 6003, "metadata": { "uid": "..." } } ] }with these headers:
| Header | Value |
|---|---|
X-AgentValet-Timestamp | Unix seconds when the batch was signed |
X-AgentValet-Signature | v1= + hex HMAC-SHA256 of <timestamp>.<raw body>, keyed with your signing secret |
User-Agent | AgentValet-SIEM/1 |
Answer with any 2xx. AgentValet retries 408, 425, 429 and 5xx; other 4xx responses and redirects go to dead.
Verify the signature
Compute the HMAC over the raw body exactly as received (before parsing), compare in constant time, and refuse a timestamp more than five minutes from your clock so an old batch cannot be replayed.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyAgentValet(rawBody, headers, secret) {
const ts = Number(headers["x-agentvalet-timestamp"]);
if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = "v1=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const got = String(headers["x-agentvalet-signature"] ?? "");
return got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}import hmac, hashlib, time
def verify_agentvalet(raw_body: bytes, headers, secret: str) -> bool:
ts = int(headers.get("X-AgentValet-Timestamp", "0"))
if abs(time.time() - ts) > 300:
return False
msg = f"{ts}.".encode() + raw_body
expected = "v1=" + hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(headers.get("X-AgentValet-Signature", ""), expected)The signing secret is generated in the settings page (or pasted, at least 32 characters) and is never shown again after you save. To rotate it, edit the destination and enter a new one.
Limits and what is not covered yet
- Splunk HEC, syslog/CEF over TLS and Shared Signals (CAEP) are not available yet.
- Some actions do not write an audit record today, so they are not streamed: changes to policies and policy rules, disconnecting or deleting a platform, agent self-diagnostic reports, and refused agent sign-ins.
- Up to 10 destinations per organisation.
- After a downgrade from Enterprise, streaming stops at once. Destinations stay listed, and you can delete them.