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

8.5 KiB

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.