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
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 throughGraphClientInterface. - Deterministic capabilities: Severity is derived deterministically from
config/intune_permissions.phpfeature count (FR-005). Testable via snapshot/golden tests. No RBAC capabilities added -- uses existingFINDINGS_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 viaDerivesWorkspaceIdFromTenant. 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
OperationRunwithtype=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_postureandstatus=resolvedadded 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_idNOT 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:
- Migration:
create_stored_reports_table(see data-model.md) - Migration:
add_resolved_to_findings_table(addresolved_at,resolved_reasoncolumns) - Model:
StoredReportwith factory - Finding model: Add
STATUS_RESOLVED,FINDING_TYPE_PERMISSION_POSTURE,resolve(),reopen()methods,resolved_atcast - FindingFactory: Add
permissionPosture()andresolved()states - Badge: Add
resolvedmapping toFindingStatusBadge - OperationCatalog: Add
TYPE_PERMISSION_POSTURE_CHECK - AlertRule: Add
EVENT_PERMISSION_MISSINGconstant (pulled from Phase D into Phase A for early availability; tasks.md T008) - Badge: Create
FindingTypeBadgewithdriftandpermission_posturemappings per BADGE-001 - 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:
- Service:
PostureScoreCalculator(pure function) - Service:
PermissionPostureFindingGenerator(findings + report creation + alert event production) - 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:
- Job:
GeneratePermissionPostureFindingsJob(load tenant, create OperationRun, call generator, record outcome) - Hook: Modify
ProviderConnectionHealthCheckJobto dispatch posture job aftercompare()returns (whenoverall_status !== 'error') - 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:
- AlertRule constant: Already delivered in Phase A (T008) — no work here
- EvaluateAlertsJob: Add
permissionMissingEvents()method - UI: Add
EVENT_PERMISSION_MISSINGto alert rule event type dropdown (existing form, just a new option) - 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:
- Artisan command:
PruneStoredReportsCommand(stored-reports:prune --days=90) - Schedule: Register in
routes/console.php(daily) - FindingFactory: Ensure
sourcefield is populated inpermissionPosture()state - Integration test: End-to-end flow (health check -> compare -> posture job -> findings + report + alert event)
- Tests: Retention pruning, schedule registration
Dependencies: Phases A-D complete.
Filament v5 Agent Output Contract
- Livewire v4.0+ compliance: Yes -- no new Livewire components introduced. Existing Findings resource already complies.
- Provider registration: No new providers. Existing
AdminPanelProviderinbootstrap/providers.phpunchanged. - Global search: No new globally searchable resources. Existing Finding resource global search behavior unchanged.
- Destructive actions: None introduced. Posture findings are system-generated, not user-deletable.
- Asset strategy: No new frontend assets. Badge mapping is PHP-only. No
filament:assetschanges needed. - 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).