TenantAtlas/specs/105-entra-admin-roles-evidence-findings/tasks.md
ahmido 6a15fe978a feat: Spec 105 — Entra Admin Roles Evidence + Findings (#128)
## Summary

Automated scanning of Entra ID directory roles to surface high-privilege role assignments as trackable findings with alerting support.

## What's included

### Core Services
- **EntraAdminRolesReportService** — Fetches role definitions + assignments via Graph API, builds payload with fingerprint deduplication
- **EntraAdminRolesFindingGenerator** — Creates/resolves/reopens findings based on high-privilege role catalog
- **HighPrivilegeRoleCatalog** — Curated list of high-privilege Entra roles (Global Admin, Privileged Auth Admin, etc.)
- **ScanEntraAdminRolesJob** — Queued job orchestrating scan → report → findings → alerts pipeline

### UI
- **AdminRolesSummaryWidget** — Tenant dashboard card showing last scan time, high-privilege assignment count, scan trigger button
- RBAC-gated: `ENTRA_ROLES_VIEW` for viewing, `ENTRA_ROLES_MANAGE` for scan trigger

### Infrastructure
- Graph contracts for `entraRoleDefinitions` + `entraRoleAssignments`
- `config/entra_permissions.php` — Entra permission registry
- `StoredReport.fingerprint` migration (deduplication support)
- `OperationCatalog` label + duration for `entra.admin_roles.scan`
- Artisan command `entra:scan-admin-roles` for CLI/scheduled use

### Global UX improvement
- **SummaryCountsNormalizer**: Zero values filtered, snake_case keys humanized (e.g. `report_deduped: 1` → `Report deduped: 1`). Affects all operation notifications.

## Test Coverage
- **12 test files**, **79+ tests**, **307+ assertions**
- Report service, finding generator, job orchestration, widget rendering, alert integration, RBAC enforcement, badge mapping

## Spec artifacts
- `specs/105-entra-admin-roles-evidence-findings/tasks.md` — Full task breakdown (38 tasks, all complete)
- `specs/105-entra-admin-roles-evidence-findings/checklists/requirements.md` — All items checked

## Files changed
46 files changed, 3641 insertions(+), 15 deletions(-)

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #128
2026-02-22 02:37:36 +00:00

31 KiB

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

  • 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

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

  • 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.
  • 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.
  • T006 [P] Extend AlertRule model in app/Models/AlertRule.php: add EVENT_ENTRA_ADMIN_ROLES_HIGH = 'entra.admin_roles.high' constant.
  • T007 [P] Add EntraAdminRolesScan = 'entra.admin_roles.scan' case to app/Support/OperationRunType.php enum.

Capability Registry

  • T008 [P] Add ENTRA_ROLES_VIEW = 'entra_roles.view' and ENTRA_ROLES_MANAGE = 'entra_roles.manage' constants to app/Support/Auth/Capabilities.php.
  • 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

  • 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

  • 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

  • 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].
  • 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.
  • 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.
  • 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

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

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

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

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

  • 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

  • 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

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

  • 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

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

  • 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

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

  • T036 Run vendor/bin/sail bin pint --dirty to fix formatting across all modified/created files.
  • T037 Run full Spec 105 test suite: vendor/bin/sail artisan test --compact --filter=EntraAdminRoles and verify all tests pass.
  • 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

# 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