Skip to main content
CoreSDK
Authorization & Policy

Audit Log

Immutable record of every authorization and policy decision made by CoreSDK.

Audit Log

Phase note. The coresdk-audit crate and sink configuration ship in Phase 2. The hash-chained AuditLogger and AuditDrain trait are available in Phase 1 as part of the coresdk-engine crate. 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"
}
FieldDescription
tsRFC 3339 timestamp with millisecond precision
trace_idULID — sortable, unique, correlates with your APM traces
tenant_idResolved tenant at decision time
actionThe action string passed to evaluate_policy
resourceResource identifier passed to evaluate_policy
allowFinal decision after all rules evaluated
deny_reasonsArray of reason strings from deny[msg] rules
latency_usPolicy 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:

FieldTypeDescription
sequence_idu64Monotonically 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)

SinkConfig keyNotes
Stdout (default)AuditSink::StdoutNDJSON to stdout
S3 / GCSAuditSink::S3Batched, Parquet or NDJSON
BigQueryAuditSink::BigQueryStreaming inserts
WebhookAuditSink::WebhookHTTP POST per event or batched
CustomAuditSink::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: true in your config to log the full policy input alongside each decision. This increases event size; scope it with audit_input_fields to log only the fields you need.

Next steps

On this page