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

12 KiB

Implementation Plan: Provider Permission Posture

Branch: 104-provider-permission-posture | Date: 2026-02-21 | Spec: 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.

  • Inventory-first: Posture findings represent "last observed" permission state from TenantPermissionService::compare(). No snapshots/backups involved -- this is analysis on existing inventory data.
  • Read/write separation: Posture generation is read-only analysis (no Intune writes). StoredReports are immutable snapshots. No preview/dry-run needed (no destructive operations).
  • Graph contract path: No new Graph calls. Reads output of existing TenantPermissionService::compare() which already goes through GraphClientInterface.
  • 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.
  • 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.
  • Workspace isolation: StoredReports include workspace_id (NOT NULL). Findings derive workspace via DerivesWorkspaceIdFromTenant. Non-member workspace access -> 404.
  • Tenant isolation: All findings, stored reports, and operation runs are scoped via tenant_id (NOT NULL). Cross-tenant access impossible at query level.
  • Run observability: Posture generation tracked as OperationRun with type=permission_posture_check. Start/complete/outcome/counts recorded.
  • Automation: Job dispatched per-tenant (no batch lock needed). Fingerprint-based upsert handles concurrency. Queue handles backpressure.
  • Data minimization: Evidence contains only permission metadata (key, type, features, status). No secrets/tokens/PII.
  • Badge semantics (BADGE-001): finding_type=permission_posture and status=resolved added to centralized badge mappers. Tests included.
  • Filament UI Action Surface Contract: NO new Resources/Pages/RelationManagers. Exemption documented in spec.
  • UX-001 (Layout & IA): No new screens. Exemption documented in spec.
  • 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)

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)

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