8.5 KiB
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.phpfollowingconfig/intune_permissions.phpschema. - Declare
RoleManagement.Read.Directory(type: application, features:['entra-admin-roles']). - Optionally:
Directory.Read.Allas 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/roleDefinitionsGET /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
- Readonly/Operator →
1.5 Add OperationRunType case
- Add
EntraAdminRolesScan = 'entra.admin_roles.scan'toApp\Support\OperationRunTypeenum.
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): boolseverityFor(string $templateId, ?string $displayName): ?stringallTemplateIds(): arrayglobalAdminTemplateId(): string
- Fallback: if
template_idnot in catalog, checkdisplay_name. - Full unit test coverage.
2.2 Implement EntraAdminRolesReportService
- Create
App\Services\EntraAdminRoles\EntraAdminRolesReportService. - Responsibilities:
- Fetch
roleDefinitionsvia Graph (using registered contract). - Fetch
roleAssignments?$expand=principalvia Graph. - Build payload per spec (role_definitions, role_assignments, totals, high_privilege).
- Compute fingerprint: SHA-256 of sorted
(role_template_or_id, principal_id, scope_id)tuples. - Determine
previous_fingerprintfrom latest existing report for this tenant. - Dedupe: if current fingerprint == latest report's fingerprint, skip creation.
- Create
StoredReportwithreport_type=entra.admin_roles.
- Fetch
- Add
REPORT_TYPE_ENTRA_ADMIN_ROLES = 'entra.admin_roles'constant toStoredReportmodel. - Tests: report creation, dedupe, fingerprint computation, previous_fingerprint chaining.
2.3 Implement EntraAdminRolesFindingGenerator
- Create
App\Services\EntraAdminRoles\EntraAdminRolesFindingGenerator. - Responsibilities:
- Accept report payload (or structured DTO).
- 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_jsonbwith role, principal, scope details - Update
times_seen,last_seen_aton existing findings
- Compute fingerprint:
- Aggregate check: count GA principals > threshold (5) → aggregate finding with distinct fingerprint.
- Auto-resolve: query open findings for this tenant+source not in current scan's fingerprint set → resolve.
- Re-open: if a resolved finding's fingerprint matches current data → set status=new, clear resolved fields.
- Collect alert events for new/re-opened findings.
- Add
FINDING_TYPE_ENTRA_ADMIN_ROLES = 'entra_admin_roles'constant toFindingmodel. - Tests: creation, idempotency, auto-resolve, re-open, aggregate finding, alert events.
Phase 3 — Job & Scheduling
3.1 Implement ScanEntraAdminRolesJob
- Create
App\Jobs\EntraAdminRoles\ScanEntraAdminRolesJob(implementsShouldQueue). - Lifecycle:
- Check tenant has provider connection; skip if not.
- Create OperationRun (
entra.admin_roles.scan, status=running). - Call
EntraAdminRolesReportService::generate(). - Call
EntraAdminRolesFindingGenerator::generate(). - Dispatch alert events via
AlertDispatchService. - 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
ScanEntraAdminRolesJobin 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'toAlertRulemodel. - Add to event type options array (used in alert rule form dropdown).
4.2 Extend EvaluateAlertsJob
- Add a new producer section in
EvaluateAlertsJobthat:- Queries findings where
source=entra.admin_roles,severity>=high,statusin (new). - Produces events with
event_type=entra.admin_roles.high. - Uses finding fingerprint as
fingerprint_keyfor cooldown/dedupe.
- Queries findings where
- Tests: alert rule matching, delivery creation, cooldown.
Phase 5 — Badge Catalog
5.1 Add entra_admin_roles to FindingTypeBadge
- Extend
App\Support\Badges\Domains\FindingTypeBadgemapper 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_typeon 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_VIEWcan view;ENTRA_ROLES_MANAGEcan 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
- Livewire v4.0+: All widget components are Livewire v4 compatible.
- Provider registration: No new panel providers — existing admin panel in
bootstrap/providers.php. - Global search: No new globally searchable resources.
- Destructive actions: "Scan now" is non-destructive (read-only Graph call); no
requiresConfirmation()needed. - Asset strategy: No new heavy assets. Widget uses standard Filament components.
- Testing plan: Widget tested as Livewire component; finding generator + report service unit tested; job feature tested; RBAC positive/negative tests included.