trustgraph/docs/tech-specs/audit-events.md
cybermaggedon d5f3b6d9f6
feat: add structured audit event system (#1027)
Add a complete audit event pipeline that emits structured, machine-
parseable events for every gateway request and IAM decision.

Schema and publisher:
- AuditEvent dataclass and notify-class queue (audit_events_queue)
- AuditPublisher utility: fire-and-forget emission with envelope
  (schema_version, event_id, event_type, timestamp, producer)
- New request_id and client_ip fields on IamRequest for correlation

Gateway (gateway.request events):
- aiohttp middleware assigns request_id, captures timing/status/sizes
  and emits an event after every HTTP request completes
- IamAuth.authenticate annotates the request with identity
- Main endpoint handlers annotate capability and workspace
- request_id and client_ip forwarded to IAM on authenticate/authorise

IAM service (iam.authenticate, iam.authorise, iam.management events):
- Emits iam.authenticate for resolve-api-key, login, anonymous auth
- Emits iam.authorise for authorise and authorise-many decisions
- Emits iam.management for user/workspace/key mutations
- All events include request_id for correlation with gateway events

Design: events land on a pub/sub notify topic — non-persistent,
per-subscriber delivery. If no audit consumer is deployed, events
are silently discarded. Storage, retention, and alerting are
consumer-side concerns outside this boundary.

Added unit tests for the publisher and gateway middleware, unit
tests for IAM audit emission, and a contract test for the AuditEvent
schema.

Tech spec: docs/tech-specs/audit-events.md
2026-07-06 10:47:49 +01:00

14 KiB

layout title parent
default Audit Events Technical Specification Tech Specs

Audit Events Technical Specification

Overview

This specification defines the audit event system for TrustGraph. Audit events provide a structured, complete record of security- relevant operations: API gateway invocations and IAM decisions.

The design principle is: emit everything, let consumers decide. Audit events are cheap to produce (a pub/sub message per operation) and rich enough to support any downstream consumer — compliance dashboards, SIEM integration, anomaly detection, billing metering, or simple grep-based debugging. This spec covers event production only. Storage, retention, alerting, and presentation are deployment-specific concerns handled by consumers outside this boundary.

Motivation

TrustGraph currently has operational logging (Python logging to stdout/Loki) but no structured audit trail. Operational logs are unstructured, filtered by level, and designed for debugging — not for answering "who did what, when, and was it allowed?"

Enterprise deployments need:

  • Compliance evidence — demonstrable record of access for auditors.
  • Incident investigation — reconstruct what happened around a security event.
  • Anomaly detection — feed structured events into monitoring systems.
  • Accountability — attribute actions to identities across workspaces.

The current logging infrastructure cannot serve these needs because it is unstructured, inconsistently formatted, and interleaves debug noise with security-relevant signals.

Design Principles

  • Complete. Every gateway request and every IAM decision emits an event. No sampling, no level-gating. The pub/sub cost is negligible; consumers filter what they need.

  • Structured. Events are typed, versioned, machine-parseable JSON objects with a fixed envelope and operation-specific payloads. No free-text messages.

  • Cheap to produce. Events land on a pub/sub topic. No synchronous writes, no blocking on consumer availability. If no consumer is subscribed, events are discarded by the broker — that is acceptable.

  • Rich. Events carry enough context to reconstruct the full security narrative without correlating against operational logs. Identity, workspace, capability, resource, outcome, timing, client metadata.

  • Immutable. Once emitted, an event is a fact. Consumers may filter, aggregate, or discard events, but never mutate them.

  • Decoupled. Producers (gateway, IAM service) have no knowledge of consumers. The topic is fire-and-forget. This keeps the critical path fast and allows diverse consumer deployments.

Architecture

Event transport

Audit events are published to a dedicated pub/sub topic, declared in the schema layer following the project's queue naming convention:

audit_events_queue = queue('audit-events', cls='notify')

This produces the queue identifier notify:tg:audit-events, which each backend maps to its native topic format (e.g. Pulsar maps notify to non-persistent://tg/notify/audit-events).

The notify class is the right fit: non-persistent, per-subscriber delivery, no competing-consumer semantics. Audit event production must never block the gateway or IAM service. Consumers that need durability persist events themselves on receipt.

A single topic carries all event types, distinguished by the event_type field in the envelope. This simplifies producer logic and allows consumers to subscribe once and filter client-side.

Producers

Two components emit audit events:

  1. API Gateway — emits a gateway.request event for every inbound HTTP/WebSocket request after the request completes (or fails).

  2. IAM Service — emits iam.authenticate and iam.authorise events for every authentication and authorisation decision.

Both producers emit asynchronously — the event is published after the response is sent (gateway) or after the decision is returned (IAM). Audit emission is never on the critical path.

Consumers

Not defined by this spec. Example consumers that deployments may wire up:

  • Append to an immutable log store (S3, Cassandra, ClickHouse).
  • Forward to a SIEM (Splunk, Elastic, Sentinel).
  • Aggregate for billing/metering.
  • Feed an anomaly detection model.
  • Write to stdout for development debugging.

Event Envelope

Every audit event shares a common envelope:

{
  "schema_version": 1,
  "event_id": "uuid-v4",
  "event_type": "gateway.request",
  "timestamp": "2026-07-05T14:23:01.123Z",
  "producer": "api-gateway",
  "payload": { ... }
}
Field Type Description
schema_version int Envelope schema version. Consumers must ignore events with versions they don't understand.
event_id string Globally unique event identifier (UUID v4).
event_type string Dot-separated event type from the vocabulary below.
timestamp string ISO 8601 UTC timestamp at event emission.
producer string Component identity that emitted the event.
payload object Event-type-specific structured data.

Event Types

gateway.request

Emitted by the API gateway for every completed request.

{
  "request_id": "uuid-v4",
  "method": "POST",
  "path": "/api/v1/flow/default/graph-rag",
  "capability": "graph-rag:query",
  "workspace": "production",
  "identity": "user:mark",
  "client_ip": "192.168.1.42",
  "user_agent": "trustgraph-cli/2.6.11",
  "status_code": 200,
  "outcome": "success",
  "duration_ms": 1423,
  "request_size_bytes": 256,
  "response_size_bytes": 4096,
  "parameters": {
    "collection": "default",
    "entity_limit": 50
  }
}
Field Type Description
request_id string Unique ID for this request, propagated to IAM events for correlation.
method string HTTP method.
path string Request path (no query string).
capability string The capability required for this endpoint (from the capability vocabulary).
workspace string Resolved workspace for this request.
identity string Authenticated identity handle, or "anonymous" if unauthenticated.
client_ip string Client IP address (may be from X-Forwarded-For).
user_agent string Client User-Agent header.
status_code int HTTP response status code.
outcome string One of success, denied, error, unauthenticated.
duration_ms int Request duration in milliseconds.
request_size_bytes int Request body size.
response_size_bytes int Response body size.
error string Error category. Present only when outcome is not success.
parameters object Operation-specific parameters extracted from the request (not the full body — only semantically relevant fields).

iam.authenticate

Emitted by the IAM service for every authentication attempt.

{
  "request_id": "uuid-v4",
  "credential_type": "api-key",
  "identity": "user:mark",
  "outcome": "success",
  "client_ip": "192.168.1.42",
  "key_id": "key-abc123"
}
Field Type Description
request_id string Correlates with the gateway request that triggered this authentication.
credential_type string One of api-key, jwt, login-password.
identity string Resolved identity on success, or "unknown" on failure.
outcome string One of success, failure.
failure_reason string Internal failure category (not exposed to clients): invalid-key, expired-jwt, bad-signature, user-disabled, unknown-user. Present only on failure.
client_ip string Forwarded from the gateway request.
key_id string API key identifier (not the secret). Present only on key-based auth.

Note: failure_reason is for the audit log only. The client response is always the same masked error per the IAM contract's security rule. The audit consumer sees the real reason; the attacker does not.

iam.authorise

Emitted by the IAM service for every authorisation decision.

{
  "request_id": "uuid-v4",
  "identity": "user:mark",
  "capability": "graph-rag:query",
  "workspace": "production",
  "resource": "flow:default",
  "outcome": "allow",
  "evaluated_roles": ["workspace-user"],
  "evaluation_time_us": 42
}
Field Type Description
request_id string Correlates with the gateway request.
identity string Identity being authorised.
capability string Capability being checked.
workspace string Workspace scope of the resource.
resource string Structured resource identifier.
outcome string One of allow, deny.
denial_reason string Why denied: no-matching-role, capability-not-in-role, workspace-not-accessible, user-disabled. Present only on denial.
evaluated_roles list of string Roles evaluated during the decision (OSS regime specific — other regimes may populate differently).
evaluation_time_us int Time to evaluate the decision in microseconds.

iam.management

Emitted by the IAM service for administrative mutations.

{
  "request_id": "uuid-v4",
  "actor": "user:admin",
  "operation": "create-user",
  "target_identity": "user:new-hire",
  "target_workspace": "engineering",
  "outcome": "success",
  "details": {
    "roles_assigned": ["workspace-user"]
  }
}
Field Type Description
request_id string Correlates with the gateway request.
actor string Identity performing the action.
operation string IAM operation name (create-user, delete-api-key, assign-role, create-workspace, etc.).
target_identity string Identity being acted upon. Present only when applicable.
target_workspace string Workspace being acted upon. Present only when applicable.
outcome string One of success, error.
details object Operation-specific details (roles assigned, key created, etc.).

Correlation

All events from a single gateway request share the same request_id. A typical request produces:

  1. One gateway.request event (after completion).
  2. One iam.authenticate event (credential validation).
  3. One or more iam.authorise events (capability checks).

Consumers can reconstruct the full request lifecycle by grouping on request_id.

Implementation

Gateway changes

The gateway emits gateway.request events. Implementation:

  • Assign a UUID request_id at request entry.
  • Pass request_id and client_ip to the IAM service in the IamRequest (new fields on the dataclass).
  • After the response is sent, publish the audit event to the audit topic. This is a non-blocking fire-and-forget publish.

IAM service changes

The IAM service emits iam.authenticate, iam.authorise, and iam.management events. Implementation:

  • Accept request_id and client_ip from the gateway on each IamRequest.
  • After each decision or mutation, publish the corresponding audit event. Non-blocking.

Schema additions

New queue declaration in trustgraph-base/trustgraph/schema/:

from trustgraph.schema.core.topic import queue

audit_events_queue = queue('audit-events', cls='notify')

New fields on IamRequest:

@dataclass
class IamRequest:
    ...
    request_id: str = ""
    client_ip: str = ""

These are informational — the IAM service does not act on them beyond echoing them into audit events.

Pub/sub producer

A lightweight audit publisher utility in trustgraph-base:

class AuditPublisher:
    def __init__(self, producer):
        self.producer = producer

    async def emit(self, event_type, payload):
        event = {
            "schema_version": 1,
            "event_id": str(uuid4()),
            "event_type": event_type,
            "timestamp": datetime.utcnow().isoformat() + "Z",
            "producer": self.component_name,
            "payload": payload,
        }
        await self.producer.send(json.dumps(event).encode())

The publisher is instantiated once per component and shared across request handlers.

What This Spec Does Not Cover

  • Storage. Where audit events are persisted, for how long, and in what format. Deployment-specific.
  • Alerting. What conditions trigger alerts. Consumer logic.
  • Retention policy. How long events are kept. Compliance- dependent.
  • UI. Audit log viewers, dashboards, search interfaces.
  • Filtering/routing. Topic partitioning, consumer-side filtering, event routing to different backends.
  • Redaction. PII handling in audit events (may be needed for GDPR — a future concern for enterprise consumers).

These are all consumer-side concerns. The value of this boundary is that producers remain simple and fast while consumers can be as sophisticated as the deployment requires.

Open Questions

  • Should WebSocket upgrade events emit separately from per-message events? Current proposal: one gateway.request per WebSocket session (on close), with duration_ms covering the full session. Per-message audit for long-lived sockets (e.g. streaming RAG) may be needed for metering but adds volume.

  • Should parameters in gateway.request be standardised per endpoint, or free-form? Standardised is more useful for consumers but requires maintenance as endpoints evolve.

  • Event ordering guarantees. Pub/sub does not guarantee ordering across partitions. Consumers that need strict ordering must sort by timestamp or request_id sequence.

References