# Audit Event Emission Guide This guide explains how to add automatic audit event emission to any StellaOps service using the shared `StellaOps.Audit.Emission` library. ## Overview The audit emission infrastructure provides: 1. **`AuditActionAttribute`** -- marks an endpoint for automatic audit event emission 2. **`AuditActionFilter`** -- ASP.NET Core endpoint filter that creates and sends `UnifiedAuditEvent` payloads 3. **`HttpAuditEventEmitter`** -- posts events to the Timeline service's `POST /api/v1/audit/ingest` endpoint 4. **`AddAuditEmission()`** -- single-line DI registration Events flow: **Service endpoint** -> `AuditActionFilter` -> `HttpAuditEventEmitter` -> **Timeline `/api/v1/audit/ingest`** -> `IngestAuditEventStore` -> merged into unified audit query results. ## Step 1: Add project reference In your service's `.csproj`, add a reference to the shared library: ```xml ``` Adjust the relative path as needed for your service's location in the repo. ## Step 2: Register in DI (Program.cs) Add a single line to your service's `Program.cs`: ```csharp using StellaOps.Audit.Emission; // After other service registrations: builder.Services.AddAuditEmission(builder.Configuration); ``` This registers: - `AuditActionFilter` (scoped endpoint filter) - `HttpAuditEventEmitter` as `IAuditEventEmitter` (singleton) - `AuditEmissionOptions` bound from configuration ## Step 3: Tag endpoints Add the `AuditActionFilter` and `AuditActionAttribute` metadata to any endpoint you want audited: ```csharp using StellaOps.Audit.Emission; app.MapPost("/api/v1/environments", CreateEnvironment) .AddEndpointFilter() .WithMetadata(new AuditActionAttribute("concelier", "create")); app.MapPut("/api/v1/environments/{id}", UpdateEnvironment) .AddEndpointFilter() .WithMetadata(new AuditActionAttribute("concelier", "update")); app.MapDelete("/api/v1/environments/{id}", DeleteEnvironment) .AddEndpointFilter() .WithMetadata(new AuditActionAttribute("concelier", "delete")); ``` ### Attribute parameters | Parameter | Required | Description | |---------------|----------|-------------| | `module` | Yes | Module name from `UnifiedAuditCatalog.Modules` (e.g., `"authority"`, `"policy"`, `"jobengine"`, `"vex"`, `"scanner"`, `"integrations"`) | | `action` | Yes | Action name from `UnifiedAuditCatalog.Actions` (e.g., `"create"`, `"update"`, `"delete"`, `"promote"`, `"approve"`) | | `ResourceType` | No | Optional resource type override. If omitted, inferred from the URL path segment after the version prefix. | ## Step 4: Configuration (optional) The emitter reads configuration from the `AuditEmission` section or environment variables: ```json { "AuditEmission": { "TimelineBaseUrl": "http://timeline.stella-ops.local", "Enabled": true, "TimeoutSeconds": 3 } } ``` Environment variable overrides: - `STELLAOPS_TIMELINE_URL` -- overrides `TimelineBaseUrl` - `AuditEmission__Enabled` -- set to `false` to disable emission - `AuditEmission__TimeoutSeconds` -- HTTP timeout for ingest calls ## How the filter works 1. The endpoint executes normally and returns its result to the caller. 2. After execution, the filter checks for `AuditActionAttribute` metadata. 3. If present, it builds an `AuditEventPayload` containing: - **Module** and **Action** from the attribute - **Actor** from `HttpContext.User` claims (`sub`, `name`, `email`, `stellaops:tenant`) - **Resource** from route parameters (first matching `id`, `resourceId`, etc., or first GUID value) - **Severity** inferred from HTTP response status code (2xx=info, 4xx=warning, 5xx=error) - **Description** auto-generated: `"{Action} {module} resource {resourceId}"` - **CorrelationId** from `X-Correlation-Id` header or `HttpContext.TraceIdentifier` 4. The event is posted asynchronously (fire-and-forget) to `POST /api/v1/audit/ingest`. 5. Failures are logged but never propagated -- audit emission must not affect the endpoint response. ## Timeline ingest endpoint The Timeline service exposes: ``` POST /api/v1/audit/ingest ``` - **Auth**: Requires `timeline:write` scope - **Body**: JSON matching the `AuditEventPayload` schema (camelCase) - **Response**: `202 Accepted` with `{ "eventId": "...", "status": "accepted" }` - **Gateway route**: Already covered by the existing `/api/v1/audit(.*)` route in `router-gateway-local.json` Ingested events are stored in an in-memory ring buffer (max 10,000 events) and merged with the HTTP-polled events from other modules (JobEngine, Policy, EvidenceLocker, Notify) in the unified audit query results. ## Architecture decisions - **Fire-and-forget emission**: Audit events are sent asynchronously after the endpoint responds. This ensures zero latency impact on the audited endpoint. - **No compile-time dependency on Timeline**: The `AuditEventPayload` DTOs in the emission library are wire-compatible with `UnifiedAuditEvent` but live in a separate namespace, avoiding circular dependencies. - **In-memory ingest store**: For the alpha phase, ingested events are stored in memory. A future sprint will add Postgres persistence for the ingest store. - **Composite event provider**: The Timeline service merges HTTP-polled events with ingested events, so all audit data appears in a single unified stream. ## File locations | File | Path | |------|------| | Shared library | `src/__Libraries/StellaOps.Audit.Emission/` | | Attribute | `src/__Libraries/StellaOps.Audit.Emission/AuditActionAttribute.cs` | | Filter | `src/__Libraries/StellaOps.Audit.Emission/AuditActionFilter.cs` | | Emitter | `src/__Libraries/StellaOps.Audit.Emission/HttpAuditEventEmitter.cs` | | DI extension | `src/__Libraries/StellaOps.Audit.Emission/AuditEmissionServiceExtensions.cs` | | Ingest endpoint | `src/Timeline/StellaOps.Timeline.WebService/Endpoints/UnifiedAuditEndpoints.cs` | | Ingest store | `src/Timeline/StellaOps.Timeline.WebService/Audit/IngestAuditEventStore.cs` | | Composite provider | `src/Timeline/StellaOps.Timeline.WebService/Audit/CompositeUnifiedAuditEventProvider.cs` |