16 KiB
16 KiB
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.phpapps/platform/app/Services/TenantConfiguration/CoverageSourceContractResolver.phpapps/platform/app/Services/TenantConfiguration/CoverageIdentityStrategyRegistry.phpapps/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.phpapps/platform/tests/Unit/Support/TenantConfiguration/Spec420M365CaptureEligibilityTest.phpapps/platform/tests/Unit/Support/TenantConfiguration/Spec420M365GenericPayloadNormalizerTest.phpapps/platform/tests/Unit/Support/TenantConfiguration/Spec420M365CaptureIdentityStrategyTest.phpapps/platform/tests/Unit/Support/TenantConfiguration/Spec420M365CaptureClaimGuardTest.phpapps/platform/tests/Unit/Support/TenantConfiguration/Spec420M365CaptureRedactionTest.phpapps/platform/tests/Feature/TenantConfiguration/Spec420M365GenericEvidenceCaptureTest.phpapps/platform/tests/Feature/TenantConfiguration/Spec420M365CaptureOperationRunTest.phpapps/platform/tests/Feature/TenantConfiguration/Spec420M365CaptureAuthorizationTest.phpapps/platform/tests/Feature/TenantConfiguration/Spec420M365ProviderConnectionScopeTest.phpapps/platform/tests/Feature/TenantConfiguration/Spec420M365NoOverclaimTest.phpapps/platform/tests/Feature/TenantConfiguration/Spec420M365NoLegacyTest.phpapps/platform/tests/Feature/TenantConfiguration/Spec420M365NoTenantIdTest.phpapps/platform/tests/Browser/Spec420M365GenericEvidenceOperatorSurfaceSmokeTest.php
- Spec artifacts:
specs/420-m365-generic-evidence-coverage-pack/tasks.mdspecs/420-m365-generic-evidence-coverage-pack/implementation-report.md
Implementation Summary
conditionalAccessPolicynow resolves through an explicitgraph_contracts.types.conditionalAccessPolicymapping in the existing generic capture resolver.- The selected missing-contract types
acceptedDomain,appPermissionPolicy, anddlpCompliancePolicyfail closed ascapture_blocked_missing_contractwithmissing_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, anddefault_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.
conditionalAccessPolicyuses a narrow identity strategy with Graph object ID stable keys.CanonicalIdentityResolverremains the only resolver path and now honors a strategy-levelstable_key_kindbefore falling back to source-class defaults.- Existing
CoverageResourceUpserter,CoverageEvidenceWriter,StartTenantConfigurationCapture,CaptureTenantConfigurationEvidenceJob, andOperationRunServiceare reused.GenericContentEvidenceCaptureServiceremains 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
TenantConfigurationResourceand one append-onlyTenantConfigurationResourceEvidencerow with raw payload, normalized payload, deterministic payload hash, redacted permission context, source metadata, andoperation_run_id. - Missing-contract selected types create no fake resources or evidence rows.
- OperationRun type remains
tenant_configuration.capture; notenant_configuration.m365_capturetype 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_onlyby 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_idownership 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:
Spec420M365GenericEvidenceOperatorSurfaceSmokeTestpassed. It loadsCoverageV2Readiness, 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:assetsis 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, anddlpCompliancePolicy; 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.