diff --git a/specs/044-drift-mvp/checklists/requirements.md b/specs/044-drift-mvp/checklists/requirements.md new file mode 100644 index 0000000..5e432f1 --- /dev/null +++ b/specs/044-drift-mvp/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: Drift MVP (044) + +**Purpose**: Validate specification completeness and quality before proceeding to implementation +**Created**: 2026-01-12 +**Feature**: [specs/044-drift-mvp/spec.md](../spec.md) + +## Content Quality + +- [ ] No implementation details (languages, frameworks, APIs) +- [ ] Focused on user value and business needs +- [ ] Written for non-technical stakeholders +- [ ] All mandatory sections completed + +## Requirement Completeness + +- [ ] No [NEEDS CLARIFICATION] markers remain +- [ ] Requirements are testable and unambiguous +- [ ] Success criteria are measurable +- [ ] Success criteria are technology-agnostic (no implementation details) +- [ ] All acceptance scenarios are defined +- [ ] Edge cases are identified +- [ ] Scope is clearly bounded +- [ ] Dependencies and assumptions identified + +## Feature Readiness + +- [ ] All functional requirements have clear acceptance criteria +- [ ] User scenarios cover primary flows +- [ ] Feature meets measurable outcomes defined in Success Criteria +- [ ] No implementation details leak into specification + +## Notes + +- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`. +- Constitution gate: this checklist must exist for features that change runtime behavior. diff --git a/specs/044-drift-mvp/contracts/admin-findings.openapi.yaml b/specs/044-drift-mvp/contracts/admin-findings.openapi.yaml new file mode 100644 index 0000000..3c742bd --- /dev/null +++ b/specs/044-drift-mvp/contracts/admin-findings.openapi.yaml @@ -0,0 +1,146 @@ +openapi: 3.0.3 +info: + title: Admin Findings API (Internal) + version: 0.1.0 + description: | + Internal contracts for the generic Findings pipeline. + Drift MVP is the first generator (finding_type=drift). + +servers: + - url: /admin/api + +paths: + /findings: + get: + summary: List findings + parameters: + - in: query + name: finding_type + schema: + type: string + enum: [drift, audit, compare] + - in: query + name: status + schema: + type: string + enum: [new, acknowledged] + - in: query + name: scope_key + schema: + type: string + - in: query + name: current_run_id + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Finding' + + /findings/{id}: + get: + summary: Get finding detail + parameters: + - in: path + name: id + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Finding' + + /findings/{id}/acknowledge: + post: + summary: Acknowledge a finding + parameters: + - in: path + name: id + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Finding' + + /drift/generate: + post: + summary: Generate drift findings (async) + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + scope_key: + type: string + description: Inventory selection hash + required: [scope_key] + responses: + '202': + description: Accepted + +components: + schemas: + Finding: + type: object + properties: + id: + type: integer + finding_type: + type: string + enum: [drift, audit, compare] + tenant_id: + type: integer + scope_key: + type: string + baseline_run_id: + type: integer + nullable: true + current_run_id: + type: integer + nullable: true + fingerprint: + type: string + subject_type: + type: string + subject_external_id: + type: string + severity: + type: string + enum: [low, medium, high] + status: + type: string + enum: [new, acknowledged] + acknowledged_at: + type: string + nullable: true + acknowledged_by_user_id: + type: integer + nullable: true + evidence_jsonb: + type: object + additionalProperties: true diff --git a/specs/044-drift-mvp/data-model.md b/specs/044-drift-mvp/data-model.md new file mode 100644 index 0000000..80a2b73 --- /dev/null +++ b/specs/044-drift-mvp/data-model.md @@ -0,0 +1,57 @@ +# Phase 1 Design: Data Model (044) + +## Entities + +### Finding + +New table: `findings` + +**Purpose**: Generic, persisted pipeline for analytic findings (Drift now; Audit/Compare later). + +**Core fields (MVP)** +- `id` (pk) +- `tenant_id` (fk tenants) +- `finding_type` (`drift` in MVP; later `audit`/`compare`) +- `scope_key` (string; deterministic; reuse Inventory selection hash) +- `baseline_run_id` (nullable fk inventory_sync_runs) +- `current_run_id` (nullable fk inventory_sync_runs) +- `fingerprint` (string; deterministic) +- `subject_type` (string; e.g. policy type) +- `subject_external_id` (string; Graph external id) +- `severity` (`low|medium|high`; MVP default `medium`) +- `status` (`new|acknowledged`) +- `acknowledged_at` (nullable) +- `acknowledged_by_user_id` (nullable fk users) +- `evidence_jsonb` (jsonb; sanitized, small; allowlist) + +**Prepared for later (nullable, out of MVP)** +- `rule_id`, `control_id`, `expected_value`, `source` + +## Constraints & Indexes + +**Uniqueness** +- Unique: `(tenant_id, fingerprint)` + +**Lookup indexes (suggested)** +- `(tenant_id, finding_type, status)` +- `(tenant_id, scope_key)` +- `(tenant_id, current_run_id)` +- `(tenant_id, baseline_run_id)` +- `(tenant_id, subject_type, subject_external_id)` + +## Relationships + +- `Finding` belongs to `Tenant`. +- `Finding` belongs to `User` via `acknowledged_by_user_id`. +- `Finding` belongs to `InventorySyncRun` via `baseline_run_id` (nullable) and `current_run_id` (nullable). + +## Evidence shape (MVP allowlist) + +For Drift MVP, `evidence_jsonb` should contain only: +- `change_type` +- `changed_fields` (list) and/or `change_counts` +- `run`: + - `baseline_run_id`, `current_run_id` + - `baseline_finished_at`, `current_finished_at` + +No raw policy payload dumps; exclude secrets/tokens; exclude volatile fields for hashing. diff --git a/specs/044-drift-mvp/plan.md b/specs/044-drift-mvp/plan.md index 6bffb81..81fe07e 100644 --- a/specs/044-drift-mvp/plan.md +++ b/specs/044-drift-mvp/plan.md @@ -1,24 +1,113 @@ -# Implementation Plan: Drift MVP +# Implementation Plan: Drift MVP (044) -**Date**: 2026-01-07 -**Spec**: `specs/044-drift-mvp/spec.md` +**Branch**: `feat/044-drift-mvp` | **Date**: 2026-01-12 | **Spec**: `specs/044-drift-mvp/spec.md` +**Input**: Feature specification from `specs/044-drift-mvp/spec.md` ## Summary -Add drift findings generation and UI using inventory and sync run metadata. +Introduce a generic, persisted Finding pipeline and implement Drift as the first generator. -## Dependencies +- Drift compares Inventory Sync Runs for the same selection scope (`scope_key`). +- Baseline run = previous successful run for the same scope; comparison run = latest successful run. +- Findings are persisted with deterministic fingerprints and support MVP triage (`new` → `acknowledged`). +- UI is DB-only for label/name resolution (no render-time Graph calls). -- Inventory core + run tracking (Spec 040) -- Inventory UI patterns (Spec 041) +## Technical Context -## Deliverables +**Language/Version**: PHP 8.4.x +**Framework**: Laravel 12 +**Admin UI**: Filament v4 + Livewire v3 +**Storage**: PostgreSQL (JSONB) +**Testing**: Pest v4 +**Target Platform**: Docker (Sail-first local), Dokploy container deployments +**Project Type**: Laravel monolith +**Performance Goals**: +- Drift generation happens async (job), with deterministic output +- Drift listing remains filterable and index-backed +**Constraints**: +- Tenant isolation for all reads/writes +- No render-time Graph calls; labels resolved from DB caches +- Evidence minimization (sanitized allowlist; no raw payload dumps) +**Scale/Scope**: +- Tenants may have large inventories; findings must be indexed for typical filtering -- Baseline definition and drift finding generation -- Drift summary + detail UI -- Acknowledge/triage actions +## Constitution Check -## Risks +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* -- False positives if baseline definition is unclear -- Data volume for large tenants +- Inventory-first: Drift is derived from Inventory Sync Runs and Inventory Items (“last observed” state). +- Read/write separation: Drift generation is analytical; writes are limited to triage acknowledgement and must be audited + tested. +- Graph contract path: Drift UI performs no Graph calls; Graph calls remain isolated in existing Inventory/Graph client layers. +- Deterministic capabilities: drift scope derives from existing selection hashing and inventory type registries. +- Tenant isolation: all reads/writes tenant-scoped; no cross-tenant leakage. +- Automation: drift generation is queued; jobs are deduped/locked per scope+run pair and observable. +- Data minimization: store only minimized evidence JSON; logs contain no secrets/tokens. + +## Project Structure + +### Documentation (this feature) + +```text +specs/044-drift-mvp/ +├── plan.md # This file (/speckit.plan output) +├── research.md # Phase 0 output +├── data-model.md # Phase 1 output +├── quickstart.md # Phase 1 output +├── contracts/ # Phase 1 output +│ └── admin-findings.openapi.yaml +└── tasks.md # Phase 2 output (/speckit.tasks) +``` + +### Source Code (repository root) + +```text +app/ +├── Filament/ +│ ├── Pages/ +│ │ └── DriftLanding.php # drift landing page (summary + generation status) +│ └── Resources/ +│ └── FindingResource/ # list/detail + acknowledge action (tenant-scoped) +├── Jobs/ +│ └── GenerateDriftFindingsJob.php # async generator (on-demand) +├── Models/ +│ └── Finding.php # generic finding model +└── Services/ + └── Drift/ + ├── DriftFindingGenerator.php # computes deterministic findings for baseline/current + ├── DriftHasher.php # baseline_hash/current_hash helpers + └── DriftScopeKey.php # scope_key is InventorySyncRun.selection_hash (single canonical definition) + +database/migrations/ +└── 2026_.._.._create_findings_table.php + +tests/Feature/Drift/ +├── DriftGenerationDeterminismTest.php +├── DriftTenantIsolationTest.php +└── DriftAcknowledgeTest.php +``` + +**Structure Decision**: Laravel monolith using Filament pages/resources and queued jobs. + +## Complexity Tracking + +> **Fill ONLY if Constitution Check has violations that must be justified** + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| (none) | | | + +## Phase 0 Output (Research) + +Completed in `specs/044-drift-mvp/research.md`. + +## Phase 1 Output (Design) + +Completed in: + +- `specs/044-drift-mvp/data-model.md` +- `specs/044-drift-mvp/contracts/` +- `specs/044-drift-mvp/quickstart.md` + +## Phase 2 Planning Notes + +Next step is expanding `specs/044-drift-mvp/tasks.md` (via `/speckit.tasks`) with phased, test-first implementation tasks. diff --git a/specs/044-drift-mvp/quickstart.md b/specs/044-drift-mvp/quickstart.md new file mode 100644 index 0000000..2e709a3 --- /dev/null +++ b/specs/044-drift-mvp/quickstart.md @@ -0,0 +1,30 @@ +# Quickstart: Drift MVP (044) + +## Run locally (Sail) + +```bash +./vendor/bin/sail up -d +./vendor/bin/sail artisan queue:work --tries=1 +``` + +## Prepare data + +1. Open the admin panel and select a tenant context. +2. Navigate to Inventory and run an Inventory Sync **twice** with the same selection (same `selection_hash`). + +## Use Drift + +1. Navigate to the new Drift area. +2. On first open, Drift will dispatch a background job to generate findings for: + - baseline = previous successful run for the same `scope_key` + - current = latest successful run for the same `scope_key` +3. Refresh the page once the job finishes. + +## Triage + +- Acknowledge a finding; it should move out of the “new” view but remain visible/auditable. + +## Notes + +- UI must remain DB-only for label resolution (no render-time Graph calls). +- Findings store minimal, sanitized evidence only. diff --git a/specs/044-drift-mvp/research.md b/specs/044-drift-mvp/research.md new file mode 100644 index 0000000..0ef8180 --- /dev/null +++ b/specs/044-drift-mvp/research.md @@ -0,0 +1,72 @@ +# Phase 0 Output: Research (044) + +## Decisions + +### 1) `scope_key` reuse + +- Decision: Use the existing Inventory selection hash as `scope_key`. + - Concretely: `scope_key = InventorySyncRun.selection_hash`. +- Rationale: + - Inventory already normalizes + hashes selection payload deterministically (via `InventorySelectionHasher`). + - It is already used for concurrency/deduping inventory runs, so it’s the right stable scope identifier. +- Alternatives considered: + - Compute a second hash (duplicate of selection_hash) → adds drift without benefit. + - Store the raw selection payload as the primary key → not stable without strict normalization. + +### 2) Baseline selection (MVP) + +- Decision: Baseline run = previous successful inventory sync run for the same `scope_key`; comparison run = latest successful inventory sync run for the same `scope_key`. +- Rationale: + - Matches “run at least twice” scenario. + - Deterministic and explainable. +- Alternatives considered: + - User-pinned baselines → valuable, but deferred (design must allow later via `scope_key`). + +### 3) Persisted generic Findings + +- Decision: Persist Findings in a generic `findings` table. +- Rationale: + - Enables stable triage (`acknowledged`) without recomputation drift. + - Reusable pipeline for Drift now, Audit/Compare later. +- Alternatives considered: + - Compute-on-demand and store only acknowledgements by fingerprint → harder operationally and can surprise users when diff rules evolve. + +### 4) Generation trigger (MVP) + +- Decision: On opening Drift, if findings for (tenant, `scope_key`, baseline_run_id, current_run_id) do not exist, dispatch an async job to generate them. +- Rationale: + - Avoids long request times. + - Avoids scheduled complexity in MVP. +- Alternatives considered: + - Generate after every inventory run → may be expensive; can be added later. + - Nightly schedule → hides immediacy and complicates operations. + +### 5) Fingerprint and state hashing + +- Decision: Use a deterministic fingerprint that changes when the underlying state changes. + - Fingerprint = `sha256(tenant_id + scope_key + subject_type + subject_external_id + change_type + baseline_hash + current_hash)`. + - baseline_hash/current_hash are computed over normalized, sanitized comparison data (exclude volatile fields like timestamps). +- Rationale: + - Stable identity for triage and audit. + - Supports future generators (audit/compare) using same semantics. +- Alternatives considered: + - Fingerprint without baseline/current hash → cannot distinguish changed vs unchanged findings. + +### 6) Evidence minimization + +- Decision: Store small, sanitized `evidence_jsonb` with an allowlist shape; no raw payload dumps. +- Rationale: + - Aligns with data minimization + safe logging. + - Avoids storing secrets/tokens. + +### 7) Name resolution and Graph safety + +- Decision: UI resolves human-readable labels using DB-backed Inventory + Foundations (047) + Groups Cache (051). No render-time Graph calls. +- Rationale: + - Works offline / when tokens are broken. + - Keeps UI safe and predictable. + +## Notes / Follow-ups for Phase 1 + +- Define the `findings` table indexes carefully for tenant-scoped filtering (status, type, scope_key, run_ids). +- Consider using existing observable run patterns (BulkOperationRun + AuditLogger) for drift generation jobs. diff --git a/specs/044-drift-mvp/spec.md b/specs/044-drift-mvp/spec.md index 05bb9ab..27031cb 100644 --- a/specs/044-drift-mvp/spec.md +++ b/specs/044-drift-mvp/spec.md @@ -20,6 +20,14 @@ ### Session 2026-01-12 - Q: Which inventory entities/types are in scope for Drift MVP? → A: Policies + Assignments. - Q: When should drift findings be generated? → A: On-demand when opening Drift: if findings for (baseline,current,scope) don’t exist yet, dispatch an async job to generate them. +### Session 2026-01-13 + +- Q: What should Drift do if there are fewer than two successful inventory runs for the same `scope_key`? → A: Show a blocked/empty state (“Need at least 2 successful runs for this scope to calculate drift”) and do not dispatch drift generation. +- Q: Should acknowledgement carry forward across comparisons? → A: No; acknowledgement is per comparison (`baseline_run_id` + `current_run_id` + `scope_key`). The same drift may re-appear as `new` in later comparisons. +- Q: Which `change_type` values are supported in Drift MVP? → A: `added`, `removed`, `modified` (assignment target/intent changes are covered under `modified`). +- Q: What is the default UI behavior for `new` vs `acknowledged` findings? → A: Default UI shows only `new`; `acknowledged` is accessible via an explicit filter. +- Q: What should the UI do if drift generation fails for a comparison? → A: Show an explicit error state (safe message + reference/run ids) and do not show findings for that comparison until a successful generation exists. + ## Pinned Decisions (MVP defaults) - Drift is implemented as a generator that writes persisted Finding rows (not only an in-memory/on-demand diff). @@ -65,6 +73,8 @@ ### Scenario 1: View drift summary - When the admin opens Drift - Then they see a summary of changes since the last baseline +- If there are fewer than two successful runs for the same `scope_key`, Drift shows a blocked/empty state and does not start drift generation. + ### Scenario 2: Drill into a drift finding - Given a drift finding exists - When the admin opens the finding @@ -75,19 +85,23 @@ ### Scenario 3: Acknowledge/triage - When the admin marks it acknowledged - Then it is hidden from “new” lists but remains auditable +- Acknowledgement is per comparison; later comparisons may still surface the same drift as `new`. + ## Functional Requirements - FR1: Baseline + scope - - Define `scope_key` as a deterministic string derived from the Inventory Selection. - - Example: `scope_key = sha256(normalized selection payload)`. - - Must remain stable across equivalent selections (normalization), and allow future pinned baselines / compare baselines. + - Define `scope_key` as the deterministic Inventory selection identifier. + - MVP definition: `scope_key = InventorySyncRun.selection_hash`. + - Rationale: selection hashing already normalizes equivalent selections; reusing it keeps drift scope stable and consistent across the product. - Baseline run (MVP) = previous successful inventory run for the same `scope_key`. - Comparison run (MVP) = latest successful inventory run for the same `scope_key`. - FR2: Finding generation (Drift MVP) - Findings are persisted per (`baseline_run_id`, `current_run_id`, `scope_key`). - - Findings cover adds, removals, and metadata changes for supported entities (Policies + Assignments). + - Findings cover adds, removals, and changes for supported entities (Policies + Assignments). + - MVP `change_type` values: `added`, `removed`, `modified`. - Findings are deterministic: same baseline/current + scope_key ⇒ same set of fingerprints. + - If fewer than two successful inventory runs exist for a given `scope_key`, Drift does not generate findings and must surface a clear blocked/empty state in the UI. - FR2a: Fingerprint definition (MVP) - Fingerprint = `sha256(tenant_id + scope_key + subject_type + subject_external_id + change_type + baseline_hash + current_hash)`. @@ -98,9 +112,13 @@ ## Functional Requirements - Assignment drift includes target changes (e.g., groupId) and intent changes. - FR3: Provide Drift UI with summary and details. + - Default lists and the Drift landing summary show only `status=new` by default. + - The UI must provide a filter to include `acknowledged` findings. + - If drift generation fails for a comparison, the UI must surface an explicit error state (no secrets), including reference identifiers (e.g., run ids), and must not fall back to stale/previous results. - FR4: Triage (MVP) - Admin can acknowledge a finding; record `acknowledged_by_user_id` + `acknowledged_at`. + - Acknowledgement does not carry forward across comparisons in the MVP. - Findings are never deleted in the MVP. ## Non-Functional Requirements diff --git a/specs/044-drift-mvp/tasks.md b/specs/044-drift-mvp/tasks.md index 7ccfb97..481f990 100644 --- a/specs/044-drift-mvp/tasks.md +++ b/specs/044-drift-mvp/tasks.md @@ -1,7 +1,150 @@ -# Tasks: Drift MVP +--- -- [ ] T001 Define baseline and scope rules -- [ ] T002 Drift finding generation (deterministic) -- [ ] T003 Drift summary + detail UI -- [ ] T004 Acknowledge/triage state -- [ ] T005 Tests for determinism and tenant scoping +description: "Task list for feature 044 drift MVP" + +--- + +# Tasks: Drift MVP (044) + +**Input**: Design documents from `specs/044-drift-mvp/` +**Prerequisites**: `plan.md` (required), `spec.md` (required), plus `research.md`, `data-model.md`, `contracts/`, `quickstart.md` + +**Tests**: REQUIRED (Pest) - feature introduces runtime behavior + new persistence. + +**Organization**: Tasks are grouped by user story (Scenario 1/2/3 in spec). + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) +- Include exact file paths in descriptions + +--- + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Project wiring for Drift MVP. + +- [ ] T001 Create/update constitution gate checklist in `specs/044-drift-mvp/checklists/requirements.md` +- [ ] T002 Confirm spec/plan artifacts are current in `specs/044-drift-mvp/{plan.md,spec.md,research.md,data-model.md,quickstart.md,contracts/admin-findings.openapi.yaml}` +- [ ] T003 Add Drift landing page shell in `app/Filament/Pages/DriftLanding.php` +- [ ] T004 [P] Add Finding resource shells in `app/Filament/Resources/FindingResource.php` and `app/Filament/Resources/FindingResource/Pages/` + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Persistence + authorization + deterministic IDs that all stories depend on. + +**Checkpoint**: DB schema exists, tenant scoping enforced, and tests can create Finding rows. + +- [ ] T005 Create `findings` migration in `database/migrations/*_create_findings_table.php` with Finding fields aligned to `specs/044-drift-mvp/spec.md`: + (tenant_id, finding_type, scope_key, baseline_run_id, current_run_id, subject_type, subject_external_id, severity, status, fingerprint unique, evidence_jsonb, acknowledged_at, acknowledged_by_user_id) +- [ ] T006 Create `Finding` model in `app/Models/Finding.php` (casts for `evidence_jsonb`, enums/constants for `finding_type`/`severity`/`status`, `acknowledged_at` handling) +- [ ] T007 [P] Add `FindingFactory` in `database/factories/FindingFactory.php` +- [ ] T008 Ensure `InventorySyncRunFactory` exists (or add it) in `database/factories/InventorySyncRunFactory.php` +- [ ] T009 Add authorization policy in `app/Policies/FindingPolicy.php` and wire it in `app/Providers/AuthServiceProvider.php` (or project equivalent) +- [ ] T010 Add Drift permissions in `config/intune_permissions.php` (view + acknowledge) and wire them into Filament navigation/actions +- [ ] T011 Enforce tenant scoping for Finding queries in `app/Models/Finding.php` and/or `app/Filament/Resources/FindingResource.php` +- [ ] T012 Implement fingerprint helper in `app/Services/Drift/DriftHasher.php` (sha256 per spec) +- [ ] T013 Pin `scope_key = InventorySyncRun.selection_hash` in `app/Services/Drift/DriftScopeKey.php` (single canonical definition) +- [ ] T014 Implement evidence allowlist builder in `app/Services/Drift/DriftEvidence.php` (new file) + +--- + +## Phase 3: User Story 1 - View drift summary (Priority: P1) MVP + +**Goal**: Opening Drift generates (async) and displays a summary of new drift findings for the latest scope. + +**Independent Test**: With 2 successful inventory runs for the same selection hash, opening Drift dispatches generation if missing and then shows summary counts. + +### Tests (write first) + +- [ ] T015 [P] [US1] Baseline selection tests in `tests/Feature/Drift/DriftBaselineSelectionTest.php` +- [ ] T016 [P] [US1] Generation dispatch tests in `tests/Feature/Drift/DriftGenerationDispatchTest.php` +- [ ] T017 [P] [US1] Tenant isolation tests in `tests/Feature/Drift/DriftTenantIsolationTest.php` +- [ ] T018 [P] [US1] Assignment drift detection test (targets + intent changes per FR2b) in `tests/Feature/Drift/DriftAssignmentDriftDetectionTest.php` + +### Implementation + +- [ ] T019 [US1] Implement run selection service in `app/Services/Drift/DriftRunSelector.php` +- [ ] T020 [US1] Implement generator job in `app/Jobs/GenerateDriftFindingsJob.php` (dedupe/lock by tenant+scope+baseline+current) +- [ ] T021 [US1] Implement generator service in `app/Services/Drift/DriftFindingGenerator.php` (idempotent) +- [ ] T022 [US1] Implement landing behavior (dispatch + status UI) in `app/Filament/Pages/DriftLanding.php` +- [ ] T023 [US1] Implement summary queries/widgets in `app/Filament/Pages/DriftLanding.php` + +--- + +## Phase 4: User Story 2 - Drill into a drift finding (Priority: P2) + +**Goal**: Admin can view a finding and see sanitized evidence + run references (DB-only label resolution). + +**Independent Test**: A persisted finding renders details without Graph calls. + +### Tests (write first) + +- [ ] T024 [P] [US2] Finding detail test in `tests/Feature/Drift/DriftFindingDetailTest.php` +- [ ] T025 [P] [US2] Evidence minimization test in `tests/Feature/Drift/DriftEvidenceMinimizationTest.php` + +### Implementation + +- [ ] T026 [US2] Implement list UI in `app/Filament/Resources/FindingResource.php` (filters: status, scope_key, run) +- [ ] T027 [US2] Implement detail UI in `app/Filament/Resources/FindingResource/Pages/ViewFinding.php` +- [ ] T028 [US2] Implement DB-only name resolution in `app/Filament/Resources/FindingResource.php` (inventory/foundations caches) + +--- + +## Phase 5: User Story 3 - Acknowledge/triage (Priority: P3) + +**Goal**: Admin can acknowledge findings; new lists hide acknowledged but records remain auditable. + +**Independent Test**: Acknowledging sets `acknowledged_at` + `acknowledged_by_user_id` and flips status. + +### Tests (write first) + +- [ ] T029 [P] [US3] Acknowledge action test in `tests/Feature/Drift/DriftAcknowledgeTest.php` +- [ ] T030 [P] [US3] Acknowledge authorization test in `tests/Feature/Drift/DriftAcknowledgeAuthorizationTest.php` + +### Implementation + +- [ ] T031 [US3] Add acknowledge actions in `app/Filament/Resources/FindingResource.php` +- [ ] T032 [US3] Implement `acknowledge()` domain method in `app/Models/Finding.php` +- [ ] T033 [US3] Ensure Drift summary excludes acknowledged by default in `app/Filament/Pages/DriftLanding.php` + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +- [ ] T034 Add DB indexes in `database/migrations/*_create_findings_table.php` (tenant_id+status, tenant_id+scope_key, tenant_id+baseline_run_id, tenant_id+current_run_id) +- [ ] T035 [P] Add determinism test in `tests/Feature/Drift/DriftGenerationDeterminismTest.php` (same baseline/current => same fingerprints) +- [ ] T036 Add job observability logs in `app/Jobs/GenerateDriftFindingsJob.php` (no secrets) +- [ ] T037 Add idempotency/upsert strategy in `app/Services/Drift/DriftFindingGenerator.php` +- [ ] T038 Ensure volatile fields excluded from hashing in `app/Services/Drift/DriftHasher.php` and cover in `tests/Feature/Drift/DriftHasherTest.php` +- [ ] T039 Validate and update `specs/044-drift-mvp/quickstart.md` after implementation + +--- + +## Dependencies & Execution Order + +- Setup (Phase 1) -> Foundational (Phase 2) -> US1 -> US2 -> US3 -> Polish + +### Parallel execution examples + +- Foundational: T007, T008, T012, T013, T014 +- US1 tests: T015, T016, T017, T018 +- US2 tests: T024, T025 +- US3 tests: T029, T030 + +--- + +## Implementation Strategy + +### MVP scope + +- MVP = Phase 1 + Phase 2 + US1. + +### Format validation + +- All tasks use `- [ ] T###` format +- Story tasks include `[US1]`/`[US2]`/`[US3]` +- All tasks include file paths