Send AI Agent Audit Logs to Microsoft Sentinel
Part of SIEM audit forwarding. Decision Terrain sends audit events to Microsoft Sentinel through the Logs Ingestion API and confirms them by reading them back. Both ingestion and query access are required; this destination has no submission-only mode. Azure public cloud is the supported baseline.
Prepare the workspace and identities
In the Azure portal or management API, use a Sentinel-enabled Log Analytics workspace and Analytics custom tables. Whoever sets this up needs permission to create and manage tables and data collection rules (DCRs).
The ingestion identity needs the Monitoring Metrics Publisher role on the DCR. Separately grant the query identity permission to query the workspace and read the configured tables, following Microsoft's workspace and table access instructions. Owning the subscription or workspace is not an ingestion grant. Microsoft's Logs Ingestion API documentation describes the DCR role and the endpoint requirements. Use the DCR's logs ingestion endpoint, or a data collection endpoint (DCE) where Private Link or your network topology requires one.
Use explicit background identities for ingestion and for query, either a client secret (tenant ID, client ID and secret) or an Azure managed identity. Complete both identity configurations before validating, testing or activating the destination. Interactive Azure CLI or browser sign-in is not a background credential. Keep secret values out of source, logs and tickets.
Managed identity prerequisites
Authentication runs in the managed audit bridge, so the identity must be available to that service's hosting environment. An identity available only to the Django application or its workers is insufficient.
- In Azure, make the user-assigned managed identity available to the bridge's hosting resource, using a hosting environment supported by Microsoft's ManagedIdentityCredential. Ensure the bridge process or container can use that environment's managed-identity endpoint.
- Grant the ingestion identity Monitoring Metrics Publisher on the DCR. Grant the query identity permission to query the workspace and read the configured audit tables; follow Microsoft's workspace and table access guidance. Allow time for role assignments to propagate.
- In System Settings → External security deployment, installation staff must assign a binding name and the identity's Azure client ID to the intended Organization. This assignment authorizes selection in Decision Terrain; it does not attach an identity to Azure infrastructure or grant Azure roles.
- In the destination form, choose
managed identityfor ingestion and/or query and select the corresponding Assigned ingestion identity or Assigned query identity. Save, validate, send a synthetic test, and verify real indexed events after activation.
The managed route passes the assigned client ID to ManagedIdentityCredential(client_id=...); it does not select an unnamed system-assigned identity. The adapter does not use DefaultAzureCredential or WorkloadIdentityCredential, so Kubernetes workload-identity federation alone does not configure this authentication path. If the bridge's hosting environment cannot supply the selected managed identity through ManagedIdentityCredential, choose client secret and provide that identity's tenant ID, client ID and secret instead. Verify authentication from the actual bridge deployment.
Create the tables and DCR
Use these templates, which ship with Decision Terrain:
| Template | Resource |
|---|---|
| Security table | Analytics table DTAudit_CL, including optional intervention and service-identity columns |
| Agent table | Analytics table DTAgentAudit_CL, including available model metadata |
| Data collection rule | Typed Custom-DTAudit and Custom-DTAgentAudit streams and table routes |
Create the tables first and wait for provisioning to succeed. In a private copy of the DCR, replace <workspace-region> and <workspace-resource-id> with your workspace's region and ARM resource ID. Keep column names, types and the two stream mappings aligned with the tables. The table API spells its datetime type dateTime; the DCR uses datetime.
Authenticated management requests use these paths with the matching private template bodies:
PUT https://management.azure.com/<workspace-resource-id-without-leading-slash>/tables/DTAudit_CL?api-version=2025-07-01
PUT https://management.azure.com/<workspace-resource-id-without-leading-slash>/tables/DTAgentAudit_CL?api-version=2025-07-01
PUT https://management.azure.com/subscriptions/<subscription>/resourceGroups/<resource-group>/providers/Microsoft.Insights/dataCollectionRules/<dcr-name>?api-version=2024-03-11
Use a separately named Direct DCR and record its properties.immutableId and properties.endpoints.logsIngestion. The DCR's ARM name or resource ID is not the ingestion immutable ID. Also record the workspace GUID for query configuration; the DCR destination uses the ARM resource ID, while the query API uses the GUID.
When you update an existing table, update the DCR too: Azure does not update DCRs automatically after a table change. Follow Microsoft's custom table guidance.
Add the destination
In Governance → Policies → External security → Audit forwarding, choose Add destination and select Microsoft Sentinel. Enter the ingestion endpoint, the workspace, the immutable DCR ID, the stream and table for each family you forward, and the ingestion and query identities (tenant, client and secret, or the assigned managed identity). Save, validate, send a synthetic test and activate collection. See SIEM audit forwarding.
Synthetic tests, activation and other destination controls require independent human security-policy approval. A pending request has not taken effect; after a different authorized administrator approves it, the original requester must repeat the exact action.
Verify indexed fields
Generate known real product events for each family you selected. Inspect raw rows for the expected Organization and event identities before deduplicating:
DTAudit_CL
| where OrganizationId == "<organization-uuid>" and EventId == "<security-event-uuid>"
| project EventName, SchemaVersion, EventId, OrganizationId, OccurredAt, CapturedAt,
ActorKind, ActorUserId, ActorServiceIdentityId, Category, Action, Result,
ResourceType, ResourceId, SourceKind, SourceId, CorrelationId,
SecurityAgentId, SecurityWorkUnknown, TimestampNs, TimeGenerated
DTAgentAudit_CL
| where OrganizationId == "<organization-uuid>" and EventId == "<agent-event-uuid>"
| project EventName, SchemaVersion, EventId, OrganizationId, AgentId, RunId,
EventSequence, EventHash, OccurredAt, CapturedAt, Action, Result,
ModelPromptTokens, TimestampNs, TimeGenerated
Use OccurredAt and TimestampNs for exact source timing; Azure can adjust TimeGenerated for out-of-window data, so keep search windows wide. For investigations, deduplicate by EventName, OrganizationId and EventId, for example with summarize arg_max(TimeGenerated, *) by EventName, OrganizationId, EventId. Do not merge agent attempts and outcomes by run or source ID. A configured schema, an API 204 or an empty successful query does not establish indexing.
Starter analytics rules
sentinel-detections.json holds eleven scheduled-rule definitions (display name, KQL, severity, tactics, frequency and look-back) for the conditions in SIEM audit forwarding. Create each as a Scheduled analytics rule, by API or ARM template, after both tables carry the columns from the current templates. Every rule ships disabled and has no incident automation. Review the thresholds before enabling.
To detect a forwarding path that went silent, compare heartbeats with an explicit list of expected Organizations. Heartbeats require an active destination that includes the security audit family; the default interval is one hour, but your operator can change or disable it. This absence detection is not included in the starter file.
Replace the example UUID in ExpectedOrganizations below and add one row per Organization expected to forward security events to this workspace. Maintain this list when Organizations start or intentionally stop forwarding; do not derive it only from recent events.
let ExpectedOrganizations = datatable(OrganizationId:string)
[
"<organization-uuid>"
];
let Heartbeats =
DTAudit_CL
| where TimeGenerated > ago(24h)
| where EventName == "organization.security" and SchemaVersion == 1
| where Action == "siem_heartbeat"
| summarize Last=max(OccurredAt) by OrganizationId;
ExpectedOrganizations
| join kind=leftouter (Heartbeats) on OrganizationId
| where isnull(Last) or Last < ago(2h)
| project TimeGenerated=now(), OrganizationId, Last
Create a Scheduled analytics rule from this query and alert when it returns any rows. Set the rule's query look-back to 24 hours and choose a suitable evaluation frequency, such as every five minutes. The expected list keeps Organizations in the results even after more than 24 hours of silence or if they never reported. A null Last means no heartbeat was found in the query window.
Freshness uses OccurredAt, the original event time, so replaying an old heartbeat cannot make the path look healthy when Azure replaces its stored TimeGenerated with a recent timestamp. The output TimeGenerated=now() marks this alert evaluation, while Last retains the latest observed heartbeat's original occurrence time. Adjust the two-hour threshold for your configured heartbeat interval and indexing delay. Verify the rule with a known missing heartbeat and a replayed old heartbeat before relying on it.
Troubleshoot
| Observation | Check |
|---|---|
| Ingestion 403 | The identity's data role on the DCR and role propagation. Resource ownership is not the ingest permission |
| Schema rejection or missing fields | Both the table and DCR declarations, stream-to-table routing, optional security columns and numeric types |
| Query failure | Separate query permissions, the workspace GUID and the configured tables |
| 401 | The credential type, tenant and client IDs and the secret. Repair credentials before retrying |
| Submitted but not indexed | The actual table rows, processing latency and DCR error telemetry |
| Older events missing from a default search | Search by original occurrence time, not TimeGenerated |
Azure can throttle ingestion; Decision Terrain backs off within bounded retries. A failed query cannot prove an event is absent, so do not resubmit blindly.