# Exception Application Audit Trail (policy.exception_applications) ## Module Policy ## Status IMPLEMENTED ## Description Records every instance of an exception being applied to a finding in a dedicated `policy.exception_applications` table, capturing exception ID, finding context, original and applied status, purl, vulnerability ID, and evaluation run ID. Exposed via ledger export for compliance. ## Implementation Details - **ExceptionApplication Model**: `src/Policy/__Libraries/StellaOps.Policy.Exceptions/Models/ExceptionApplication.cs` - Sealed record with fields: Id (Guid), TenantId, ExceptionId, FindingId, VulnerabilityId, OriginalStatus, AppliedStatus, EffectName, EffectType, EvaluationRunId, PolicyBundleDigest, AppliedAt, Metadata - `Create()` static factory method enforces non-null ExceptionId/FindingId, accepts deterministic applicationId and appliedAt timestamps - Metadata stored as `ImmutableDictionary` for extensibility - **IExceptionApplicationRepository**: `src/Policy/__Libraries/StellaOps.Policy.Exceptions/Repositories/IExceptionApplicationRepository.cs` - `RecordAsync(application)` -- persists a single application record - `RecordBatchAsync(applications)` -- bulk persist for batch evaluation - Query by ExceptionId, FindingId, VulnerabilityId, EvaluationRunId, TimeRange - `GetStatisticsAsync(tenantId, filter?)` returns `ExceptionApplicationStatistics` (TotalApplications, UniqueExceptions, UniqueFindings, UniqueVulnerabilities, ByEffectType counts, ByAppliedStatus counts, EarliestApplication, LatestApplication) - `CountAsync(tenantId, filter?)` for total count with optional filter - `ExceptionApplicationFilter` record supports paging (Limit/Offset), date range (FromDate/ToDate), and field filters - **PostgresExceptionApplicationRepository**: `src/Policy/__Libraries/StellaOps.Policy.Exceptions/Repositories/PostgresExceptionApplicationRepository.cs` -- Postgres persistence for the `policy.exception_applications` table - **ExceptionEvaluator**: `src/Policy/__Libraries/StellaOps.Policy.Exceptions/Services/ExceptionEvaluator.cs` -- creates ExceptionApplication records when exceptions match findings during policy evaluation ## E2E Test Plan - [ ] Apply exception to finding; query `GetByExceptionIdAsync(tenantId, exceptionId)`; verify record includes correct ExceptionId, FindingId, OriginalStatus, AppliedStatus, EffectName, EffectType - [ ] Apply exception with VulnerabilityId; query `GetByVulnerabilityIdAsync(tenantId, vulnId)`; verify record returned with correct VulnerabilityId - [ ] Apply exception during batch evaluation; verify EvaluationRunId is populated; query `GetByEvaluationRunIdAsync(tenantId, runId)` and verify all applications for that run - [ ] Apply exception; verify AppliedAt timestamp matches evaluation time (deterministic) - [ ] Apply exception with PolicyBundleDigest; verify digest is recorded in the application record - [ ] Call `RecordBatchAsync` with 5 applications; verify all 5 are persisted - [ ] Call `GetByTimeRangeAsync(tenantId, from, to)` with a range encompassing 3 applications; verify exactly 3 returned - [ ] Call `GetStatisticsAsync(tenantId)` after 10 applications across 3 exceptions and 5 findings; verify TotalApplications=10, UniqueExceptions=3, UniqueFindings=5, ByEffectType counts sum to 10 - [ ] Call `CountAsync(tenantId, filter)` with EffectType="suppress" filter; verify count matches expected