# Implementation Report: Spec 420 - M365 Generic Evidence Coverage Pack ## Status - Result: passed implementation loop; ready for manual review. - Active spec directory: `specs/420-m365-generic-evidence-coverage-pack`. - Branch: `420-m365-generic-evidence-coverage-pack`. - HEAD at implementation start/final validation: `52523980 feat: expand m365 tcm workload registry (#486)`. - Initial dirty state: active spec directory was untracked; no unrelated modified runtime files were present. - Final dirty state: expected active-spec, runtime, and test changes only. No unrelated work was modified. - Historical specs: Specs 414, 415, 417, 418, and 419 were used as read-only dependency context only. No closed historical spec was rewritten or stripped of validation history. ## Activated Skills And Gates - Activated skills: `spec-kit-implementation-loop`, `pest-testing`; repo gates applied for spec readiness, workspace scope safety, RBAC/action safety, operation-run truth, evidence anchor contract, Product Surface, TCM cutover guard, customer output, and browser read-only audit. - Hard-gate result: pass with conditions before code, then pass after validation. - Stop conditions: none hit. No compare/render/restore/certification/customer output, no UI start action, no dashboard, no endpoint guessing, no direct HTTP, no `tenant_id`, no v1 compatibility adapter, and no M365 mini-platform were introduced. - Analysis/fix iterations: two implementation loops; one browser assertion correction because the existing safe inspect modal intentionally does not render source endpoint, followed by a manual-review fix loop for redaction proof, RBAC role precision, and OperationRun browser-fixture summary keys. Final post-implementation analysis found no confirmed in-scope findings. - Merge Readiness Gate: passed, subject to human review. ## Changed Files - Runtime: - `apps/platform/app/Services/TenantConfiguration/GenericContentEvidenceCaptureService.php` - `apps/platform/app/Services/TenantConfiguration/CoverageSourceContractResolver.php` - `apps/platform/app/Services/TenantConfiguration/CoverageIdentityStrategyRegistry.php` - `apps/platform/app/Services/TenantConfiguration/CanonicalIdentityResolver.php` - Existing test update: - `apps/platform/tests/Unit/Support/TenantConfiguration/Spec417CoverageIdentityStrategyRegistryTest.php` - New Spec 420 tests: - `apps/platform/tests/Unit/Support/TenantConfiguration/Spec420M365CaptureSourceContractResolverTest.php` - `apps/platform/tests/Unit/Support/TenantConfiguration/Spec420M365CaptureEligibilityTest.php` - `apps/platform/tests/Unit/Support/TenantConfiguration/Spec420M365GenericPayloadNormalizerTest.php` - `apps/platform/tests/Unit/Support/TenantConfiguration/Spec420M365CaptureIdentityStrategyTest.php` - `apps/platform/tests/Unit/Support/TenantConfiguration/Spec420M365CaptureClaimGuardTest.php` - `apps/platform/tests/Unit/Support/TenantConfiguration/Spec420M365CaptureRedactionTest.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec420M365GenericEvidenceCaptureTest.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec420M365CaptureOperationRunTest.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec420M365CaptureAuthorizationTest.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec420M365ProviderConnectionScopeTest.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec420M365NoOverclaimTest.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec420M365NoLegacyTest.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec420M365NoTenantIdTest.php` - `apps/platform/tests/Browser/Spec420M365GenericEvidenceOperatorSurfaceSmokeTest.php` - Spec artifacts: - `specs/420-m365-generic-evidence-coverage-pack/tasks.md` - `specs/420-m365-generic-evidence-coverage-pack/implementation-report.md` ## Implementation Summary - `conditionalAccessPolicy` now resolves through an explicit `graph_contracts.types.conditionalAccessPolicy` mapping in the existing generic capture resolver. - The selected missing-contract types `acceptedDomain`, `appPermissionPolicy`, and `dlpCompliancePolicy` fail closed as `capture_blocked_missing_contract` with `missing_source_contract_mapping`. - Registry truth from Spec 419 is preserved: selected M365 types remain registry-seeded as `source_class = tcm`, `support_state = out_of_scope`, and `default_claim_state = internal_only`; runtime capture eligibility is narrow and contract-driven. - Source metadata now carries actual source contract key, endpoint, source version, source schema hash, registry source class, registry support state, and schema-hash availability where captured. - `conditionalAccessPolicy` uses a narrow identity strategy with Graph object ID stable keys. `CanonicalIdentityResolver` remains the only resolver path and now honors a strategy-level `stable_key_kind` before falling back to source-class defaults. - Existing `CoverageResourceUpserter`, `CoverageEvidenceWriter`, `StartTenantConfigurationCapture`, `CaptureTenantConfigurationEvidenceJob`, and `OperationRunService` are reused. `GenericContentEvidenceCaptureService` remains the capture path and now redacts normalized payloads before persistence/hashing while preserving the raw-payload evidence boundary. ## Capture Eligibility Matrix | Canonical type | Runtime result | Provider called | Evidence row | Notes | | --- | --- | --- | --- | --- | | `conditionalAccessPolicy` | `captured` when fake Graph payload exists | yes, through `GraphClientInterface::listPolicies()` | yes | Explicit repo-real source contract only; no alias or endpoint guessing. | | `acceptedDomain` | `capture_blocked_missing_contract` | no | no | Stable reason `missing_source_contract_mapping`. | | `appPermissionPolicy` | `capture_blocked_missing_contract` | no | no | Stable reason `missing_source_contract_mapping`. | | `dlpCompliancePolicy` | `capture_blocked_missing_contract` | no | no | Stable reason `missing_source_contract_mapping`; source aliases are ignored at runtime. | ## Source Contract Matrix | Canonical type | Contract key | Endpoint | Version/schema | Outcome | | --- | --- | --- | --- | --- | | `conditionalAccessPolicy` | `conditionalAccessPolicy` | `/identity/conditionalAccess/policies` | `v1.0`; schema hash captured when available, explicit unavailable state tested | Capture enabled. | | `acceptedDomain` | none | none | none | Missing contract blocker. | | `appPermissionPolicy` | none | none | none | Missing contract blocker. | | `dlpCompliancePolicy` | none | none | none | Missing contract blocker. | ## Evidence And OperationRun Proof - Captured Conditional Access evidence persists one `TenantConfigurationResource` and one append-only `TenantConfigurationResourceEvidence` row with raw payload, normalized payload, deterministic payload hash, redacted permission context, source metadata, and `operation_run_id`. - Missing-contract selected types create no fake resources or evidence rows. - OperationRun type remains `tenant_configuration.capture`; no `tenant_configuration.m365_capture` type was introduced. - OperationRun lifecycle transitions remain service-owned through `OperationRunService`. - Summary counts use existing whitelisted numeric keys. - Dedupe/retry behavior reuses the active-run check in `StartTenantConfigurationCapture`. - OperationRun has no separate message column in the current model; OperationRun context and failure summary do not contain raw payloads, provider response bodies, tokens, or secrets. ## RBAC And Scope Proof - Authorized owner and manager paths can start the selected capture. - Operator, readonly, and missing-capability paths are denied with 403 after membership is established. - Non-member and missing environment entitlement paths are denied without leaking environment data. - Cross-workspace/cross-environment provider connections are rejected before run creation. - Job execution revalidates workspace, managed environment, provider connection, OperationRun type, and target scope before provider work. ## Redaction, Claims, And Safety Proof - Secret-bearing provider payload tests prove raw payload remains inside the evidence raw-payload storage boundary, while normalized evidence payload, permission context, OperationRun context/failure summary, audit metadata, and logs do not persist or emit the secret values. - Broad M365, certified, restore-ready, customer-ready, complete-tenant, all-resource, and unscoped 100% claims remain blocked. - Captured Conditional Access evidence remains `internal_only` by default. - No customer output, report, export, publish, restore, certify, or Review Pack path was added. ## No-Legacy And Data Model Proof - No v1 adapter, fallback reader, dual-write path, old gap taxonomy, workload-specific table/model/engine/namespace/service, or mini-platform was introduced. - No `tenant_id` ownership truth was added to Coverage v2 tables or changed capture paths. - No migrations, constraints, indexes, enums/status families, operation types, tables, or persisted taxonomies were added. ## Product Surface Close-Out - No-legacy posture: no legacy UI/API compatibility path and no approved exception. - Product Surface Impact: data-impact only on the existing Coverage v2 Readiness surface when a captured M365 generic evidence row exists. - UI Surface Impact: no runtime UI files, routes, navigation entries, Filament resources/widgets/pages/actions, Blade views, Livewire components, labels, customer outputs, dashboards, or downloads were edited. - Page archetype: existing internal operator readiness/review page. - Surface budgets: unchanged; one existing table row and existing inspect slide-over render seeded data. - Technical Annex/deep-link demotion: unchanged; source endpoint is intentionally not rendered in the safe inspect modal. - Canonical status vocabulary: existing Coverage v2 labels only (`Content backed`, `Stable`, `Internal only`, existing capture outcome/status values). - Product Surface exceptions: none. - Human Product Sanity: pass. Existing page renders the Conditional Access row as internal generic evidence, shows no broad M365/certified/restore-ready/customer-ready claim, does not expose payload secrets, and adds no new high-impact UI action. - Visible complexity outcome: unchanged; no new surface controls or navigation. - Browser proof: `Spec420M365GenericEvidenceOperatorSurfaceSmokeTest` passed. It loads `CoverageV2Readiness`, verifies the row and inspect slide-over, checks Livewire availability, checks no Graph/TCM/provider network calls during render, checks no console/JS errors, and checks no new capture/restore/certify/export/download action on the main surface. ## Filament v5 Output Contract - Livewire v4.0+ compliance: no Livewire API changes; existing Filament v5/Livewire v4 surface is exercised by the browser test. - Provider registration location: no panel/provider registration changed. Laravel 12 provider registration remains under `apps/platform/bootstrap/providers.php`. - Global search: no Filament Resource global-search behavior changed; no new Resource was added. - Destructive/high-impact actions: no new Filament action or UI start action was added. Backend capture start remains capability-gated and OperationRun/audit backed; there is no new destructive action requiring a Filament confirmation modal. - Asset strategy: no assets registered and no frontend bundles changed. `php artisan filament:assets` is not newly required by this implementation beyond existing deployment practice. - Testing plan: page/data impact covered by browser smoke; resolver/capture/evidence/identity/RBAC/scope/OperationRun/no-overclaim/no-legacy/no-tenant-id covered by Pest unit and feature tests. ## Validation Commands - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - passed. - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration/Spec420M365CaptureSourceContractResolverTest.php tests/Unit/Support/TenantConfiguration/Spec420M365CaptureEligibilityTest.php tests/Unit/Support/TenantConfiguration/Spec420M365GenericPayloadNormalizerTest.php tests/Unit/Support/TenantConfiguration/Spec420M365CaptureIdentityStrategyTest.php tests/Unit/Support/TenantConfiguration/Spec420M365CaptureClaimGuardTest.php tests/Unit/Support/TenantConfiguration/Spec420M365CaptureRedactionTest.php` - passed in latest combined focused run. - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantConfiguration/Spec420M365GenericEvidenceCaptureTest.php tests/Feature/TenantConfiguration/Spec420M365CaptureOperationRunTest.php tests/Feature/TenantConfiguration/Spec420M365CaptureAuthorizationTest.php tests/Feature/TenantConfiguration/Spec420M365ProviderConnectionScopeTest.php tests/Feature/TenantConfiguration/Spec420M365NoOverclaimTest.php tests/Feature/TenantConfiguration/Spec420M365NoLegacyTest.php tests/Feature/TenantConfiguration/Spec420M365NoTenantIdTest.php` - passed in latest combined focused run. - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration/Spec420M365CaptureSourceContractResolverTest.php tests/Unit/Support/TenantConfiguration/Spec420M365CaptureEligibilityTest.php tests/Unit/Support/TenantConfiguration/Spec420M365GenericPayloadNormalizerTest.php tests/Unit/Support/TenantConfiguration/Spec420M365CaptureIdentityStrategyTest.php tests/Unit/Support/TenantConfiguration/Spec420M365CaptureClaimGuardTest.php tests/Unit/Support/TenantConfiguration/Spec420M365CaptureRedactionTest.php tests/Feature/TenantConfiguration/Spec420M365GenericEvidenceCaptureTest.php tests/Feature/TenantConfiguration/Spec420M365CaptureOperationRunTest.php tests/Feature/TenantConfiguration/Spec420M365CaptureAuthorizationTest.php tests/Feature/TenantConfiguration/Spec420M365ProviderConnectionScopeTest.php tests/Feature/TenantConfiguration/Spec420M365NoOverclaimTest.php tests/Feature/TenantConfiguration/Spec420M365NoLegacyTest.php tests/Feature/TenantConfiguration/Spec420M365NoTenantIdTest.php` - 35 passed, 215 assertions. - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec420M365GenericEvidenceOperatorSurfaceSmokeTest.php` - 1 passed, 43 assertions. - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration/Spec415CoverageSourceContractResolverTest.php tests/Unit/Support/TenantConfiguration/Spec417CanonicalIdentityResolverTest.php tests/Unit/Support/TenantConfiguration/Spec417CoverageIdentityStrategyRegistryTest.php tests/Unit/Support/TenantConfiguration/Spec419M365WorkloadRegistryTest.php tests/Feature/TenantConfiguration/Spec415GenericContentBackedCaptureTest.php tests/Feature/TenantConfiguration/Spec417CanonicalIdentityPersistenceTest.php tests/Feature/TenantConfiguration/Spec417CoverageResourceIdentityUpsertTest.php tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php` - 31 passed, 2 skipped, 975 assertions. - `git diff --check` - passed. ## Deployment Impact - Migrations: none. - Environment variables: none. - Queue/cron: existing tenant-configuration capture queue path still applies; no new worker type. - Storage/volumes: none. - Assets: none. - Dokploy/staging: deploy as ordinary app code/test change; validate on Staging before Production per repo release rules. No additional rollback step beyond reverting code. - PostgreSQL lane: N/A because no schema/check constraint/index changed. ## Deferred Work And Residual Risk - Deferred by spec: explicit source contracts for `acceptedDomain`, `appPermissionPolicy`, and `dlpCompliancePolicy`; compare/render/restore/certification/customer output; broader M365 dashboard or support claims. - Residual risk: this proves a fake-provider generic capture path for the selected first pack, not real Microsoft Graph production coverage for all M365 resources. - Remaining in-scope findings: none.