# 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` |