TenantAtlas/specs/104-provider-permission-posture/plan.md
ahmido ef380b67d1 feat(104): Provider Permission Posture (#127)
Implements Spec 104: Provider Permission Posture.

What changed
- Generates permission posture findings after each tenant permission compare (queued)
- Stores immutable posture snapshots as StoredReports (JSONB payload)
- Adds global Finding resolved lifecycle (`resolved_at`, `resolved_reason`) with `resolve()` / `reopen()`
- Adds alert pipeline event type `permission_missing` (Alerts v1) and Filament option for Alert Rules
- Adds retention pruning command + daily schedule for StoredReports
- Adds badge mappings for `resolved` finding status and `permission_posture` finding type

UX fixes discovered during manual verification
- Hide “Diff” section for non-drift findings (only drift findings show diff)
- Required Permissions page: “Re-run verification” now links to Tenant view (not onboarding)
- Preserve Technical Details `<details>` open state across Livewire re-renders (Alpine state)

Verification
- Ran `vendor/bin/sail artisan test --compact --filter=PermissionPosture` (50 tests)
- Ran `vendor/bin/sail artisan test --compact --filter="FindingResolved|FindingBadge|PermissionMissingAlert"` (20 tests)
- Ran `vendor/bin/sail bin pint --dirty`

Filament v5 / Livewire v4 compliance
- Filament v5 + Livewire v4: no Livewire v3 usage.

Panel provider registration (Laravel 11+)
- No new panels added. Existing panel providers remain registered via `bootstrap/providers.php`.

Global search rule
- No changes to global-searchable resources.

Destructive actions
- No new destructive Filament actions were added in this PR.

Assets / deploy notes
- No new Filament assets registered. Existing deploy step `php artisan filament:assets` remains unchanged.

Test coverage
- New/updated Pest feature tests cover generator behavior, job integration, alerting, retention pruning, and resolved lifecycle.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #127
2026-02-21 22:32:52 +00:00

197 lines
12 KiB
Markdown

# Implementation Plan: Provider Permission Posture
**Branch**: `104-provider-permission-posture` | **Date**: 2026-02-21 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/104-provider-permission-posture/spec.md`
## Summary
Implement a Provider Permission Posture system that automatically generates findings for missing Intune permissions, persists posture snapshots as stored reports, integrates with the existing alerts pipeline, and extends the Finding model with a global `resolved` status. The posture generator is dispatched event-driven after each `TenantPermissionService::compare()` call, uses fingerprint-based idempotent upsert (matching the DriftFindingGenerator pattern), and derives severity from the feature-impact count in `config/intune_permissions.php`.
## Technical Context
**Language/Version**: PHP 8.4 (Laravel 12)
**Primary Dependencies**: Filament v5, Livewire v4, Pest v4
**Storage**: PostgreSQL (via Sail), JSONB for stored report payloads and finding evidence
**Testing**: Pest v4 (`vendor/bin/sail artisan test --compact`)
**Target Platform**: Linux server (Docker/Sail locally, Dokploy for staging/production)
**Project Type**: Web application (Laravel monolith)
**Performance Goals**: Posture generation completes in <5s per tenant (14 permissions); alert delivery within 2 min of finding creation
**Constraints**: No external API calls from posture generator (reads TenantPermissionService output); all work is queued
**Scale/Scope**: Up to ~50 tenants per workspace, 14 required permissions per tenant, ~700 findings max in steady state
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- [x] **Inventory-first**: Posture findings represent "last observed" permission state from `TenantPermissionService::compare()`. No snapshots/backups involved -- this is analysis on existing inventory data.
- [x] **Read/write separation**: Posture generation is read-only analysis (no Intune writes). StoredReports are immutable snapshots. No preview/dry-run needed (no destructive operations).
- [x] **Graph contract path**: No new Graph calls. Reads output of existing `TenantPermissionService::compare()` which already goes through `GraphClientInterface`.
- [x] **Deterministic capabilities**: Severity is derived deterministically from `config/intune_permissions.php` feature count (FR-005). Testable via snapshot/golden tests. No RBAC capabilities added -- uses existing `FINDINGS_VIEW`, `FINDINGS_MANAGE`, `ALERTS_VIEW`, `ALERTS_MANAGE`.
- [x] **RBAC-UX**: No new Filament pages/resources. Posture findings appear in existing Findings resource (tenant-scoped). Non-member -> 404. Member without FINDINGS_VIEW -> 403. No new capabilities needed.
- [x] **Workspace isolation**: StoredReports include `workspace_id` (NOT NULL). Findings derive workspace via `DerivesWorkspaceIdFromTenant`. Non-member workspace access -> 404.
- [x] **Tenant isolation**: All findings, stored reports, and operation runs are scoped via `tenant_id` (NOT NULL). Cross-tenant access impossible at query level.
- [x] **Run observability**: Posture generation tracked as `OperationRun` with `type=permission_posture_check`. Start/complete/outcome/counts recorded.
- [x] **Automation**: Job dispatched per-tenant (no batch lock needed). Fingerprint-based upsert handles concurrency. Queue handles backpressure.
- [x] **Data minimization**: Evidence contains only permission metadata (key, type, features, status). No secrets/tokens/PII.
- [x] **Badge semantics (BADGE-001)**: `finding_type=permission_posture` and `status=resolved` added to centralized badge mappers. Tests included.
- [x] **Filament UI Action Surface Contract**: NO new Resources/Pages/RelationManagers. Exemption documented in spec.
- [x] **UX-001 (Layout & IA)**: No new screens. Exemption documented in spec.
- [x] **SCOPE-001 ownership**: StoredReports are tenant-owned (`workspace_id` + `tenant_id` NOT NULL). Findings are tenant-owned (existing). AlertRule is workspace-owned (existing, no change).
**Post-Phase-1 re-check**: All items pass. No violations found.
## Project Structure
### Documentation (this feature)
```text
specs/104-provider-permission-posture/
├── plan.md # This file
├── spec.md # Feature specification
├── research.md # Phase 0: research decisions
├── data-model.md # Phase 1: entity/table design
├── quickstart.md # Phase 1: implementation guide
├── contracts/
│ └── internal-services.md # Phase 1: service contracts
├── checklists/
│ └── requirements.md # Quality checklist
└── tasks.md # Phase 2 output (created by /speckit.tasks)
```
### Source Code (repository root)
```text
app/
├── Models/
│ ├── Finding.php # MODIFIED: +STATUS_RESOLVED, +FINDING_TYPE_PERMISSION_POSTURE, +resolve(), +reopen()
│ ├── StoredReport.php # NEW: generic stored report model
│ └── AlertRule.php # MODIFIED: +EVENT_PERMISSION_MISSING constant
├── Services/
│ └── PermissionPosture/
│ ├── PostureResult.php # NEW: value object (findingsCreated, resolved, reopened, etc.)
│ ├── PostureScoreCalculator.php # NEW: pure function, score = round(granted/required * 100)
│ └── PermissionPostureFindingGenerator.php # NEW: core generator (findings + report + alert events)
├── Jobs/
│ ├── GeneratePermissionPostureFindingsJob.php # NEW: queued per-tenant job
│ ├── ProviderConnectionHealthCheckJob.php # MODIFIED: dispatch posture job after compare()
│ └── Alerts/
│ └── EvaluateAlertsJob.php # MODIFIED: +permissionMissingEvents() method
├── Support/
│ ├── OperationCatalog.php # MODIFIED: +TYPE_PERMISSION_POSTURE_CHECK
│ └── Badges/Domains/
│ ├── FindingStatusBadge.php # MODIFIED: +resolved badge mapping
│ └── FindingTypeBadge.php # NEW: finding type badge map (permission_posture, drift)
├── Filament/Resources/
│ └── AlertRuleResource.php # MODIFIED: +EVENT_PERMISSION_MISSING in eventTypeOptions()
└── Console/Commands/
└── PruneStoredReportsCommand.php # NEW: retention cleanup
database/
├── migrations/
│ ├── XXXX_create_stored_reports_table.php # NEW
│ └── XXXX_add_resolved_to_findings_table.php # NEW
└── factories/
├── StoredReportFactory.php # NEW
└── FindingFactory.php # MODIFIED: +permissionPosture(), +resolved() states
tests/Feature/
├── PermissionPosture/
│ ├── PostureScoreCalculatorTest.php # NEW
│ ├── PermissionPostureFindingGeneratorTest.php # NEW
│ ├── GeneratePermissionPostureFindingsJobTest.php # NEW
│ ├── StoredReportModelTest.php # NEW
│ ├── PruneStoredReportsCommandTest.php # NEW: retention pruning tests
│ └── PermissionPostureIntegrationTest.php # NEW: end-to-end flow test
├── Alerts/
│ └── PermissionMissingAlertTest.php # NEW
├── Support/Badges/
│ └── FindingBadgeTest.php # NEW: resolved + permission_posture badge tests
└── Models/
└── FindingResolvedTest.php # NEW: resolved lifecycle tests
routes/
└── console.php # MODIFIED: schedule prune command
```
**Structure Decision**: Standard Laravel monolith structure. New services go under `app/Services/PermissionPosture/`. Tests mirror the service structure under `tests/Feature/PermissionPosture/`.
## Complexity Tracking
> No constitution violations. No complexity justifications needed.
## Implementation Phases
### Phase A -- Foundation (StoredReports + Finding Model Extensions)
**Goal**: Establish the data layer that all other phases depend on.
**Deliverables**:
1. Migration: `create_stored_reports_table` (see data-model.md)
2. Migration: `add_resolved_to_findings_table` (add `resolved_at`, `resolved_reason` columns)
3. Model: `StoredReport` with factory
4. Finding model: Add `STATUS_RESOLVED`, `FINDING_TYPE_PERMISSION_POSTURE`, `resolve()`, `reopen()` methods, `resolved_at` cast
5. FindingFactory: Add `permissionPosture()` and `resolved()` states
6. Badge: Add `resolved` mapping to `FindingStatusBadge`
7. OperationCatalog: Add `TYPE_PERMISSION_POSTURE_CHECK`
8. AlertRule: Add `EVENT_PERMISSION_MISSING` constant (pulled from Phase D into Phase A for early availability; tasks.md T008)
9. Badge: Create `FindingTypeBadge` with `drift` and `permission_posture` mappings per BADGE-001
10. Tests: StoredReport model CRUD, Finding resolve/reopen lifecycle, badge rendering
**Dependencies**: None (foundation layer).
### Phase B -- Core Generator
**Goal**: Implement the posture finding generator that produces findings, stored reports, and computes posture scores.
**Deliverables**:
1. Service: `PostureScoreCalculator` (pure function)
2. Service: `PermissionPostureFindingGenerator` (findings + report creation + alert event production)
3. Tests: Score calculation (edge cases: 0 permissions, all granted, all missing), finding generation (create, auto-resolve, re-open, idempotency, error handling, severity derivation)
**Dependencies**: Phase A (models, migrations, constants).
### Phase C -- Job + Health Check Hook
**Goal**: Wire the generator into the existing health check pipeline as a queued job.
**Deliverables**:
1. Job: `GeneratePermissionPostureFindingsJob` (load tenant, create OperationRun, call generator, record outcome)
2. Hook: Modify `ProviderConnectionHealthCheckJob` to dispatch posture job after `compare()` returns (when `overall_status !== 'error'`)
3. Tests: Job dispatch integration, skip-if-no-connection, OperationRun tracking, error handling
**Dependencies**: Phase B (generator service).
### Phase D -- Alerts Integration
**Goal**: Connect posture findings to the existing alert pipeline.
**Deliverables**:
1. AlertRule constant: Already delivered in Phase A (T008) — no work here
2. EvaluateAlertsJob: Add `permissionMissingEvents()` method
3. UI: Add `EVENT_PERMISSION_MISSING` to alert rule event type dropdown (existing form, just a new option)
4. Tests: Alert event production, severity filtering, cooldown/dedupe, alert rule matching
**Dependencies**: Phase B (generator produces alert events), Phase A (AlertRule constant).
### Phase E -- Retention + Polish
**Goal**: Implement stored report cleanup and finalize integration.
**Deliverables**:
1. Artisan command: `PruneStoredReportsCommand` (`stored-reports:prune --days=90`)
2. Schedule: Register in `routes/console.php` (daily)
3. FindingFactory: Ensure `source` field is populated in `permissionPosture()` state
4. Integration test: End-to-end flow (health check -> compare -> posture job -> findings + report + alert event)
5. Tests: Retention pruning, schedule registration
**Dependencies**: Phases A-D complete.
## Filament v5 Agent Output Contract
1. **Livewire v4.0+ compliance**: Yes -- no new Livewire components introduced. Existing Findings resource already complies.
2. **Provider registration**: No new providers. Existing `AdminPanelProvider` in `bootstrap/providers.php` unchanged.
3. **Global search**: No new globally searchable resources. Existing Finding resource global search behavior unchanged.
4. **Destructive actions**: None introduced. Posture findings are system-generated, not user-deletable.
5. **Asset strategy**: No new frontend assets. Badge mapping is PHP-only. No `filament:assets` changes needed.
6. **Testing plan**: Pest feature tests for generator (create/resolve/reopen/idempotency), score calculator, job dispatch, alert integration, badge rendering, retention command. All mounted as service/job tests, not Livewire component tests (no new components).