## 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
117 lines
8.3 KiB
Markdown
117 lines
8.3 KiB
Markdown
# Research: Entra Admin Roles Evidence + Findings (Spec 105)
|
|
|
|
**Date**: 2026-02-21 | **Branch**: `105-entra-admin-roles-evidence-findings`
|
|
|
|
## R1 — StoredReport Fingerprint + Deduplication
|
|
|
|
**Decision**: Add `fingerprint` and `previous_fingerprint` columns to `stored_reports` via a new migration.
|
|
|
|
**Rationale**: The spec requires content-based deduplication — if the role assignment data is identical between scans, no new report should be created. The existing `stored_reports` table (Spec 104) has no fingerprint columns. Spec 104's permission posture reports don't use fingerprinting (they always create a new report per compare run). Spec 105 introduces a different pattern: only store a new report when data has changed.
|
|
|
|
**Migration**:
|
|
- Add `fingerprint` (string(64), nullable) — SHA-256 of sorted role assignment tuples
|
|
- Add `previous_fingerprint` (string(64), nullable) — references the prior report's fingerprint for drift chain
|
|
- Add unique index on `[tenant_id, report_type, fingerprint]` to enforce deduplication at DB level
|
|
- Nullable because existing permission_posture reports have no fingerprint
|
|
|
|
**Alternatives considered**:
|
|
- Fingerprint as payload field (not a column): Rejected because DB-level uniqueness constraint prevents race conditions
|
|
- Separate table for Entra reports: Rejected because it fragments the report model unnecessarily
|
|
|
|
## R2 — Graph API Pattern: roleManagement/directory
|
|
|
|
**Decision**: Register two new endpoints in `config/graph_contracts.php` under a new `entraRoleDefinitions` and `entraRoleAssignments` type section. Graph calls go through `GraphClientInterface`.
|
|
|
|
**Rationale**: The constitution requires all Graph calls to go through the contract registry. The unified RBAC API uses `GET /roleManagement/directory/roleDefinitions` and `GET /roleManagement/directory/roleAssignments?$expand=principal`. These are read-only, paginated endpoints returning standard Graph collections.
|
|
|
|
**Implementation**:
|
|
- New graph_contracts entry: `entraRoleDefinitions` → `roleManagement/directory/roleDefinitions`
|
|
- New graph_contracts entry: `entraRoleAssignments` → `roleManagement/directory/roleAssignments`, with `allowed_expand: ['principal']`
|
|
- The service uses `GraphClientInterface::getCollection()` (or equivalent paginated reader) to fetch all results
|
|
|
|
**Pagination**: Both endpoints return paginated results (standard `@odata.nextLink`). The existing paginated Graph reader pattern handles this.
|
|
|
|
## R3 — HighPrivilegeRoleCatalog Classification Strategy
|
|
|
|
**Decision**: Pure PHP class with a static catalog mapping `template_id` → severity. Fallback to `display_name` matching when `template_id` is null (custom roles).
|
|
|
|
**Rationale**: Microsoft documents stable `template_id` values for built-in roles. These are GUIDs that don't change across tenants. Using `template_id` as primary key ensures cross-tenant consistency. Display name fallback handles edge cases where `template_id` is null.
|
|
|
|
**v1 Catalog** (from spec):
|
|
|
|
| Role | Template ID | Severity |
|
|
|------|------------|----------|
|
|
| Global Administrator | `62e90394-69f5-4237-9190-012177145e10` | critical |
|
|
| Privileged Role Administrator | `e8611ab8-c189-46e8-94e1-60213ab1f814` | high |
|
|
| Security Administrator | `194ae4cb-b126-40b2-bd5b-6091b380977d` | high |
|
|
| Conditional Access Administrator | `b1be1c3e-b65d-4f19-8427-f6fa0d97feb9` | high |
|
|
| Exchange Administrator | `29232cdf-9323-42fd-ade2-1d097af3e4de` | high |
|
|
| Authentication Administrator | `c4e39bd9-1100-46d3-8c65-fb160da0071f` | high |
|
|
|
|
**Extensibility**: The catalog class is a standalone value object — future specs can make this configurable per workspace via settings. v1 is hardcoded.
|
|
|
|
## R4 — Finding Fingerprint Formula
|
|
|
|
**Decision**: `entra_admin_role:{tenant_id}:{role_template_or_id}:{principal_id}:{scope_id}` hashed to SHA-256 (64 chars).
|
|
|
|
**Rationale**: This matches the existing pattern from `PermissionPostureFindingGenerator` which uses `permission_posture:{tenant_id}:{permission_key}`. Including all four dimensions (tenant, role, principal, scope) ensures uniqueness even if the same principal holds the same role at different scopes.
|
|
|
|
**Aggregate finding** (Too many GAs): `entra_admin_role_ga_count:{tenant_id}` — single fingerprint per tenant for the threshold finding.
|
|
|
|
**Alternatives considered**:
|
|
- Including `assignment_id` from Graph: Rejected because assignment IDs can change when role assignments are recreated, causing false re-opens
|
|
|
|
## R5 — Entra Permissions Registry Merge Strategy
|
|
|
|
**Decision**: Modify `TenantPermissionService::getRequiredPermissions()` to merge `config/intune_permissions.php` and `config/entra_permissions.php`.
|
|
|
|
**Rationale**: The current implementation at L23 hardcodes `config('intune_permissions.permissions', [])`. The merge must:
|
|
1. Load both registries
|
|
2. Concatenate permission arrays
|
|
3. Return the merged set
|
|
|
|
This is a minimal change (2-3 lines). The posture generator and compare logic are generic — they iterate the returned permissions array regardless of source.
|
|
|
|
**Non-breaking guarantee**: Existing Intune permissions appear unchanged in the merged array. Posture scores recompute to include Entra permissions proportionally.
|
|
|
|
## R6 — Alert Event Integration
|
|
|
|
**Decision**: Add `entraAdminRolesHighEvents()` method to `EvaluateAlertsJob`, following the exact same pattern as `highDriftEvents()` and `permissionMissingEvents()`.
|
|
|
|
**Rationale**: The existing alert evaluation job collects events from dedicated methods and dispatches them. Adding one more method that queries `finding_type=entra_admin_roles` with `status=new` and severity `>= high` within the time window follows the established pattern exactly.
|
|
|
|
**Event structure**: Same shape as existing events (`event_type`, `tenant_id`, `severity`, `fingerprint_key`, `title`, `body`, `metadata`). The `fingerprint_key` uses `finding:{finding_id}` for cooldown dedup.
|
|
|
|
## R7 — OperationRunType for Scan
|
|
|
|
**Decision**: Add `EntraAdminRolesScan = 'entra.admin_roles.scan'` to the `OperationRunType` enum.
|
|
|
|
**Rationale**: The existing enum has cases like `InventorySync`, `PolicySync`, etc. The dot-notation value (`entra.admin_roles.scan`) follows the pattern of `policy.sync`, `policy.sync_one`, etc. Active-run uniqueness is enforced per `(workspace_id, tenant_id, run_type)` via `OperationRunService::ensureRunWithIdentity()`.
|
|
|
|
## R8 — Tenant Dashboard Widget Pattern
|
|
|
|
**Decision**: New `AdminRolesSummaryWidget` extending `Filament\Widgets\Widget` with a custom Blade view, matching the existing `RecentOperationsSummary` widget pattern.
|
|
|
|
**Rationale**: Existing tenant dashboard widgets (`RecentOperationsSummary`, `TenantVerificationReport`) use the same pattern: extend `Widget`, resolve the tenant from Filament context, query data, return view data. The Admin Roles card follows this exact pattern:
|
|
1. Resolve tenant via `Filament::getTenant()`
|
|
2. Query latest `stored_report` where `report_type=entra.admin_roles`
|
|
3. Extract summary stats (timestamp, high-privilege count)
|
|
4. Return view data to Blade template
|
|
5. "Scan now" action dispatches job (gated by `ENTRA_ROLES_MANAGE`)
|
|
|
|
**RBAC**: The widget is gated by `ENTRA_ROLES_VIEW` via `canView()` method or `static::canView()`. The "Scan now" button checks `ENTRA_ROLES_MANAGE` separately.
|
|
|
|
## R9 — No New Migration for Findings Table
|
|
|
|
**Decision**: No migration needed for the `findings` table.
|
|
|
|
**Rationale**: The existing `findings` table already has all required columns: `finding_type`, `source`, `scope_key`, `fingerprint` (unique per tenant), `subject_type`, `subject_external_id`, `severity`, `status`, `evidence_jsonb`, `resolved_at`, `resolved_reason`. Adding `finding_type=entra_admin_roles` is a new value in an existing string column — no schema change.
|
|
|
|
## R10 — Scan Scheduling Pattern
|
|
|
|
**Decision**: Daily scheduled dispatch following existing workspace dispatcher pattern.
|
|
|
|
**Rationale**: The spec mentions "daily via workspace dispatcher alongside existing scheduled scans." The existing pattern in `routes/console.php` schedules a command that iterates workspaces and dispatches per-tenant jobs. The admin roles scan follows this same dispatching loop.
|
|
|
|
**Implementation**: Either extend the existing dispatcher or add a `ScheduleEntraAdminRolesScanCommand` that iterates workspaces → tenants with active connections → dispatches `ScanEntraAdminRolesJob` per tenant. On-demand scan uses the same job, dispatched from the widget action.
|