# Tasks: Entra Admin Roles Evidence + Findings

**Input**: Design documents from `/specs/105-entra-admin-roles-evidence-findings/`
**Prerequisites**: plan.md (required), spec.md (required), research.md, data-model.md, contracts/internal-services.md, quickstart.md

**Tests**: For runtime behavior changes in this repo, tests are REQUIRED (Pest). All user stories include test tasks.
**Operations**: This feature introduces queued work (`ScanEntraAdminRolesJob`). Tasks include `OperationRun` creation via `OperationRunService::ensureRunWithIdentity()` with type `entra.admin_roles.scan`, and outcome tracking per constitution.
**RBAC**: New capabilities introduced: `ENTRA_ROLES_VIEW` and `ENTRA_ROLES_MANAGE`. Tasks include:
- Capability constants in `App\Support\Auth\Capabilities` (no raw strings)
- `RoleCapabilityMap` mapping: Readonly/Operator → VIEW; Manager/Owner → MANAGE
- Widget `canView()` gated by `ENTRA_ROLES_VIEW`; "Scan now" action gated by `ENTRA_ROLES_MANAGE` server-side
- 404 vs 403 semantics: non-member/not entitled → 404; member missing capability → 403
- Authorization plane: tenant-context (`/admin/t/{tenant}/...`)
- No new globally searchable resources. Existing search behavior unchanged.
- No destructive actions — scan is read-only, no `->requiresConfirmation()` needed
- Positive + negative authorization tests in widget test (US5)
**Filament UI Action Surfaces**: **Partial exemption** — no new Resources/Pages/RelationManagers. Only changes: (1) new tenant card widget with "Scan now" header action, (2) new option added to `AlertRuleResource` event type dropdown. Widget is not a full Resource — Action Surface Contract does not apply to simple stat/card widgets (documented in spec UI Action Matrix).
**Filament UI UX-001**: **Exemption** — no new Create/Edit/View pages. Widget follows existing card conventions. Report viewer reuses existing stored reports viewer.
**Badges**: Adds `entra_admin_roles` finding type to `FindingTypeBadge` per BADGE-001 (`BadgeSpec('Entra admin roles', 'danger', 'heroicon-m-identification')`). Tests included.

**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.

## Format: `[ID] [P?] [Story] Description`

- **[P]**: Can run in parallel (different files, no dependencies on incomplete tasks)
- **[Story]**: Which user story this task belongs to (US1, US2, US3, US4, US5, US6)
- Include exact file paths in descriptions

---

## Phase 1: Setup

**Purpose**: No project setup needed — existing Laravel project with all framework dependencies.

*Phase skipped.*

---

## Phase 2: Foundation (Blocking Prerequisites)

**Purpose**: Migration, config files, model constants, enum cases, capability registry, badge mappings, and factory states that ALL user stories depend on. Maps to plan Phase A.

**⚠️ CRITICAL**: No user story work can begin until this phase is complete.

### Migration

- [X] T001 Create migration for adding `fingerprint` (string(64), nullable) and `previous_fingerprint` (string(64), nullable) columns to `stored_reports` table via `vendor/bin/sail artisan make:migration add_fingerprint_to_stored_reports_table --no-interaction`. Add unique index on `[tenant_id, report_type, fingerprint]` and index on `[tenant_id, report_type, created_at DESC]`. Columns are nullable because existing `permission_posture` reports (Spec 104) don't use fingerprinting. See `specs/105-entra-admin-roles-evidence-findings/data-model.md` migration section.

### Config Files

- [X] T002 [P] Create `config/entra_permissions.php` with permissions array containing `RoleManagement.Read.Directory` (type: application, features: `['entra-admin-roles']`). Follow exact schema of existing `config/intune_permissions.php`. See `specs/105-entra-admin-roles-evidence-findings/data-model.md` config section.
- [X] T003 [P] Add `entraRoleDefinitions` and `entraRoleAssignments` entries to `config/graph_contracts.php`. `entraRoleDefinitions`: resource `roleManagement/directory/roleDefinitions`, allowed_select `[id, displayName, templateId, isBuiltIn]`. `entraRoleAssignments`: resource `roleManagement/directory/roleAssignments`, allowed_select `[id, roleDefinitionId, principalId, directoryScopeId]`, allowed_expand `[principal]`. See `specs/105-entra-admin-roles-evidence-findings/data-model.md` graph contracts section.

### Model & Enum Constants

- [X] T004 [P] Extend `StoredReport` model in `app/Models/StoredReport.php`: add `REPORT_TYPE_ENTRA_ADMIN_ROLES = 'entra.admin_roles'` constant, add `fingerprint` and `previous_fingerprint` to `$fillable` array. See `specs/105-entra-admin-roles-evidence-findings/data-model.md` StoredReport section.
- [X] T005 [P] Extend `Finding` model in `app/Models/Finding.php`: add `FINDING_TYPE_ENTRA_ADMIN_ROLES = 'entra_admin_roles'` constant. No method changes — existing `resolve()`, `reopen()`, and fingerprint-based lookup are reused from Spec 104.
- [X] T006 [P] Extend `AlertRule` model in `app/Models/AlertRule.php`: add `EVENT_ENTRA_ADMIN_ROLES_HIGH = 'entra.admin_roles.high'` constant.
- [X] T007 [P] Add `EntraAdminRolesScan = 'entra.admin_roles.scan'` case to `app/Support/OperationRunType.php` enum.

### Capability Registry

- [X] T008 [P] Add `ENTRA_ROLES_VIEW = 'entra_roles.view'` and `ENTRA_ROLES_MANAGE = 'entra_roles.manage'` constants to `app/Support/Auth/Capabilities.php`.
- [X] T009 [P] Update `app/Support/Auth/RoleCapabilityMap.php` to map new capabilities: Readonly/Operator → `ENTRA_ROLES_VIEW`; Manager/Owner → `ENTRA_ROLES_VIEW` + `ENTRA_ROLES_MANAGE`. Follow existing mapping patterns in the file.

### Badge Mapping

- [X] T010 [P] Add `entra_admin_roles` mapping to `app/Support/Badges/Domains/FindingTypeBadge.php`: `Finding::FINDING_TYPE_ENTRA_ADMIN_ROLES => new BadgeSpec('Entra admin roles', 'danger', 'heroicon-m-identification')`. Follow existing pattern from `permission_posture` and `drift` mappings.

### Factory State

- [X] T011 [P] Add `entraAdminRoles()` factory state to `database/factories/FindingFactory.php`: sets `finding_type → Finding::FINDING_TYPE_ENTRA_ADMIN_ROLES`, `source → 'entra.admin_roles'`, `severity → Finding::SEVERITY_CRITICAL`, `subject_type → 'role_assignment'`, sample evidence in `evidence_jsonb` with `role_display_name`, `principal_display_name`, `principal_type`, `principal_id`, `role_definition_id`, `directory_scope_id`, `is_built_in`, `measured_at`.

### Verify & Test Foundation

- [X] T012 Run migration via `vendor/bin/sail artisan migrate` and verify `stored_reports` table has `fingerprint` and `previous_fingerprint` columns, unique index on `[tenant_id, report_type, fingerprint]`, and index on `[tenant_id, report_type, created_at DESC]`.
- [X] T013 [P] Write StoredReport fingerprint tests in `tests/Feature/EntraAdminRoles/StoredReportFingerprintTest.php`: (1) `fingerprint` and `previous_fingerprint` columns are fillable and persist correctly, (2) unique index prevents duplicate `(tenant_id, report_type, fingerprint)` combinations, (3) nullable columns allow null values for existing reports without fingerprints.
- [X] T014 [P] Write badge rendering test for `entra_admin_roles` type in `tests/Feature/EntraAdminRoles/FindingTypeBadgeTest.php` (or extend existing badge test file): `entra_admin_roles` renders with `danger` color and `heroicon-m-identification` icon.
- [X] T015 [P] Write capabilities registry test in `tests/Feature/EntraAdminRoles/EntraPermissionsRegistryTest.php`: (1) `Capabilities::ENTRA_ROLES_VIEW` and `Capabilities::ENTRA_ROLES_MANAGE` constants exist, (2) `RoleCapabilityMap` maps VIEW to Readonly/Operator, MANAGE to Manager/Owner.

**Checkpoint**: Foundation ready — migration, config, constants, capabilities, badge, and factory in place. User story implementation can begin.

---

## Phase 3: User Story 1 — Scan & Evidence Snapshot (Priority: P1) 🎯 MVP

**Goal**: Fetch Entra directory role data from Graph, classify high-privilege roles, and persist as a fingerprinted stored report with deduplication. Maps to plan Phase B.

**Independent Test**: Trigger report generation for a tenant; confirm a `stored_report` exists with `report_type=entra.admin_roles`, valid payload (role_definitions, role_assignments, totals, high_privilege), and content-based fingerprint.

### Implementation for User Story 1

- [X] T016 [P] [US1] Create `HighPrivilegeRoleCatalog` in `app/Services/EntraAdminRoles/HighPrivilegeRoleCatalog.php` with static `CATALOG` map (6 template_id → severity pairs) and `DISPLAY_NAME_FALLBACK` map (case-insensitive). Methods: `classify(string $templateIdOrId, ?string $displayName): ?string` (returns severity or null), `isHighPrivilege(...)`: bool, `isGlobalAdministrator(...)`: bool, `allTemplateIds(): array`. Classification prefers `template_id`, falls back to `display_name`. See `specs/105-entra-admin-roles-evidence-findings/data-model.md` HighPrivilegeRoleCatalog section and spec FR-006.
- [X] T017 [P] [US1] Create `EntraAdminRolesReportResult` value object in `app/Services/EntraAdminRoles/EntraAdminRolesReportResult.php` with `readonly` properties: `bool $created`, `?int $storedReportId`, `string $fingerprint`, `array $payload`. See `specs/105-entra-admin-roles-evidence-findings/data-model.md`.
- [X] T018 [US1] Create `EntraAdminRolesReportService` in `app/Services/EntraAdminRoles/EntraAdminRolesReportService.php` with constructor injecting `GraphClientInterface` and `HighPrivilegeRoleCatalog`. Method `generate(Tenant $tenant, ?OperationRun $operationRun = null): EntraAdminRolesReportResult` must: (1) fetch `roleDefinitions` via Graph contract `entraRoleDefinitions`, (2) fetch `roleAssignments` with `$expand=principal` via contract `entraRoleAssignments`, (3) build payload per FR-005 schema (provider_key, domain, measured_at, role_definitions, role_assignments, totals, high_privilege), (4) compute fingerprint: SHA-256 of sorted `"{role_template_or_id}:{principal_id}:{scope_id}"` tuples joined by `\n`, (5) check if latest report for `(tenant_id, report_type=entra.admin_roles)` has same fingerprint → return `created=false` if match, (6) create `StoredReport` with `previous_fingerprint` from latest existing report. All-or-nothing: if either Graph call fails, throw (no partial report). See `specs/105-entra-admin-roles-evidence-findings/contracts/internal-services.md` Service 1.

### Tests for User Story 1

- [X] T019 [P] [US1] Write HighPrivilegeRoleCatalog tests in `tests/Feature/EntraAdminRoles/HighPrivilegeRoleCatalogTest.php`: (1) classify Global Administrator template_id → `critical`, (2) classify all 5 other high-privilege template_ids → `high`, (3) display_name fallback for role without template_id match, (4) unknown template_id + unknown display_name → null, (5) null display_name with unknown template_id → null, (6) `isGlobalAdministrator()` returns true only for GA template_id, (7) `allTemplateIds()` returns all 6 entries, (8) display_name matching is case-insensitive.
- [X] T020 [US1] Write EntraAdminRolesReportService tests in `tests/Feature/EntraAdminRoles/EntraAdminRolesReportServiceTest.php`: (1) new report created with correct `report_type=entra.admin_roles`, payload schema per FR-005, and fingerprint, (2) dedup on identical fingerprint — second call returns `created=false` with existing report ID, (3) changed data → new report with different fingerprint and `previous_fingerprint` chain, (4) Graph roleDefinitions failure → exception thrown, no partial report (all-or-nothing), (5) Graph roleAssignments failure → exception thrown, no partial report, (6) payload contains role_definitions, role_assignments, totals (roles_total, assignments_total, high_privilege_assignments), and high_privilege section, (7) fingerprint is deterministic regardless of Graph response ordering, (8) tenant with zero role assignments → valid report with empty assignments and totals.

**Checkpoint**: US1 complete — Entra admin role data is fetched from Graph, classified, fingerprinted, and persisted as stored reports with deduplication. This is the evidence pipeline.

---

## Phase 4: User Story 2 — Generate Findings for High-Privilege Assignments (Priority: P1)

**Goal**: Generate, upsert, auto-resolve, and re-open findings based on report data. Create aggregate "Too many Global Admins" finding when threshold exceeded. Wire everything via the scan job with OperationRun tracking and scheduling. Maps to plan Phases C + D.

**Independent Test**: Run the finding generator for a tenant with a Global Administrator assignment; confirm a finding with `finding_type=entra_admin_roles`, `severity=critical`, correct fingerprint and evidence exists.

### Implementation for User Story 2

- [X] T021 [P] [US2] Create `EntraAdminRolesFindingResult` value object in `app/Services/EntraAdminRoles/EntraAdminRolesFindingResult.php` with `readonly` properties: `int $created`, `int $resolved`, `int $reopened`, `int $unchanged`, `int $alertEventsProduced`. See `specs/105-entra-admin-roles-evidence-findings/data-model.md`.
- [X] T022 [US2] Create `EntraAdminRolesFindingGenerator` in `app/Services/EntraAdminRoles/EntraAdminRolesFindingGenerator.php` with constructor injecting `HighPrivilegeRoleCatalog`. Method `generate(Tenant $tenant, array $reportPayload, ?OperationRun $operationRun = null): EntraAdminRolesFindingResult` must: (1) iterate `role_assignments` from payload, classify each via catalog, (2) for each high-privilege assignment: create finding via fingerprint upsert (`firstOrNew` on `[tenant_id, fingerprint]`), fingerprint = `substr(hash('sha256', "entra_admin_role:{tenant_id}:{role_template_or_id}:{principal_id}:{scope_id}"), 0, 64)`, (3) set severity: `critical` for Global Admin, `high` for others, (4) set evidence per FR-009 schema, `subject_type='role_assignment'`, `subject_external_id="{principal_id}:{role_definition_id}"`, (5) auto-resolve stale findings: query open `entra_admin_roles` findings whose fingerprint NOT in current scan → `resolve('role_assignment_removed')`, (6) re-open resolved findings when fingerprint matches current scan, (7) aggregate finding: when GA count > 5 → create "Too many Global Admins" finding with fingerprint `substr(hash('sha256', "entra_admin_role_ga_count:{tenant_id}"), 0, 64)`, severity=high, auto-resolve when ≤ 5, (8) produce alert events for new/re-opened findings with severity ≥ high. Method `getAlertEvents(): array` returns collected events. See `specs/105-entra-admin-roles-evidence-findings/contracts/internal-services.md` Service 2.
- [X] T023 [US2] Create `ScanEntraAdminRolesJob` in `app/Jobs/ScanEntraAdminRolesJob.php` implementing `ShouldQueue`. Constructor: `int $tenantId`, `int $workspaceId`, `?int $initiatorUserId = null`. `handle()` must: (1) resolve Tenant, check for active provider connection → return early if none (no OperationRun, no error per FR-018), (2) create OperationRun via `OperationRunService::ensureRunWithIdentity()` with type `entra.admin_roles.scan`, (3) call `EntraAdminRolesReportService::generate()`, (4) call `EntraAdminRolesFindingGenerator::generate()` with report payload (regardless of whether report was new or deduped — finding generator handles auto-resolve for stale findings from removed assignments), (5) record success on OperationRun with counts from both results, (6) on Graph error → record failure with sanitized error message, re-throw for retry. See `specs/105-entra-admin-roles-evidence-findings/contracts/internal-services.md` Job section.
- [X] T024 [US2] Register daily scan schedule in `routes/console.php`: iterate workspaces → tenants with active provider connections → dispatch `ScanEntraAdminRolesJob` per tenant. Follow existing scheduled scan patterns in the file. See plan Phase D (scheduling) and FR-016.

### Tests for User Story 2

- [X] T025 [US2] Write EntraAdminRolesFindingGenerator tests in `tests/Feature/EntraAdminRoles/EntraAdminRolesFindingGeneratorTest.php`: (1) creates individual findings for high-privilege assignments with correct type/severity/fingerprint/evidence/source, (2) severity mapping: Global Admin → critical, others → high, (3) idempotent upsert — same data across scans → no duplicates, `times_seen` and `last_seen_at` updated, (4) auto-resolve on removed assignment (finding status → resolved, resolved_reason = `role_assignment_removed`), (5) re-open resolved finding when role re-assigned (status → new, resolved_at/resolved_reason cleared, evidence updated), (6) aggregate "Too many Global Admins" finding created when GA count > 5 with correct fingerprint/severity/evidence, (7) aggregate finding auto-resolved when GA count ≤ 5, (8) alert events produced for new/re-opened findings with severity ≥ high, (9) no alert events for unchanged or resolved findings, (10) evidence schema includes role_display_name, principal_display_name, principal_type, principal_id, role_definition_id, directory_scope_id, is_built_in, measured_at, (11) handles all principal types (user, group, servicePrincipal), (12) subject_type='role_assignment' and subject_external_id='{principal_id}:{role_definition_id}' set on every finding, (13) stale findings for assignments no longer in scan are auto-resolved (catches removed assignments), (14) auto-resolve applies to both `new` and `acknowledged` findings (acknowledged metadata preserved), (15) scoped assignments (`directory_scope_id != '/'`) do not downgrade severity and the scope is captured in evidence.
- [X] T026 [US2] Write ScanEntraAdminRolesJob tests in `tests/Feature/EntraAdminRoles/ScanEntraAdminRolesJobTest.php`: (1) successful run creates OperationRun with type=entra.admin_roles.scan and records success with counts, (2) skips tenant without active provider connection — no OperationRun, no findings, no report, (3) Graph failure → OperationRun marked failed with sanitized error, job re-thrown for retry, (4) finding generator called even when report was deduped (handles auto-resolve), (5) OperationRun uniqueness enforced per (workspace_id, tenant_id, run_type).

**Checkpoint**: US1 + US2 complete — full scan pipeline operational: Graph fetch → stored report → findings → auto-resolve → re-open → aggregate. This is the core MVP.

---

## Phase 5: User Story 4 — Entra Permissions in Permission Posture (Priority: P2)

**Goal**: Integrate Entra-specific Graph permissions into the existing permission posture pipeline so posture scores reflect whether tenants can run admin role scans. Maps to plan Phase D (TenantPermissionService merge).

**Independent Test**: Verify that after loading the merged registry (Intune + Entra), `RoleManagement.Read.Directory` appears in the required permissions list. Verify posture score computation includes it.

### Implementation for User Story 4

- [X] T027 [US4] Modify `app/Services/Intune/TenantPermissionService.php` method `getRequiredPermissions()` to merge `config('entra_permissions.permissions', [])` alongside existing `config('intune_permissions.permissions', [])`. The merge must be non-breaking (existing Intune posture flows unchanged). See `specs/105-entra-admin-roles-evidence-findings/contracts/internal-services.md` Modified: TenantPermissionService section.

### Tests for User Story 4

- [X] T028 [US4] Extend or create tests in `tests/Feature/EntraAdminRoles/EntraPermissionsRegistryTest.php`: (1) merged required permissions list includes both Intune and Entra entries, (2) `RoleManagement.Read.Directory` appears in merged list with correct type and features, (3) existing Intune permissions unchanged after merge, (4) empty `entra_permissions.permissions` config → merge returns only Intune entries (non-breaking fallback), (5) posture score reflects the Entra permission gap (missing `RoleManagement.Read.Directory` results in score < 100).

**Checkpoint**: US4 complete — posture scores accurately reflect Entra permission gaps.

---

## Phase 6: User Story 3 — Alert on High-Privilege Admin Role Events (Priority: P2)

**Goal**: Connect admin roles findings to the existing alert pipeline so operators receive notifications for new high-privilege assignments. Maps to plan Phase E.

**Independent Test**: Create an alert rule for `entra.admin_roles.high` with min severity = high, run the finding generator with a new Global Admin assignment, and confirm a delivery is queued.

### Implementation for User Story 3

- [X] T029 [US3] Add `entraAdminRolesHighEvents(int $workspaceId, CarbonImmutable $windowStart): array` method to `app/Jobs/Alerts/EvaluateAlertsJob.php` — queries `Finding` where `finding_type='entra_admin_roles'`, `status IN ('new')`, `severity IN ('high', 'critical')`, `updated_at > $windowStart`. Returns event arrays matching existing event schema (`event_type=entra.admin_roles.high`, `fingerprint_key=finding:{id}`). Wire the method into `handle()` event collection alongside existing `highDriftEvents()`, `compareFailedEvents()`, and `permissionMissingEvents()`. Follow same pattern as existing event methods.
- [X] T030 [P] [US3] Add `EVENT_ENTRA_ADMIN_ROLES_HIGH` option to event type dropdown in `app/Filament/Resources/AlertRuleResource.php`: `AlertRule::EVENT_ENTRA_ADMIN_ROLES_HIGH => 'Entra admin roles (high privilege)'`.

### Tests for User Story 3

- [X] T031 [US3] Write alert integration tests in `tests/Feature/EntraAdminRoles/AdminRolesAlertIntegrationTest.php`: (1) alert rule for EVENT_ENTRA_ADMIN_ROLES_HIGH with min severity = high + new finding of severity = critical → delivery queued, (2) alert rule with min severity = critical + finding of severity = high → no delivery queued, (3) cooldown/dedupe prevents duplicate notifications for same finding across scans (fingerprint_key-based suppression), (4) resolved findings do not produce alert events, (5) new event type appears in AlertRuleResource event type options.

**Checkpoint**: US3 complete — operators receive alerts for high-privilege admin role events via existing alert channels.

---

## Phase 7: User Story 5 — Dashboard Widget (Priority: P3) + User Story 6 — Report Viewer (Priority: P3)

**Goal (US5)**: Provide a tenant dashboard card showing admin roles posture at-a-glance with "Scan now" CTA and "View latest report" link.
**Goal (US6)**: Ensure existing stored reports viewer supports filtering by `entra.admin_roles` report type.
Maps to plan Phase F.

**Independent Test (US5)**: Navigate to a tenant dashboard after a scan; confirm the card shows correct timestamp and high-privilege count.
**Independent Test (US6)**: Navigate to stored reports viewer filtered by `entra.admin_roles`; confirm report displays summary and high-privilege assignments table.

### Implementation for User Story 5

- [X] T032 [P] [US5] Create `admin-roles-summary.blade.php` in `resources/views/filament/widgets/tenant/admin-roles-summary.blade.php` — card template with: summary stats (last scan timestamp, high-privilege count), empty state ("No scan performed"), "Scan now" CTA (gated by `ENTRA_ROLES_MANAGE`), "View latest report" link (gated by `ENTRA_ROLES_VIEW`). Follow existing tenant card widget templates.
- [X] T033 [US5] Create `AdminRolesSummaryWidget` in `app/Filament/Widgets/Tenant/AdminRolesSummaryWidget.php` extending `Widget`. Must: (1) resolve tenant via `Filament::getTenant()`, (2) query latest `StoredReport` where `report_type=entra.admin_roles` and `tenant_id` matches, (3) extract last scan timestamp + high-privilege count from payload, (4) implement `canView()` gated by `ENTRA_ROLES_VIEW` capability via `Gate::check()` (server-side enforcement, not just UI hiding), (5) "Scan now" action dispatches `ScanEntraAdminRolesJob` after checking `ENTRA_ROLES_MANAGE` server-side, (6) render empty state when no report exists. Follow existing tenant dashboard widget patterns.

### Tests for User Story 5

- [X] T034 [US5] Write widget tests in `tests/Feature/EntraAdminRoles/AdminRolesSummaryWidgetTest.php` (Livewire component test): (1) widget renders with report data — shows timestamp and high-privilege count, (2) empty state renders "No scan performed" when no report exists, (3) "Scan now" dispatches `ScanEntraAdminRolesJob` for user with `ENTRA_ROLES_MANAGE`, (4) "Scan now" not visible / returns 403 for user with `ENTRA_ROLES_VIEW` but without `ENTRA_ROLES_MANAGE`, (5) widget hidden (canView returns false) for user without `ENTRA_ROLES_VIEW`, (6) non-member → 404.

### Tests for User Story 6

- [X] T035 [US6] Write report viewer integration test in `tests/Feature/EntraAdminRoles/AdminRolesReportViewerTest.php`: first confirm the existing stored reports viewer can render the `entra.admin_roles` payload as a summary + high-privilege assignments table; then assert: (1) stored reports viewer shows reports with `report_type=entra.admin_roles`, (2) report displays summary totals and high-privilege assignments table from payload, (3) multiple reports ordered by creation date descending.

**Checkpoint**: US5 + US6 complete — tenant dashboard shows admin roles posture card, and stored reports viewer supports the new report type.

---

## Phase 8: Polish & Cross-Cutting Concerns

**Purpose**: Code quality, full test pass, and end-to-end validation.

- [X] T036 Run `vendor/bin/sail bin pint --dirty` to fix formatting across all modified/created files.
- [X] T037 Run full Spec 105 test suite: `vendor/bin/sail artisan test --compact --filter=EntraAdminRoles` and verify all tests pass.
- [X] T038 Run quickstart.md validation: verify all files listed in quickstart.md "New Files Created" and "Modified Files Summary" tables exist and match the implementation.

---

## Dependencies & Execution Order

### Phase Dependencies

```
Phase 2 (Foundation) ─────┬──> Phase 3 (US1 - P1) ──> Phase 4 (US2 - P1) 🎯 MVP
                           │                                    │
                           │                                    ├──> Phase 5 (US4 - P2)
                           │                                    ├──> Phase 6 (US3 - P2)
                           │                                    └──> Phase 7 (US5+US6 - P3)
                           │
                           └── BLOCKS all user story work

Phase 5, Phase 6, Phase 7 can proceed in parallel after Phase 4

Phase 8 (Polish) depends on all phases complete
```

- **Foundation (Phase 2)**: No dependencies — start immediately. BLOCKS all user stories.
- **US1 (Phase 3)**: Depends on Phase 2 completion. Report service is prerequisite for finding generator.
- **US2 (Phase 4)**: Depends on Phase 3 (finding generator uses report payload structure; scan job calls report service). **Completing Phase 4 delivers the core MVP.**
- **US4 (Phase 5)**: Depends on Phase 4 (posture score context requires scan to be operational). Can run in parallel with Phase 6 and Phase 7.
- **US3 (Phase 6)**: Depends on Phase 4 (finding generator produces alert events). Can run in parallel with Phase 5 and Phase 7.
- **US5+US6 (Phase 7)**: Depends on Phase 4 (widget displays report data from completed scans). Can run in parallel with Phase 5 and Phase 6.
- **Polish (Phase 8)**: Depends on all user stories being complete.

### User Story Dependencies

- **User Story 1 (P1)**: Can start after Foundation — no dependencies on other stories
- **User Story 2 (P1)**: Depends on US1 (finding generator uses catalog + report payload structure)
- **User Story 4 (P2)**: Independent of other stories after Phase 4 — only requires Foundation config
- **User Story 3 (P2)**: Independent after Phase 4 — only requires finding generator to produce events
- **User Story 5 (P3)**: Independent after Phase 4 — widget queries stored reports
- **User Story 6 (P3)**: Independent after Phase 4 — tests existing viewer with new report type

### Within Each User Story

- Value objects before services
- Services before jobs
- Jobs before schedule registration
- Implementation before tests
- Core logic before integration points

### Parallel Opportunities

**Phase 2 (Foundation)**:
- T002 + T003 (config files, independent)
- T004 + T005 + T006 + T007 (model/enum constants, independent files)
- T008 + T009 (capabilities, can be done together logically but different files)
- T010 + T011 (badge + factory, independent files)
- T013 + T014 + T015 (tests, independent test files)

**Phase 3 (US1)**:
- T016 + T017 (HighPrivilegeRoleCatalog + ReportResult VO, independent files)
- T019 can start after T016 (tests catalog)

**Phase 4 (US2)**:
- T021 can run in parallel with T016/T017 if started early (VO, no dependencies)

**Phases 5 + 6 + 7** (after Phase 4 complete):
- T027 (US4) + T029 (US3) + T032+T033 (US5) — all independent of each other
- T030 (US3 AlertRuleResource change) can run in parallel with T029

---

## Parallel Example: Foundation Phase

```bash
# Batch 1: Migration + Config (parallel)
T001: Create fingerprint migration
T002: Create config/entra_permissions.php
T003: Add Graph contract entries

# Batch 2: Constants + Capabilities + Badge + Factory (parallel, after migration written)
T004: StoredReport constant + fillables
T005: Finding constant
T006: AlertRule constant
T007: OperationRunType enum case
T008: Capabilities constants
T009: RoleCapabilityMap update
T010: FindingTypeBadge mapping
T011: FindingFactory state

# Batch 3: Run migration
T012: vendor/bin/sail artisan migrate

# Batch 4: Foundation tests (parallel)
T013: StoredReport fingerprint test
T014: Badge rendering test
T015: Capabilities registry test
```

---

## Implementation Strategy

### MVP First (User Story 1 + 2)

1. Complete Phase 2: Foundation
2. Complete Phase 3: User Story 1 (catalog + report service)
3. Complete Phase 4: User Story 2 (finding generator + scan job + scheduling)
4. **STOP and VALIDATE**: Run `vendor/bin/sail artisan test --compact --filter=EntraAdminRoles`
5. Full scan pipeline operational: Graph → report → findings → auto-resolve → aggregate

### Incremental Delivery

1. Foundation → ready
2. US1 (P1) → evidence pipeline: reports created from Graph data ✅
3. US2 (P1) → findings + job: full scan pipeline with findings lifecycle ✅ **MVP!**
4. US4 (P2) → posture: Entra permissions in posture scores ✅
5. US3 (P2) → alerts: push notifications for high-privilege events ✅
6. US5+US6 (P3) → UI: dashboard card + report viewer ✅
7. Polish → formatting, full test pass, quickstart validation ✅

Each phase adds value without breaking previous phases.

---

## Notes

- [P] tasks = different files, no dependencies on incomplete tasks
- [Story] label maps task to specific user story for traceability
- Fingerprint for individual findings: `sha256("entra_admin_role:{tenant_id}:{role_template_or_id}:{principal_id}:{scope_id}")`
- Fingerprint for aggregate finding: `sha256("entra_admin_role_ga_count:{tenant_id}")`
- Report fingerprint: SHA-256 of sorted `"{role_template_or_id}:{principal_id}:{scope_id}"` tuples
- Severity: `critical` for Global Administrator, `high` for all other high-privilege roles
- Threshold for "Too many Global Admins": 5 (hardcoded v1, TODO for future settings)
- RBAC boundary: `ENTRA_ROLES_VIEW` gates card + report only; findings use existing `FINDINGS_VIEW`
- All Graph calls go through `GraphClientInterface` via registered contracts
- Scan is non-destructive (read-only) — no `->requiresConfirmation()` needed
- Commit after each task or logical group
- Stop at any checkpoint to validate story independently
