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