- 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
188 lines
18 KiB
Markdown
188 lines
18 KiB
Markdown
# 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).
|