feat/044-drift-mvp #58
35
specs/044-drift-mvp/checklists/requirements.md
Normal file
35
specs/044-drift-mvp/checklists/requirements.md
Normal file
@ -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.
|
||||
146
specs/044-drift-mvp/contracts/admin-findings.openapi.yaml
Normal file
146
specs/044-drift-mvp/contracts/admin-findings.openapi.yaml
Normal file
@ -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
|
||||
57
specs/044-drift-mvp/data-model.md
Normal file
57
specs/044-drift-mvp/data-model.md
Normal file
@ -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.
|
||||
@ -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.
|
||||
|
||||
30
specs/044-drift-mvp/quickstart.md
Normal file
30
specs/044-drift-mvp/quickstart.md
Normal file
@ -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.
|
||||
72
specs/044-drift-mvp/research.md
Normal file
72
specs/044-drift-mvp/research.md
Normal file
@ -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.
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user