# 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).