diff --git a/specs/104-provider-permission-posture/checklists/requirements.md b/specs/104-provider-permission-posture/checklists/requirements.md new file mode 100644 index 0000000..4e21934 --- /dev/null +++ b/specs/104-provider-permission-posture/checklists/requirements.md @@ -0,0 +1,45 @@ +# Specification Quality Checklist: Provider Permission Posture + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-02-26 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Constitution Alignment + +- [x] Constitution alignment (required) -- contract registry, safety gates, tenant isolation, run observability, tests +- [x] Constitution alignment (RBAC-UX) -- authorization planes, 404/403 semantics, capability registry +- [x] Constitution alignment (OPS-EX-AUTH-001) -- not applicable, documented +- [x] Constitution alignment (BADGE-001) -- new badge values documented, centralized map extended +- [x] Constitution alignment (Filament Action Surfaces) -- exemption documented (no new surfaces) +- [x] Constitution alignment (UX-001) -- exemption documented (no new screens) + +## Notes + +- All checklist items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`. +- No [NEEDS CLARIFICATION] markers; all decisions were made with informed defaults based on codebase research (Q1-Q6) and architectural decision validation from prior conversation. +- Key informed defaults documented in Assumptions section: TenantPermissionService as data source, DriftFindingGenerator pattern for idempotent upsert, Alerts v1 generic framework for new event types. diff --git a/specs/104-provider-permission-posture/spec.md b/specs/104-provider-permission-posture/spec.md new file mode 100644 index 0000000..13f69e5 --- /dev/null +++ b/specs/104-provider-permission-posture/spec.md @@ -0,0 +1,187 @@ +# 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).