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
22 KiB
Feature Specification: Provider Permission Posture
Feature Branch: 104-provider-permission-posture
Created: 2026-02-21
Status: Draft
Input: User description: "Provider Permission Posture - StoredReports foundation, Permission Posture Findings generation, and Alerts integration for measured app permissions"
Spec Scope Fields (mandatory)
- Scope: tenant (per-tenant posture assessment) + workspace (alerts extend workspace-level infrastructure)
- Primary Routes:
- No new Filament pages in this spec; findings and stored reports are backend data consumed by existing Findings/Alerts UI
- Existing: Monitoring > Findings (extended with
finding_type=permission_posture) - Existing: Monitoring > Alerts (extended with new
EVENT_PERMISSION_MISSINGevent type)
- Data Ownership:
stored_reports-- tenant-owned, generic report storage (new table, workspace_id + tenant_id NOT NULL per SCOPE-001)findings-- tenant-owned, extended withfinding_type=permission_posture(existing table)tenant_permissions-- tenant-owned, source of truth for measured permission state (existing table)alert_rules-- workspace-owned, extended withEVENT_PERMISSION_MISSINGtrigger (existing table)
- RBAC:
- Workspace membership is required for any access (non-members receive 404)
- Viewing posture findings uses existing
FINDINGS_VIEWcapability - Acknowledging posture findings uses existing
FINDINGS_MANAGEcapability - Alert rules for permission events use existing
ALERTS_VIEW/ALERTS_MANAGEcapabilities - No new RBAC capabilities are introduced
Assumptions and Dependencies
- TenantPermissionService already performs live permission checks via Microsoft Graph and persists results to
tenant_permissions. That flow is unchanged; this spec reads its output. - DriftFindingGenerator establishes the fingerprint-based idempotent upsert pattern for findings. The new
PermissionPostureFindingGeneratorfollows the same pattern. - Alerts v1 (Spec 099) provides the generic
AlertDispatchServiceandEvaluateAlertsJobframework. This spec adds a new event type; no structural changes to alerting. config/intune_permissions.phpis the single registry of required permissions (14 entries, alltype: application). The posture generator reads this registry to know what to expect.findings.sourcecolumn was added in a recent migration but is not currently populated. This spec uses it to tag posture findings (source=permission_check).
User Scenarios and Testing (mandatory)
User Story 1 - Generate permission posture findings (Priority: P1)
As a workspace operator, after my tenant's permissions have been checked, I want the system to automatically generate findings for any missing or degraded permissions so that I can see permission gaps alongside drift findings.
Why this priority: This is the core value. Without posture findings, nothing downstream (alerts, reports) has data to work with.
Independent Test: Run the posture finding generator for a tenant with 2 missing permissions; confirm 2 findings of type permission_posture exist with severity, fingerprint, and evidence populated.
Acceptance Scenarios:
- Given a tenant has 14 required permissions and 12 are granted, When the posture finding generator runs, Then 2 findings are created with
finding_type=permission_posture,status=new, andseveritybased on the number of features blocked. - Given a tenant had a missing permission that is now granted, When the posture finding generator runs again, Then the previously-created finding for that permission is auto-resolved (status changes to
resolved,resolved_atandresolved_reasonrecorded). - Given a tenant has all required permissions granted, When the posture finding generator runs, Then no new findings are created; any previously open posture findings are auto-resolved.
- Given an operator has acknowledged a posture finding and the permission is later re-granted, When the posture finding generator runs, Then the finding is auto-resolved (acknowledged status does not block auto-resolve); acknowledgement metadata is preserved.
- Given the posture generator runs twice for the same permission state, When the same missing permissions persist, Then no duplicate findings are created (fingerprint-based idempotency).
- Given a permission finding was auto-resolved (permission was granted) and then the permission is revoked again, When the posture generator runs, Then the existing finding is re-opened (status set to
new,resolved_at/resolved_reasoncleared, evidence updated).
User Story 2 - Persist posture snapshot as a stored report (Priority: P2)
As a workspace operator, I want each permission check to produce a durable posture snapshot (stored report) so that I can track permission health over time and answer "what was the posture at time T?"
Why this priority: Stored reports provide temporal context and form the foundation for future dashboards and trend analysis.
Independent Test: Run the posture check for a tenant; confirm a stored report record exists with the correct report type, payload schema, and associated tenant.
Acceptance Scenarios:
- Given a tenant permission check completes, When the stored report is created, Then it contains the full posture payload (required permissions, granted statuses, computed score, timestamp) and is associated with the tenant and workspace.
- Given multiple posture checks run over time, When I query stored reports for a tenant, Then I can see the history of snapshots ordered by creation date.
- Given the stored reports table has a polymorphic type field, When other spec domains later produce reports, Then they can use the same table without schema changes.
User Story 3 - Alert on missing permissions (Priority: P3)
As a workspace manager, I want to be notified via existing alert channels (Teams/email) when a tenant is missing critical permissions, so I can take corrective action before operations fail.
Why this priority: Alerts close the feedback loop. Operators learn about permission gaps without polling the UI.
Independent Test: Create an alert rule for EVENT_PERMISSION_MISSING, run the posture generator for a tenant with a missing high-impact permission, and confirm a delivery is queued for the matching rule.
Acceptance Scenarios:
- Given an alert rule exists for
EVENT_PERMISSION_MISSINGwith minimum severity = high, When a posture finding of severity high is created, Then the alert dispatch service queues a delivery for each enabled destination on that rule. - Given an alert rule exists for
EVENT_PERMISSION_MISSINGwith minimum severity = critical, When a posture finding of severity high (not critical) is created, Then no delivery is queued for that rule. - Given the same permission is still missing across two posture runs, When the alert is evaluated, Then cooldown/dedupe logic from Alerts v1 prevents duplicate notifications (fingerprint-based suppression).
User Story 4 - Posture score calculation (Priority: P2)
As a workspace operator, I want a normalized posture score (0-100) for each tenant that summarizes how many required permissions are granted versus missing, so I can quickly compare tenant health at a glance.
Why this priority: A single numeric score enables sorting, filtering, and future dashboard widgets.
Independent Test: Check a tenant with 12/14 permissions granted; confirm score = 86 (round(12/14 * 100)). Check a tenant with 14/14; confirm score = 100.
Acceptance Scenarios:
- Given a tenant has N of M required permissions granted, When the posture score is calculated, Then the score equals
round(N / M * 100). - Given the registry has 0 required permissions (edge case), When the score is calculated, Then the score is 100 (no requirements = fully compliant).
- Given the posture score is stored in the report payload, When users query stored reports, Then they can sort/filter tenants by posture score.
Edge Cases
- Graph API unreachable during posture check: If TenantPermissionService returns an error state for a permission, the posture generator does NOT create a missing-permission finding for that key. Instead, it creates a finding with
finding_type=permission_postureandcheck_error=truein evidence (per FR-015), using a distinct error fingerprint. - Registry changes (permissions added/removed): If a new permission is added to
config/intune_permissions.php, the next posture run automatically detects it as missing (for tenants that don't have it). If a permission is removed from the registry, existing findings for that key are auto-resolved on the next run. - Tenant with no provider connection: The posture generator skips tenants without a configured provider connection; no findings or reports are created.
- Concurrent posture runs: Fingerprint-based upsert ensures idempotency. Two concurrent runs for the same tenant produce the same set of findings without duplicates.
- Large workspace with many tenants: The posture check is dispatched per-tenant as a queued job (event-driven after
compare()), not as a blocking batch operation.
Requirements (mandatory)
Constitution alignment (required): This feature introduces long-running/queued work (posture generator job).
- Contract registry: No new Graph contracts; uses existing TenantPermissionService read path.
- Safety gates: Posture generation is a read-only analysis; no write operations to Intune. No confirmation needed.
- Tenant isolation: Findings and stored reports are always scoped to a specific tenant (via
tenant_idFK). Workspace-level queries filter by tenant entitlement. - Run observability: The posture generator runs as a queued job. Its execution is tracked by the existing
OperationRunmechanism withtype=permission_posture_check. Status, outcome, started_at, completed_at are recorded. - Tests: Unit tests for finding generation logic, score calculation, auto-resolve, and alert event production.
Constitution alignment (RBAC-UX): This feature does not introduce new authorization behavior.
- Authorization planes: Tenant-context (
/admin/t/{tenant}/...) for viewing tenant posture findings; workspace-context for alerts. - 404 vs 403: Non-member / not entitled to workspace or tenant scope results in 404. Member missing
FINDINGS_VIEWresults in 403. - Server-side enforcement: Existing FindingPolicy and AlertRule policies apply. No new policies needed.
- Capability registry: Uses
FINDINGS_VIEW,FINDINGS_MANAGE,ALERTS_VIEW,ALERTS_MANAGE-- all existing. - Global search: No new globally searchable resources.
- Destructive actions: None. Posture findings are system-generated, not user-deletable.
- Authorization tests: Exemption — no new policies, gates, or authorization surfaces are introduced. Posture findings use existing
FindingPolicyandFINDINGS_VIEW/FINDINGS_MANAGEcapabilities. Existing authorization tests in Findings resource cover this. If future specs add posture-specific authorization surfaces, add dedicated tests at that time.
Constitution alignment (OPS-EX-AUTH-001): Not applicable -- no OIDC/SAML login flows involved.
Constitution alignment (BADGE-001): This feature introduces new badge values:
finding_type=permission_posture-- new finding type value. Badge rendering uses the centralized finding-type badge map; the new type is added there with icon and color.severityvalues: Uses existinglow,medium,high,critical-- no new severity values.statusvalues: Uses existingnew,acknowledged+ newresolvedstatus for auto-closed findings. Theresolvedstatus is added to the centralized badge map.- Tests cover rendering of
permission_posturetype badge andresolvedstatus badge.
Constitution alignment (Filament Action Surfaces): No new Filament Resources/Pages/RelationManagers are introduced. Posture findings appear in the existing Findings list with the new finding_type filter value. Exemption: No UI Action Matrix needed -- no new action surfaces.
Constitution alignment (UX-001 -- Layout and Information Architecture): No new Filament screens. Posture findings use the existing Findings UI which already complies with UX-001. Exemption: No layout changes to audit.
Functional Requirements
- FR-001 (Stored Reports Table): The system MUST provide a generic
stored_reportstable with a polymorphicreport_typefield, a JSONBpayloadcolumn, and foreign keys totenant_idandworkspace_id. This table is reusable by future spec domains. - FR-002 (Posture Payload Schema): Each stored posture report MUST contain:
report_type=permission_posture, a payload withrequired_permissions(from registry),granted_statuses(per-key status at check time),posture_score(integer 0-100), andchecked_attimestamp. - FR-003 (Posture Score Calculation): The system MUST calculate posture score as
round(granted_count / required_count * 100). Ifrequired_countis 0, score MUST be 100. - FR-004 (Posture Finding Generation): For each permission that is
status=missingafter a tenant permission check, the system MUST create or update a finding withfinding_type=permission_posture, a deterministic fingerprint based ontenant_id + permission_key, and severity derived from the number of features that depend on that permission. Posture findings MUST setsubject_type='permission'andsubject_external_idto the permission key for uniform entity referencing. - FR-005 (Severity Derivation): Finding severity MUST be derived from feature impact:
- Permission blocks 3+ features results in
critical - Permission blocks 2 features results in
high - Permission blocks 1 feature results in
medium - Permission blocks 0 features results in
low
- Permission blocks 3+ features results in
- FR-006 (Finding Evidence): Each posture finding MUST store evidence in
evidence_jsonbcontaining at minimum:permission_key,permission_type,expected_status,actual_status,blocked_features(list), andchecked_at. - FR-007 (Finding Source Tag): Posture findings MUST set
source=permission_checkon thefindingstable to distinguish them from drift findings. - FR-008 (Auto-Resolve): When a previously-missing permission is now granted, the system MUST auto-resolve the corresponding finding by changing its status to
resolvedand recordingresolved_atandresolved_reason=permission_granted. Auto-resolve applies regardless of current status: bothnewandacknowledgedfindings transition toresolved. Acknowledgement metadata (acknowledged_at,acknowledged_by) is preserved on the record for audit. Theresolvedstatus,resolved_at, andresolved_reasoncolumns are global additions to thefindingstable (usable by all finding types, not justpermission_posture). - FR-009 (Idempotent Upsert): The posture generator MUST use fingerprint-based upsert (
firstOrNewontenant_id + fingerprint) to prevent duplicate findings for the same permission on the same tenant. If a resolved finding is matched (permission became missing again), the system MUST re-open it: set status tonew, clearresolved_atandresolved_reason, and updateevidence_jsonbto reflect the current state. - FR-010 (Alert Event): When posture findings are created or re-opened, the system MUST produce
EVENT_PERMISSION_MISSINGevents for the alert dispatch pipeline. Events MUST include tenant_id, permission_key, severity, and a deterministic fingerprint for cooldown/dedupe. Resolved or unchanged findings do NOT produce alert events. - FR-011 (Alert Rule Integration): The
EVENT_PERMISSION_MISSINGevent type MUST be available as an option when creating/editing alert rules in the existing Alerts UI. No new alert pages are needed. - FR-012 (Queued Execution): Posture generation MUST execute as a queued job (per-tenant), dispatched automatically after each
TenantPermissionService::compare()completes (event-driven). No independent schedule or manual trigger is required. - FR-013 (Operation Run Tracking): Each posture generation execution MUST be tracked as an
OperationRunwithtype=permission_posture_check, recording start, completion, outcome (success/failure), and error details. - FR-014 (Tenant Isolation): Findings, stored reports, and operation runs MUST be scoped to a specific tenant via
tenant_id. Cross-tenant data access MUST be impossible at the query level. - FR-015 (Error Handling): If the permission check returns an error state for a specific permission key, the system MUST NOT create a
missingfinding for that key. Instead, it MUST create a finding withfinding_type=permission_postureand error evidence inevidence_jsonbcontaining:check_error=true,error_message(string),permission_key, andchecked_at. Error findings are NOT required to includeexpected_status,actual_status, orblocked_featuresfrom FR-006. The finding uses fingerprintsha256("permission_posture:{tenant_id}:{permission_key}:error")(distinct from the normal missing-permission fingerprint). Severity for error findings defaults tolow. - FR-016 (Skip Unconnected Tenants): The posture generator MUST skip tenants that have no configured provider connection. No findings or reports are created for unconnected tenants.
- FR-017 (Registry as Source): The set of required permissions MUST be read from
config/intune_permissions.phpat runtime. No hardcoded permission lists in the generator. - FR-018 (Retention): Stored reports MUST support configurable retention. Default: 90 days.
UI Action Matrix (mandatory when Filament is changed)
Exemption: This spec does NOT add or modify any Filament Resource, RelationManager, or Page.
Posture findings are rendered via the existing Findings Resource with a new finding_type filter value (permission_posture). The EVENT_PERMISSION_MISSING option is added to the existing Alert Rules form event type dropdown. No new surfaces, actions, or pages are created.
Key Entities (include if feature involves data)
- StoredReport: A generic, tenant-owned report record (workspace_id + tenant_id NOT NULL per SCOPE-001). Polymorphic
report_typedistinguishes domain (e.g.,permission_posture). JSONBpayloadholds the full report data. Supports temporal queries (ordered bycreated_at). - Finding (extended): Existing model extended with
finding_type=permission_postureandsource=permission_check. Each posture finding has a deterministic fingerprint (tenant_id + permission_key), severity derived from feature impact, and structured evidence inevidence_jsonb. Posture findings setsubject_type='permission'andsubject_external_id={permission_key}for uniform entity referencing across finding types. Theresolvedstatus (plusresolved_attimestampTz andresolved_reasonstring columns) is added globally to the Finding model — all finding types (drift, permission_posture, future) can use this lifecycle state. - AlertRule (extended): Existing model gains
EVENT_PERMISSION_MISSINGas a validevent_typeconstant. No schema change; the constant is added to the model and the UI dropdown.
Clarifications
Session 2026-02-21
- Q: What dispatches the posture generation job? → A: Event-driven — dispatched automatically after each
TenantPermissionService::compare()completes. - Q: Does
resolvedstatus apply globally to all finding types or only permission_posture? → A: Global —resolvedis a valid status for ALL finding types (drift, permission_posture, future types). Theresolved_atandresolved_reasoncolumns are added to thefindingstable as nullable columns usable by any finding type. - Q: When a resolved finding's permission becomes missing again, re-open or create new? → A: Re-open — set status back to
new, clearresolved_at/resolved_reason, update evidence to current state. Preserves history and avoids stale resolved records accumulating. - Q: Can an
acknowledgedfinding be auto-resolved when the permission is re-granted? → A: Yes — auto-resolve applies regardless of current status (new→resolvedandacknowledged→resolved). The finding represents a factual state; when the fact changes, the finding is resolved. Acknowledgement history (acknowledged_at,acknowledged_by) is preserved for audit.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001 (Posture coverage): For any tenant with a configured provider connection, 100% of required permissions from the registry are evaluated and their status reflected in the posture report and findings.
- SC-002 (Finding accuracy): The number of open
permission_posturefindings for a tenant exactly matches the number ofmissingpermissions reported by the tenant permission check (no duplicates, no omissions). - SC-003 (Auto-resolve latency): When a previously-missing permission is granted, the corresponding finding is auto-resolved within the next posture check cycle (no manual intervention needed).
- SC-004 (Score reliability): Posture score for a tenant with N of M permissions granted equals
round(N / M * 100). Deterministic and reproducible. - SC-005 (Alert delivery): When a posture finding of qualifying severity is created and an active alert rule matches, a delivery is queued within the standard alert processing time (under 2 minutes per Alerts v1 SLA).
- SC-006 (Temporal audit): An operator can query stored reports to see a tenant's posture at any point in the last 90 days.
- SC-007 (No duplicates): Repeated posture checks for the same permission state on the same tenant produce no duplicate findings (fingerprint idempotency verified).