TenantAtlas/specs/105-entra-admin-roles-evidence-findings/plan.md

196 lines
8.5 KiB
Markdown

# Plan — 105 Entra Admin Roles Evidence + Findings
**Feature Branch**: `105-entra-admin-roles-evidence-findings`
**Created**: 2026-02-21
**Depends on**: Spec 104 (stored_reports, posture patterns), Alerts v1 (099), Findings + OperationRuns
---
## Phase 1 — Infrastructure & Permissions Registry
### 1.1 Add Entra required permissions registry
- Create `config/entra_permissions.php` following `config/intune_permissions.php` schema.
- Declare `RoleManagement.Read.Directory` (type: application, features: `['entra-admin-roles']`).
- Optionally: `Directory.Read.All` as fallback entry (type: application, features: `['entra-admin-roles']`).
### 1.2 Merge registry loader
- Locate where `config('intune_permissions.permissions')` is loaded (TenantPermissionService or equivalent).
- Extend the loader to also read `config('entra_permissions.permissions')` and merge into the combined required-permissions list.
- Existing Intune posture flows must remain unchanged (no breaking change).
- Add test: registry merger returns combined set; existing tests still pass.
### 1.3 Register Graph endpoints in `config/graph_contracts.php`
- Add entries for:
- `GET /roleManagement/directory/roleDefinitions`
- `GET /roleManagement/directory/roleAssignments?$expand=principal`
- Follow existing contract registration patterns (endpoint key, method, URI, scopes).
### 1.4 Add RBAC capabilities
- Add to `App\Support\Auth\Capabilities`:
- `ENTRA_ROLES_VIEW = 'entra_roles.view'`
- `ENTRA_ROLES_MANAGE = 'entra_roles.manage'`
- Update role-to-capability mapping:
- Readonly/Operator → `ENTRA_ROLES_VIEW`
- Manager/Owner → `ENTRA_ROLES_VIEW` + `ENTRA_ROLES_MANAGE`
### 1.5 Add OperationRunType case
- Add `EntraAdminRolesScan = 'entra.admin_roles.scan'` to `App\Support\OperationRunType` enum.
---
## Phase 2 — Core Services
### 2.1 Implement `HighPrivilegeRoleCatalog`
- Create `App\Services\EntraAdminRoles\HighPrivilegeRoleCatalog`.
- Hardcoded mapping of template_id → (display_name, severity).
- Public methods:
- `isHighPrivilege(string $templateId, ?string $displayName): bool`
- `severityFor(string $templateId, ?string $displayName): ?string`
- `allTemplateIds(): array`
- `globalAdminTemplateId(): string`
- Fallback: if `template_id` not in catalog, check `display_name`.
- Full unit test coverage.
### 2.2 Implement `EntraAdminRolesReportService`
- Create `App\Services\EntraAdminRoles\EntraAdminRolesReportService`.
- Responsibilities:
1. Fetch `roleDefinitions` via Graph (using registered contract).
2. Fetch `roleAssignments?$expand=principal` via Graph.
3. Build payload per spec (role_definitions, role_assignments, totals, high_privilege).
4. Compute fingerprint: SHA-256 of sorted `(role_template_or_id, principal_id, scope_id)` tuples.
5. Determine `previous_fingerprint` from latest existing report for this tenant.
6. Dedupe: if current fingerprint == latest report's fingerprint, skip creation.
7. Create `StoredReport` with `report_type=entra.admin_roles`.
- Add `REPORT_TYPE_ENTRA_ADMIN_ROLES = 'entra.admin_roles'` constant to `StoredReport` model.
- Tests: report creation, dedupe, fingerprint computation, previous_fingerprint chaining.
### 2.3 Implement `EntraAdminRolesFindingGenerator`
- Create `App\Services\EntraAdminRoles\EntraAdminRolesFindingGenerator`.
- Responsibilities:
1. Accept report payload (or structured DTO).
2. For each role assignment where role is high-privilege (per `HighPrivilegeRoleCatalog`):
- Compute fingerprint: `entra_admin_role:{tenant_id}:{role_template_or_id}:{principal_id}:{scope_id}`
- Upsert finding: `finding_type=entra_admin_roles`, `source=entra.admin_roles`
- Set severity per catalog (critical for GA, high otherwise)
- Populate `evidence_jsonb` with role, principal, scope details
- Update `times_seen`, `last_seen_at` on existing findings
3. Aggregate check: count GA principals > threshold (5) → aggregate finding with distinct fingerprint.
4. Auto-resolve: query open findings for this tenant+source not in current scan's fingerprint set → resolve.
5. Re-open: if a resolved finding's fingerprint matches current data → set status=new, clear resolved fields.
6. Collect alert events for new/re-opened findings.
- Add `FINDING_TYPE_ENTRA_ADMIN_ROLES = 'entra_admin_roles'` constant to `Finding` model.
- Tests: creation, idempotency, auto-resolve, re-open, aggregate finding, alert events.
---
## Phase 3 — Job & Scheduling
### 3.1 Implement `ScanEntraAdminRolesJob`
- Create `App\Jobs\EntraAdminRoles\ScanEntraAdminRolesJob` (implements `ShouldQueue`).
- Lifecycle:
1. Check tenant has provider connection; skip if not.
2. Create OperationRun (`entra.admin_roles.scan`, status=running).
3. Call `EntraAdminRolesReportService::generate()`.
4. Call `EntraAdminRolesFindingGenerator::generate()`.
5. Dispatch alert events via `AlertDispatchService`.
6. Mark OperationRun completed (success/failure with error details).
- Active-run uniqueness: check for existing running OperationRun before starting.
- Tests: job dispatching, OperationRun lifecycle, skip on no connection, failure handling.
### 3.2 Add to workspace dispatcher
- Register `ScanEntraAdminRolesJob` in the daily workspace dispatch schedule.
- Same pattern as existing scheduled scans: iterate tenants with active connections, dispatch per tenant.
---
## Phase 4 — Alerts Integration
### 4.1 Add alert event type
- Add constant `EVENT_ENTRA_ADMIN_ROLES_HIGH = 'entra.admin_roles.high'` to `AlertRule` model.
- Add to event type options array (used in alert rule form dropdown).
### 4.2 Extend `EvaluateAlertsJob`
- Add a new producer section in `EvaluateAlertsJob` that:
- Queries findings where `source=entra.admin_roles`, `severity>=high`, `status` in (`new`).
- Produces events with `event_type=entra.admin_roles.high`.
- Uses finding fingerprint as `fingerprint_key` for cooldown/dedupe.
- Tests: alert rule matching, delivery creation, cooldown.
---
## Phase 5 — Badge Catalog
### 5.1 Add `entra_admin_roles` to `FindingTypeBadge`
- Extend `App\Support\Badges\Domains\FindingTypeBadge` mapper with:
- `Finding::FINDING_TYPE_ENTRA_ADMIN_ROLES => new BadgeSpec('Entra admin roles', 'danger', 'heroicon-m-shield-exclamation')`
- Test: badge renders correctly for new type.
---
## Phase 6 — UI
### 6.1 Tenant card widget "Admin Roles"
- Create a Livewire/Filament widget for the tenant dashboard.
- Displays: last scan timestamp, high-privilege assignment count.
- CTA: "Scan now" (visible only if user has `ENTRA_ROLES_MANAGE`).
- Link: "View latest report" (stored reports viewer filtered by `entra.admin_roles`).
- Empty state: "No scan performed" + gated "Scan now" CTA.
### 6.2 Report viewer enhancements
- Extend existing stored reports viewer to handle `report_type=entra.admin_roles`:
- Summary section showing totals.
- Table of high-privilege role assignments (principal display name, type, role, scope).
- Filter by `report_type` on report listing.
---
## Phase 7 — Tests
### 7.1 Unit tests
- `HighPrivilegeRoleCatalog`: classification by template_id, fallback to display_name, unknown role returns false.
- `EntraAdminRolesReportService`: report creation, fingerprint computation, dedupe, previous_fingerprint.
- `EntraAdminRolesFindingGenerator`: finding creation, severity assignment, idempotent upsert, auto-resolve, re-open, aggregate finding.
- Registry merge: combined Intune + Entra permissions.
### 7.2 Feature tests
- `ScanEntraAdminRolesJob`: full lifecycle (report + findings + alerts), skip unconnected, failure handling.
- Alerts: event type matching, delivery creation, cooldown.
- RBAC: `ENTRA_ROLES_VIEW` can view; `ENTRA_ROLES_MANAGE` can scan; missing capability → 403; non-member → 404.
### 7.3 Posture integration smoke test
- With Entra permissions in merged registry, posture generator includes them in score calculation.
### 7.4 Run full suite
- `vendor/bin/sail artisan test --compact` — all tests green.
- `vendor/bin/sail bin pint --dirty` — code style clean.
---
## Filament v5 Compliance Notes
1. **Livewire v4.0+**: All widget components are Livewire v4 compatible.
2. **Provider registration**: No new panel providers — existing admin panel in `bootstrap/providers.php`.
3. **Global search**: No new globally searchable resources.
4. **Destructive actions**: "Scan now" is non-destructive (read-only Graph call); no `requiresConfirmation()` needed.
5. **Asset strategy**: No new heavy assets. Widget uses standard Filament components.
6. **Testing plan**: Widget tested as Livewire component; finding generator + report service unit tested; job feature tested; RBAC positive/negative tests included.