TenantAtlas/specs/104-provider-permission-posture/spec.md
Ahmed Darrazi dced3f1ed2 spec: 104 Provider Permission Posture - initial spec
- StoredReports foundation (generic table, polymorphic report_type)
- Permission Posture Findings generation (fingerprint-based, auto-resolve)
- Alerts integration (EVENT_PERMISSION_MISSING event type)
- Posture score calculation (0-100 normalized)
- 18 functional requirements, 7 success criteria
- 4 user stories (P1-P3), 5 edge cases
- Full constitution alignment documented
- Quality checklist: all items pass
2026-02-21 01:48:09 +01:00

18 KiB

Feature Specification: Provider Permission Posture

Feature Branch: 104-provider-permission-posture Created: 2026-02-26 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 (stored reports and 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 -- workspace-owned, generic report storage (new table)
    • 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 from new to resolved).
  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 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).

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 records the permission as status=error in evidence but does NOT create a missing-permission finding for that key. It generates a separate permission_check_error finding instead.
  • 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, 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: Positive test: user with FINDINGS_VIEW can see posture findings. Negative test: user without tenant entitlement receives 404.

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.
  • 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.
  • 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.
  • FR-010 (Alert Event): When posture findings are created or updated, 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.
  • 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) to avoid blocking user-facing requests.
  • 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 separate finding with evidence indicating the check failed.
  • 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, workspace-scoped report record. Polymorphic report_type distinguishes domain (e.g., permission_posture). JSONB payload holds the full report data. Associated with a tenant_id and workspace_id. 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. Supports resolved status for auto-closed findings.
  • 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.

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