# Implementation Report: Spec 424 - Security Defaults Content-Backed Comparable Support ## Preflight - **Active spec**: `specs/424-security-defaults-content-backed-comparable-support/` - **Implementation start**: 2026-06-30 19:22:46 CEST - **Branch**: `424-security-defaults-content-backed-comparable-support` - **HEAD**: `c49784b3 feat: complete spec 423 security compliance readiness pack (#490)` - **Initial dirty state**: untracked active spec directory only. - **Activated skills**: `spec-kit-implementation-loop`, `pest-testing`, `.agent/workflows/spec-readiness-gate`, `.agent/repo-contracts/workspace-scope-safety`, `.agent/repo-contracts/rbac-action-safety`, `.agent/repo-contracts/operation-run-truth`, `.agent/repo-contracts/evidence-anchor-contract`, `.agent/repo-contracts/provider-freshness-semantics`, `.agent/repo-contracts/product-surface-gate`. - **Hard-gate stop conditions checked**: no unrelated dirty files; no completed-spec rewrite; no new schema/table/persisted enum/status family; no new route/navigation/dashboard/action/customer output; no restore/apply/certification/Review Pack/report/export scope; no direct HTTP, Graph SDK bypass, runtime docs fetch, or broad singleton framework; no `tenant_id` ownership path. Existing fresh-install seed alignment is allowed only for the already-identified one-row `securityDefaults` entry in `2026_06_26_000419_expand_tenant_configuration_workloads.php`. ## Completed-Spec Guardrail Specs 414, 415, 417, 418, 419, 420, 421, and 423 are read-only dependency context. No completed historical spec files are edited. ## Preflight Evidence - `securityDefaults` currently exists as a registry-only/out-of-scope TCM planning row in `ResourceTypeRegistry` and the fresh-install workload expansion migration. - `CoverageSourceContractResolver` has no `securityDefaults` mapping before this spec. - `config/graph_contracts.php` has no Security Defaults contract before this spec. - `CoverageIdentityStrategyRegistry` has no `securityDefaults` identity strategy before this spec. - `EntraComparablePayloadNormalizer` supports Conditional Access before this spec and not Security Defaults. - `GenericContentEvidenceCaptureService` already accepts singleton Graph payloads that contain an `id`, so no broad singleton capture framework is planned. - Official Microsoft Graph documentation confirms the Security Defaults read endpoint as `GET /policies/identitySecurityDefaultsEnforcementPolicy`, the resource fields `id`, `displayName`, `description`, and `isEnabled`, and least-privileged read permission `Policy.Read.All`. ## Implementation Summary - Added `securityDefaults` as an explicit Coverage v2 source contract in `apps/platform/config/graph_contracts.php` using `/policies/identitySecurityDefaultsEnforcementPolicy`, `graph_version = v1.0`, `response_shape = singleton`, safe select fields, singleton-safe volatile fields, and `Policy.Read.All`. - Added `securityDefaults` to `CoverageSourceContractResolver` explicit mappings; missing contract resource still blocks as `capture_blocked_missing_contract`. - Added request-local Graph version handling in `MicrosoftGraphClient::listPolicies()` so this contract calls the v1.0 endpoint without changing global Graph defaults or bypassing `GraphClientInterface`. - Updated `MicrosoftGraphClient`, Security Defaults capture, and `graph:contract:check` singleton probes so they use the contract-local v1.0 endpoint and do not send `$top` to `/policies/identitySecurityDefaultsEnforcementPolicy`; the endpoint remains constrained to the contract `$select`. - Promoted only the `securityDefaults` registry row to active `graph_v1_fallback` / `fallback_supported` / internal-only / non-restorable support in `ResourceTypeRegistry`, and aligned the one existing fresh-install migration seed row allowed by the amended plan. - Added the idempotent `tenant-configuration:sync-defaults` deploy command to run the existing resource-type and supported-scope default sync paths on already-migrated environments. - Added a bounded identity strategy requiring a stable Graph `id`; display-name-only and `sourceId`-only Security Defaults payloads remain `missing_external_id`. - Extended existing Entra typed helpers for Security Defaults normalization, critical enabled-state compare, and safe render summaries. No Blade, Filament Resource/Page/Widget, route, navigation, dashboard, action, export, report, Review Pack, restore, certify, or customer output was added. - Hardened `ClaimGuard` so Security Defaults-specific claims are treated as workload claims: selected internal/operator comparable/renderable wording is allowed, while certification, restore, customer-ready, full, 100 percent, M365 certified, and Review Pack wording is blocked. ## Source Contract And Capture Matrix | Area | Result | | --- | --- | | Contract key | `securityDefaults` | | Endpoint | `/policies/identitySecurityDefaultsEnforcementPolicy` | | Version | `v1.0` via contract-local `graph_version` | | Response shape | singleton; `GraphClientInterface` list calls, capture, and live contract probes use v1.0 and omit `$top` | | Permission proof | `Policy.Read.All` recorded as the least-privileged read permission | | Capture path | Existing `GenericContentEvidenceCaptureService`; singleton payloads with `id` are already captured without new framework code | | Missing contract | Blocks as `capture_blocked_missing_contract` / `missing_graph_contract_resource`; no fake evidence | | Permission block | 403 response maps to `capture_blocked_permission`; no fake evidence | | Scope safety | Provider connection and OperationRun target-scope mismatches fail before provider work | | Registry posture | one active `graph_v1_fallback` row; old TCM planning row is inactive after `ResourceTypeRegistry::syncDefaults()` | | Fresh-install seed | Existing migration `2026_06_26_000419_expand_tenant_configuration_workloads.php` aligned for only the `securityDefaults` row | | Existing DB sync | run `cd apps/platform && php artisan tenant-configuration:sync-defaults` after deployment and before Security Defaults capture | ## Identity, Normalize, Compare, And Render Matrix | Area | Result | | --- | --- | | Identity | `graph.security_defaults.v1`, stable `graph_object_id` only when `id` is present | | Missing identity | display-name-only and `sourceId`-only payloads resolve to `missing_external_id`; no stable claim | | Normalized fields | `display_name`, `enabled`, `enabled_state`, `source_identity.id`, diagnostics | | Volatile fields | `@odata.context` and `@odata.etag` ignored as volatile | | Compare | enabled and enabled-state changes are critical material changes; redacted/unsupported diagnostics are informational | | Render | existing Coverage v2 inspector shows Security Defaults display name, enabled state, evidence state, identity state, claim state, last captured, compare summary, and redaction diagnostics | | Read model | Existing Entra dispatch path supports Security Defaults; no new generic registry/framework | ## Claim Guard, Redaction, And Scope Proof - Claim Guard allows only selected internal/operator Security Defaults comparable/renderable/ready-for-operator-review wording. - Claim Guard blocks Security Defaults certified, restore-ready, customer-ready, Review Pack, all/full, 100 percent, and M365 certification wording. - `CoveragePayloadRedactor` was reused; no redactor extension was required. Tests prove secret values are redacted from normalized payloads, render summaries, and compare results. - Existing Coverage v2 owner/workspace/managed-environment/provider connection scoping remains authoritative; no `tenant_id` ownership path was added. - `SupportedScopeResolver` was not changed. No certified, restore-ready, customer-ready, full Entra, or broad M365 scope names were added. ## Product Surface Close-Out - **No-legacy posture**: no legacy path or compatibility exception; this is a canonical Coverage v2 extension. - **Product Surface Impact**: existing Coverage v2 readiness/inspect surface can render Security Defaults summaries when renderable content-backed evidence exists. - **UI Surface Impact**: no route, navigation item, dashboard, action, table, Filament Resource/Page/Widget, provider registration, or Blade file changed. Existing inspector content changes only for Security Defaults renderable evidence. - **Page archetype**: Technical Annex / internal operator evidence inspection. - **Surface budgets**: decision-first status remains Coverage/Evidence/Identity/Claim badges; Security Defaults summary adds compact `summary_fields`; diagnostics remain secondary; raw/support details remain collapsed under existing technical details. - **Technical Annex / deep-link demotion**: source endpoint, contract key, schema hash, canonical key, operation link, and source class remain in existing technical details, not default-visible summary fields. - **Canonical status vocabulary**: existing Coverage v2 states only (`renderable`, `content_backed`, `stable`, `internal_only`, blockers where applicable). - **Product Surface exceptions**: none. - **Focused browser proof**: `php artisan test --compact tests/Browser/Spec424SecurityDefaultsComparableRenderableOperatorSurfaceSmokeTest.php` passed, proving rendered Security Defaults inspect output, compare summary, redaction badges, no raw/secrets/source endpoint default exposure, no restore/certified/customer-ready wording, no new high-impact action, no remote Graph/TCM/provider resource calls, and no JavaScript/console errors. - **Human Product Sanity result**: pass. An internal operator can see that Security Defaults is enabled and materially changed versus prior evidence without seeing raw payloads, secrets, source endpoints, or overclaim wording. - **Visible complexity outcome**: neutral. The existing inspector hierarchy is reused; no nested cards, new actions, new navigation, or new page structure. ## Filament v5 Output Contract - **Livewire v4.0+ compliance**: unchanged; platform remains Filament v5 on Livewire v4 and the focused browser smoke exercises the existing Filament surface. - **Provider registration location**: unchanged; Laravel 12 provider registration remains in `apps/platform/bootstrap/providers.php`. - **Global search**: unchanged; no Filament Resource was added or made globally searchable. - **Destructive/high-impact actions**: none added or changed. No restore/apply/capture-start/export/certify action was introduced. - **Asset strategy**: no assets registered and no new frontend bundle or `filament:assets` deployment requirement introduced. - **Testing plan coverage**: unit tests cover contract, graph version URL, singleton no-`$top` probes, identity, normalization, compare, render, and claims; feature tests cover registry, deploy sync command, capture, permission/scope/RBAC/no-remote/no-mini-platform; browser smoke covers the existing rendered inspector. ## Validation - Sail attempts: - `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec424` -> interrupted after about two minutes with no output because Sail/Docker exec did not progress in this session. - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration/Spec424SecurityDefaultsSourceContractTest.php tests/Unit/Support/TenantConfiguration/Spec424SecurityDefaultsTypedSemanticsTest.php tests/Feature/TenantConfiguration/Spec424SecurityDefaultsCaptureReadinessTest.php` -> interrupted after about one minute with no output for the same reason. - Local fallback validation: - `cd apps/platform && php artisan list --raw | rg '^tenant-configuration:sync-defaults'` -> passed; command is registered. - `cd apps/platform && php artisan test --compact tests/Unit/Support/TenantConfiguration/Spec424SecurityDefaultsSourceContractTest.php tests/Unit/Support/TenantConfiguration/Spec424SecurityDefaultsTypedSemanticsTest.php tests/Feature/TenantConfiguration/Spec424SecurityDefaultsCaptureReadinessTest.php` -> passed, 32 tests / 206 assertions. - `cd apps/platform && php artisan test --compact tests/Browser/Spec424SecurityDefaultsComparableRenderableOperatorSurfaceSmokeTest.php` -> passed, 1 test / 46 assertions. - `cd apps/platform && php artisan test --compact tests/Feature/TenantConfiguration/Spec421EntraCoverageLevelPromotionTest.php tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php tests/Unit/Support/TenantConfiguration/Spec419M365WorkloadRegistryTest.php` -> passed, 11 tests / 814 assertions, 1 skipped. - `cd apps/platform && php artisan test --compact --filter=ClaimGuard` -> passed, 110 tests / 123 assertions. - `cd apps/platform && php artisan test --compact tests/Feature/TenantConfiguration/Spec420M365GenericEvidenceCaptureTest.php tests/Feature/TenantConfiguration/Spec421EntraComparableRenderableTest.php tests/Unit/Support/TenantConfiguration/Spec421EntraConditionalAccessNormalizerTest.php tests/Unit/Support/TenantConfiguration/Spec421EntraComparableDiffTest.php tests/Unit/Support/TenantConfiguration/Spec421EntraRenderableSummaryTest.php` -> passed, 14 tests / 120 assertions. - `cd apps/platform && php artisan test --compact tests/Feature/TenantConfiguration/Spec420M365NoLegacyTest.php tests/Feature/TenantConfiguration/Spec415NoLegacyNoUiActivationTest.php tests/Feature/TenantConfiguration/TenantConfigurationKernelSchemaTest.php` -> passed, 5 tests / 41 assertions, 1 skipped. - `cd apps/platform && ./vendor/bin/pint --dirty --format agent` -> passed and formatted `apps/platform/app/Services/TenantConfiguration/GenericContentEvidenceCaptureService.php`. - `git diff --check` -> passed. ## Deployment Impact - **Schema migrations**: none added. - **Migration seed alignment**: one existing fresh-install seed row was aligned for `securityDefaults` only, as allowed by the amended plan. No other existing migration seed was changed. - **Existing databases**: already-migrated staging/production databases must run `cd apps/platform && php artisan tenant-configuration:sync-defaults` after deployment and before Security Defaults capture is attempted. The command is idempotent, syncs Coverage v2 resource-type and supported-scope defaults, and deactivates any stale active Security Defaults TCM planning row. - **Environment variables**: none. - **Queues / scheduler / workers**: no new jobs, queues, scheduler entries, or worker requirements. Existing capture queue behavior applies. - **Storage / volumes**: none. - **Runtime assets**: none. - **Provider registration**: none. - **External services**: no new live Microsoft Graph, TCM, provider, Microsoft docs, or remote network calls in render/compare paths. Capture uses the existing provider gateway and Graph client abstraction. - **Staging / production**: validate registry sync/default state and the focused Coverage v2 inspect path in Staging before Production promotion. ## Residual Risks / Follow-Up Candidates - This spec supports only `securityDefaults`. Other Entra types remain deferred. - This is internal/operator content-backed comparable/renderable support only. It is not restore readiness, certification, legal/regulatory attestation, customer proof, Review Pack output, or full Entra/M365 coverage. - Existing long-lived environments need the documented `tenant-configuration:sync-defaults` deployment step before Security Defaults capture; no schema migration handles that automatically.