TenantAtlas/specs/104-provider-permission-posture/spec.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

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_MISSING event 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 with finding_type=permission_posture (existing table)
    • tenant_permissions -- tenant-owned, source of truth for measured permission state (existing table)
    • alert_rules -- workspace-owned, extended with EVENT_PERMISSION_MISSING trigger (existing table)
  • RBAC:
    • Workspace membership is required for any access (non-members receive 404)
    • Viewing posture findings uses existing FINDINGS_VIEW capability
    • Acknowledging posture findings uses existing FINDINGS_MANAGE capability
    • Alert rules for permission events use existing ALERTS_VIEW / ALERTS_MANAGE capabilities
    • 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 PermissionPostureFindingGenerator follows the same pattern.
  • Alerts v1 (Spec 099) provides the generic AlertDispatchService and EvaluateAlertsJob framework. This spec adds a new event type; no structural changes to alerting.
  • config/intune_permissions.php is the single registry of required permissions (14 entries, all type: application). The posture generator reads this registry to know what to expect.
  • findings.source column 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:

  1. 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, and severity based on the number of features blocked.
  2. 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_at and resolved_reason recorded).
  3. 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.
  4. 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.
  5. 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).
  6. 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_reason cleared, 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:

  1. 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.
  2. 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.
  3. 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:

  1. Given an alert rule exists for EVENT_PERMISSION_MISSING with 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.
  2. Given an alert rule exists for EVENT_PERMISSION_MISSING with minimum severity = critical, When a posture finding of severity high (not critical) is created, Then no delivery is queued for that rule.
  3. 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:

  1. Given a tenant has N of M required permissions granted, When the posture score is calculated, Then the score equals round(N / M * 100).
  2. Given the registry has 0 required permissions (edge case), When the score is calculated, Then the score is 100 (no requirements = fully compliant).
  3. 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_posture and check_error=true in 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_id FK). Workspace-level queries filter by tenant entitlement.
  • Run observability: The posture generator runs as a queued job. Its execution is tracked by the existing OperationRun mechanism with type=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_VIEW results 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 FindingPolicy and FINDINGS_VIEW/FINDINGS_MANAGE capabilities. 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.
  • severity values: Uses existing low, medium, high, critical -- no new severity values.
  • status values: Uses existing new, acknowledged + new resolved status for auto-closed findings. The resolved status is added to the centralized badge map.
  • Tests cover rendering of permission_posture type badge and resolved status 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_reports table with a polymorphic report_type field, a JSONB payload column, and foreign keys to tenant_id and workspace_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 with required_permissions (from registry), granted_statuses (per-key status at check time), posture_score (integer 0-100), and checked_at timestamp.
  • FR-003 (Posture Score Calculation): The system MUST calculate posture score as round(granted_count / required_count * 100). If required_count is 0, score MUST be 100.
  • FR-004 (Posture Finding Generation): For each permission that is status=missing after a tenant permission check, the system MUST create or update a finding with finding_type=permission_posture, a deterministic fingerprint based on tenant_id + permission_key, and severity derived from the number of features that depend on that permission. Posture findings MUST set subject_type='permission' and subject_external_id to 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
  • FR-006 (Finding Evidence): Each posture finding MUST store evidence in evidence_jsonb containing at minimum: permission_key, permission_type, expected_status, actual_status, blocked_features (list), and checked_at.
  • FR-007 (Finding Source Tag): Posture findings MUST set source=permission_check on the findings table 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 resolved and recording resolved_at and resolved_reason=permission_granted. Auto-resolve applies regardless of current status: both new and acknowledged findings transition to resolved. Acknowledgement metadata (acknowledged_at, acknowledged_by) is preserved on the record for audit. The resolved status, resolved_at, and resolved_reason columns are global additions to the findings table (usable by all finding types, not just permission_posture).
  • FR-009 (Idempotent Upsert): The posture generator MUST use fingerprint-based upsert (firstOrNew on tenant_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 to new, clear resolved_at and resolved_reason, and update evidence_jsonb to reflect the current state.
  • FR-010 (Alert Event): When posture findings are created or re-opened, the system MUST produce EVENT_PERMISSION_MISSING events 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_MISSING event 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 OperationRun with type=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 missing finding for that key. Instead, it MUST create a finding with finding_type=permission_posture and error evidence in evidence_jsonb containing: check_error=true, error_message (string), permission_key, and checked_at. Error findings are NOT required to include expected_status, actual_status, or blocked_features from FR-006. The finding uses fingerprint sha256("permission_posture:{tenant_id}:{permission_key}:error") (distinct from the normal missing-permission fingerprint). Severity for error findings defaults to low.
  • 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.php at 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_type distinguishes domain (e.g., permission_posture). JSONB payload holds the full report data. Supports temporal queries (ordered by created_at).
  • Finding (extended): Existing model extended with finding_type=permission_posture and source=permission_check. Each posture finding has a deterministic fingerprint (tenant_id + permission_key), severity derived from feature impact, and structured evidence in evidence_jsonb. Posture findings set subject_type='permission' and subject_external_id={permission_key} for uniform entity referencing across finding types. The resolved status (plus resolved_at timestampTz and resolved_reason string 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_MISSING as a valid event_type constant. 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 resolved status apply globally to all finding types or only permission_posture? → A: Global — resolved is a valid status for ALL finding types (drift, permission_posture, future types). The resolved_at and resolved_reason columns are added to the findings table 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, clear resolved_at/resolved_reason, update evidence to current state. Preserves history and avoids stale resolved records accumulating.
  • Q: Can an acknowledged finding be auto-resolved when the permission is re-granted? → A: Yes — auto-resolve applies regardless of current status (newresolved and acknowledgedresolved). 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_posture findings for a tenant exactly matches the number of missing permissions 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).