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