Status: experimental
Issue: #671
Parent: #667
Schema: schemas/enterprise-audit-event-v0.1.schema.json
The Enterprise Audit Ledger is a compact security/audit evidence stream for privileged enterprise operations. It is deliberately separate from:
The Product Spine event source answers “what lifecycle transition happened?” for the run model. This ledger answers “which authenticated identities, authorization decision, scoped resource revision, and outcome were involved in a privileged enterprise operation?”
An audit event is evidence only. It does not authorize or execute the action it records.
The dependency-free implementation is
idkmesh.enterprise_audit.LocalEnterpriseAuditLedger.
Appending requires one AuditEventInput composed from:
AuthorizationDecision;ActorContext;ActorContext that is scoped to the target project;The recorder does not accept an arbitrary payload/metadata map. In particular,
it does not copy the authorization decision’s free-form explanatory
message. The retained reason is the stable decision code.
This deliberately shrinks the surface on which prompts, headers, tokens, credentials, or other secret material could accidentally become audit data.
Every event binds:
sequence;event_id (audit-000000000001, …);occurred_at_epoch;The event has no generic payload, headers, metadata, prompt, or
secret field.
The audit writer reuses the existing E3 authorization objects rather than creating a second policy engine.
The AuthorizationDecision supplies the requested tenant/project,
principal, action, resource identity, policy revision, effect, reason code and
approval evidence. The event stores a canonical SHA-256 digest of the complete
decision object.
The supplied actor must match the decision principal and identity revision.
The actor is not required to be authorized for the target scope. That is intentional: a denied cross-tenant access attempt is important audit evidence. The trusted service writing the audit record must be bound to the target tenant/project scope.
The local reference adapter uses a dedicated SQLite database with
PRAGMA user_version = 1.
The enterprise_audit_events table has:
INTEGER PRIMARY KEY AUTOINCREMENT sequence;Two SQLite triggers abort every UPDATE and DELETE. The public ledger
API has no update or delete method.
Appending runs under BEGIN IMMEDIATE, so concurrent writers serialize the
selection of the previous head, next sequence, hash-chain construction, and
insert.
The event digest is:
sha256(canonical-json(event-without-event_digest))
The canonical JSON rule is sorted keys, compact separators and UTF-8.
Each event includes the previous event’s digest, so replay verification checks:
This detects rewrite, reordering and interior deletion.
A hash chain by itself cannot prove that the final rows were not silently removed if the verifier has no prior knowledge of the old head.
For that reason the ledger exposes an AuditCheckpoint:
event_count;head_sequence;head_digest.verify(expected_checkpoint=...) compares the current ledger against that
saved checkpoint. A tail deletion then fails event-count, head-sequence and/or
head-digest verification.
A production deployment that needs truncation evidence must persist/checkpoint the head in a separately governed location (for example a durable archive, attestation service, or external SIEM). The local SQLite file cannot self-authenticate its own missing suffix.
AuditExportSink is the vendor-neutral export protocol:
write_event(event: Mapping[str, Any]) -> None
LocalEnterpriseAuditLedger.export_to_sink(...) requires an explicit
tenant_id, optionally narrows to one project_id, reads validated events in
sequence order and calls that boundary. Unscoped cross-tenant export is not a
convenience default. Export does not mutate ledger state.
JsonLinesAuditSink is the dependency-free reference sink. It emits one
canonical event per NDJSON line, suitable for:
Vendor-specific delivery, authentication, retries and acknowledgement semantics belong in adapters above this contract. A failed sink call cannot alter the ledger.
Each event records:
minimum_days;eligible_after_epoch.The helper retention_state(...) returns one of:
retain;eligible_for_expiry;legal_hold.A governed external legal-hold system may supply a
legal_hold_hook(event) -> bool.
This module intentionally does not implement deletion. Reaching retention eligibility is evidence that an external retention controller may consider the event for expiry; it is not deletion authority.
The contract is fixed-shape and stores identifiers/digests rather than arbitrary request data. Raw credential values, Authorization headers, API tokens, prompts and secret material must never be passed as identifiers.
No software can infer whether an opaque identifier string was actually copied from a secret. Operators/adapters therefore remain responsible for supplying only normalized identities, references, stable reason codes and digests. The ledger reduces the accidental-leak surface; it does not make a false claim that arbitrary strings can be classified perfectly.
The ledger fails closed when:
A denied actor may still be recorded because denial itself is security evidence.
Every event fixes the following authority ceiling:
{
"audit_only": true,
"authorizes_action": false,
"canonical_state_write": false,
"git_push": false,
"merge": false
}
A valid hash chain proves consistency of retained audit evidence relative to its checkpoint. It does not prove the underlying operation was correct, that all privileged operations were instrumented, or that an external compliance standard is satisfied.
This E4 ledger is a reusable evidence primitive for E8/E9 service integration. Future middleware can append events after authorization and operation outcomes without changing the E3 policy kernel.
Completeness of real production event coverage remains an integration concern:
the pure E3 authorize() function stays side-effect free and does not write
audit state itself.