# Code Generation Summary — U3 HTTP Security Headers & CSP **Generated**: 2026-07-28 **Verified**: `dotnet build SlpModularCms.sln -c Release` → **0 errors**; `dotnet test` → **315 passed, 0 failed** (baseline 253, so **+62**) --- ## Files Created | File | Purpose | |---|---| | `Core/Hosting/Security/SecurityHeadersOptions.cs` | `SecurityHeadersOptions` + `PathPolicyRule`. Defaults **are** the production values | | `Core/Hosting/Security/CspPolicyCatalog.cs` | `SecurityHeaderSet` record + the two policy definitions as a pure function | | `Core/Hosting/Security/CspPolicyProvider.cs` | `ICspPolicyProvider` + `FrozenDictionary`-backed implementation | | `Core/Hosting/Security/PathPolicyResolver.cs` | Ordered, segment-aware prefix matching | | `Core/Hosting/Security/SecurityHeaderWriter.cs` | **All decision logic**, static and dependency-free | | `Core/Hosting/Security/SecurityHeadersMiddleware.cs` | Glue: gate, resolve, register the response-start callback | | `Core/Hosting/Security/SecurityHeadersExtensions.cs` | `AddCmsSecurityHeaders` / `UseCmsSecurityHeaders` + startup logging | ## Test Files Created | File | Tests | Covers | |---|---|---| | `Core.Tests/Hosting/Security/SecurityHeaderWriterTests.cs` | 20 | Per-header scoping, HSTS gating, never-overwrite, content-type parsing | | `Core.Tests/Hosting/Security/CspPolicyCatalogTests.cs` | 24 | Directive content, `script-src 'self'` under Strict, origin injection, unknown policy | | `Core.Tests/Hosting/Security/PathPolicyResolverTests.cs` | 15 | Segment matching, `/administrator` vs `/admin`, first-match-wins, normalisation | | `Core.Tests/Hosting/Security/SecurityHeadersOptionsValidationTests.cs` | 15 | `ValidateOnStart` through a real container | ## Files Modified | File | Change | |---|---| | `Api/Program.cs` | `AddCmsSecurityHeaders`; `UseCmsSecurityHeaders` first inside `UseExceptionHandler` | | `Api.Slave/Program.cs` | Same. The slave serves `/api/v1` and `/health` and is reached during diagnosis | | `Api/appsettings.json` | `SecurityHeaders` section — three strict prefixes | | `Api.Slave/appsettings.json` | `SecurityHeaders` section — two strict prefixes (no `/admin` there) | --- ## Business Rule Coverage | Rule | Where | Test | |---|---|---| | BR-U3-01 `nosniff` on every response | `SecurityHeaderWriter.Apply` | `Apply_ShouldWriteNosniff_ForAScriptFile` | | BR-U3-02 HSTS except in Development | `sendHsts` parameter | `Apply_ShouldSkipHsts_WhenSendHstsIsFalse` | | BR-U3-03 CSP/Frame/Referrer on HTML only | `IsHtml` early return | `Apply_ShouldWriteOnlyAlwaysHeaders_ForNonHtmlResponses` | | BR-U3-04 never overwrite | `TryAdd` | `Apply_ShouldNotOverwrite_HeadersAlreadyPresent` | | BR-U3-05 write at response start | `Response.OnStarting` | Carried to Build and Test | | BR-U3-06 before static files | `Program.cs` position | Carried to Build and Test | | BR-U3-07 never throw per request | catch-and-log in the callback | Carried to Build and Test | | BR-U3-08 all but HSTS in Development | `sendHsts: false` path | `Apply_ShouldSkipHsts_WhenSendHstsIsFalse` | | BR-U3-09 headers on error responses | inside `UseExceptionHandler` | Carried to Build and Test | | BR-U3-10…12 two policies, config mapping | `CspPolicyCatalog`, `PathPolicyResolver` | Catalog + resolver suites | | BR-U3-13 `script-src 'self'` under Strict | `CspPolicyCatalog.Build` | `Strict_ShouldNotAllowInlineOrEvalScript` | | BR-U3-14 `style-src 'unsafe-inline'` | `CspPolicyCatalog.Build` | `Strict_ShouldAllowInlineStyle_ButOnlyStyle` | | BR-U3-15, BR-U3-16 relaxed is enforcing, no external script | `CspPolicyCatalog.Build` | `Relaxed_ShouldNotPermitExternalScriptOrigins_ByDefault` | | BR-U3-17 `object-src`/`base-uri` | `CspPolicyCatalog.Build` | `BothPolicies_ShouldBlockPluginsAndBaseTagInjection` | | BR-U3-18 composed once | `FrozenDictionary` in the constructor | `Provider_ShouldComposeBothPolicies_WithNoOriginsConfigured` | | BR-U3-19 frame options per policy | `CspPolicyCatalog.Build` | `Strict_ShouldDenyFramingEntirely`, `Relaxed_ShouldAllowSameOriginFraming_…` | | BR-U3-20 unknown name fatal | `ValidateOnStart` | `Validation_ShouldFail_ForAnUnknownPolicyNameInPathPolicies` | | BR-U3-21 empty origin lists valid | `Join` returns the base list | `Validation_ShouldPass_WithNoConfigurationAtAll` | | BR-U3-22 Umami origin warning | **Withdrawn — REF-U3-01**, moved to the U5 CI gate | n/a | | BR-U3-23 log permitted origins | `UseCmsSecurityHeaders` | Carried to Build and Test | | BR-U3-24 disable logs a warning | `UseCmsSecurityHeaders` | Carried to Build and Test | --- ## Deviations and Additions **One rule withdrawn, one check added.** REF-U3-01 (raised at NFR Design): BR-U3-22's Umami-origin startup warning is not implementable, because the backend never sees `VITE_UMAMI_WEBSITE_ID`. Replaced by a blocking U5 CI gate. Nothing in the generated code attempts it. **Origin format validation added beyond the functional design.** `AllowedScriptOrigins` and `AllowedConnectOrigins` are validated at startup to be scheme-and-host without a path. A CSP source list silently ignores a malformed source, so `https://analytics.example.com/script.js` in configuration would produce a policy that looks configured and blocks the script anyway. Caught at startup instead. --- ## Carried to Phase-Level Build and Test Everything below needs a real response feature or a running host — `DefaultHttpContext.Response.OnStarting` is a no-op, so none of it can be asserted in a unit test without asserting the wrong thing: | Behaviour | Why it matters | |---|---| | Headers present on a **static website asset** | Verifies the registration sits before `UseCmsStaticContent`. A misordered registration compiles, starts, passes all 315 tests and serves the entire website with no CSP | | Headers present on the availability gate's **503** and on an unhandled-exception **ProblemDetails** | Verifies the position inside `UseExceptionHandler` (BR-U3-09) | | `/admin/dashboard` gets `DENY`, `/` gets `SAMEORIGIN` | End-to-end policy selection | | `/admin/assets/*.js` gets only `nosniff` + HSTS | Content-type scoping against real static-file responses | | Startup log lines for permitted origins and for `Enabled: false` | BR-U3-23, BR-U3-24 | | Both hosts start with the new section present | `ValidateOnStart` against the committed `appsettings.json` |