Audit Log
Immutable record of every authorization and policy decision made by CoreSDK.
Audit Log
Phase note. The
coresdk-auditcrate and sink configuration ship in Phase 2. The hash-chainedAuditLoggerandAuditDraintrait are available in Phase 1 as part of thecoresdk-enginecrate. Sink routing (S3, BigQuery, Webhook) and the streaming/query API are Phase 2.
CoreSDK writes an immutable audit event for every authorization check — whether the decision is allow or deny. Logs are structured JSON, include a distributed trace ID, and can be streamed to S3, BigQuery, or any SIEM in real time (Phase 2).
What gets logged
Every call to engine.policy().evaluate() or _sdk.evaluate_policy() produces one audit event:
{
"ts": "2026-03-19T14:22:01.342Z",
"trace_id": "01HX7BKQM3F8NVYC2T65PWJR4E",
"tenant_id": "acme",
"subject": "usr_2xK9",
"action": "orders:read",
"resource": "ord_7mP3",
"resource_owner": "usr_2xK9",
"policy": "data.authz.allow",
"allow": true,
"deny_reasons": [],
"latency_us": 45,
"sdk_version": "1.0.0"
}| Field | Description |
|---|---|
ts | RFC 3339 timestamp with millisecond precision |
trace_id | ULID — sortable, unique, correlates with your APM traces |
tenant_id | Resolved tenant at decision time |
action | The action string passed to evaluate_policy |
resource | Resource identifier passed to evaluate_policy |
allow | Final decision after all rules evaluated |
deny_reasons | Array of reason strings from deny[msg] rules |
latency_us | Policy evaluation time in microseconds |
Phase 1: Hash-chained audit records
Every audit event is SHA-256 chained to the previous one, making the log tamper-evident. Any deletion or modification of a record breaks the chain and is detectable at verification time.
HashChain and AuditLogger
use coresdk_engine::audit::{AuditLogger, AuditEvent, HashChain, NoopDrain};
// Build a logger with a no-op drain (stdout by default in production)
let logger = AuditLogger::new(NoopDrain);
// Append returns the new chain head
let chain = HashChain::new();
let chain = chain.append(&event1)?;
let chain = chain.append(&event2)?;
// Verify the entire chain (e.g. on startup or during an audit)
chain.verify_chain(&[event1, event2])?;AuditEvent hash fields
Each AuditEvent carries three integrity fields in addition to the domain fields shown above:
| Field | Type | Description |
|---|---|---|
sequence_id | u64 | Monotonically increasing counter within the chain |
previous_hash | [u8; 32] | SHA-256 of the previous serialised event; zeros for the first event |
record_hash | [u8; 32] | SHA-256 of all fields of this event (including previous_hash) |
The chain is computed over the canonical JSON serialisation of all domain fields plus sequence_id and previous_hash. Altering any field — including reordering keys — invalidates record_hash and breaks verify_chain().
AuditDrain trait
Implement AuditDrain to write events to a custom backend:
use coresdk_engine::audit::{AuditDrain, AuditEvent, AuditError};
struct MyDrain { /* ... */ }
impl AuditDrain for MyDrain {
fn write(&self, event: &AuditEvent) -> Result<(), AuditError> {
// serialize and ship event to your backend
Ok(())
}
}
let logger = AuditLogger::new(MyDrain { /* ... */ });Use NoopDrain in tests to discard events without side effects.
Phase 2: Sink configuration
Configure a sink in your SDK init to route audit events to a durable store:
// Phase 2 — sink routing ships in Phase 2
use coresdk_engine::{Engine, EngineConfig, audit::AuditSink};
use std::time::Duration;
let engine = Engine::new(EngineConfig {
tenant_id: "acme".to_string(),
audit_sink: AuditSink::S3 {
bucket: "acme-audit-logs".to_string(),
prefix: "coresdk/".to_string(),
region: "us-east-1".to_string(),
flush_interval: Duration::from_secs(5),
},
..Default::default()
})?;# Phase 2 — sink routing ships in Phase 2
from coresdk import CoreSDKClient, SDKConfig
from coresdk.audit import AuditSink
_sdk = CoreSDKClient(SDKConfig(
sidecar_addr="http://127.0.0.1:7233",
tenant_id="acme",
service_name="my-api",
audit_sink=AuditSink.s3(
bucket="acme-audit-logs",
prefix="coresdk/",
region="us-east-1",
flush_interval=5,
),
))Supported sinks (Phase 2)
| Sink | Config key | Notes |
|---|---|---|
| Stdout (default) | AuditSink::Stdout | NDJSON to stdout |
| S3 / GCS | AuditSink::S3 | Batched, Parquet or NDJSON |
| BigQuery | AuditSink::BigQuery | Streaming inserts |
| Webhook | AuditSink::Webhook | HTTP POST per event or batched |
| Custom | AuditSink::Custom(fn) | Provide your own AuditDrain impl |
Phase 2: Streaming audit logs
For real-time SIEM integration, read the audit stream programmatically:
// Phase 2
use futures::StreamExt;
let mut stream = engine.audit().stream().await?;
while let Some(event) = stream.next().await {
let event = event?;
println!("{}", serde_json::to_string(&event)?);
siem_client.ingest(&event).await?;
}# Phase 2
async for event in _sdk.audit().stream():
print(event.model_dump_json())
await siem_client.ingest(event)Retention and compliance
- Audit events are immutable — CoreSDK never modifies or deletes a written event.
- Configure retention on your sink (e.g., S3 Object Lock, BigQuery table expiration).
- For SOC 2 / HIPAA workloads, enable
audit_include_input: truein your config to log the full policy input alongside each decision. This increases event size; scope it withaudit_input_fieldsto log only the fields you need.
Next steps
- Writing Rego Policies — understand what drives each allow/deny decision
- Attribute-Based Access Control — enrich audit events with resource and claim attributes
- Testing Policies — dry-run policies without writing audit events