## 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
220 lines
17 KiB
Markdown
220 lines
17 KiB
Markdown
# Implementation Plan: Entra Admin Roles Evidence + Findings
|
|
|
|
**Branch**: `105-entra-admin-roles-evidence-findings` | **Date**: 2026-02-21 | **Spec**: [spec.md](spec.md)
|
|
**Input**: Feature specification from `/specs/105-entra-admin-roles-evidence-findings/spec.md`
|
|
|
|
## Summary
|
|
|
|
Implement an Entra Admin Roles evidence and findings pipeline that scans a tenant's directory role definitions and active role assignments via Microsoft Graph, persists results as fingerprinted stored reports, generates severity-classified findings for high-privilege assignments, integrates with the existing alerts pipeline, adds a tenant dashboard widget, and extends the permission posture with Entra-specific Graph permissions. The implementation reuses infrastructure from Spec 104 (StoredReports, fingerprint-based dedup, posture pattern) and the existing findings/alerts framework.
|
|
|
|
## Technical Context
|
|
|
|
**Language/Version**: PHP 8.4 (Laravel 12)
|
|
**Primary Dependencies**: Filament v5, Livewire v4, Pest v4
|
|
**Storage**: PostgreSQL (via Sail), JSONB for stored report payloads and finding evidence
|
|
**Testing**: Pest v4 (`vendor/bin/sail artisan test --compact`)
|
|
**Target Platform**: Linux server (Docker/Sail locally, Dokploy for staging/production)
|
|
**Project Type**: Web application (Laravel monolith)
|
|
**Performance Goals**: Single tenant scan completes within 30s for up to 200 role assignments; alert delivery queued within 2 mins of finding creation
|
|
**Constraints**: No external API calls from finding generator (reads report payload); all Graph calls via contract path; all work is queued
|
|
**Scale/Scope**: Up to ~50 tenants per workspace, ~200 role assignments per tenant, ~6 high-privilege role types in v1 catalog
|
|
|
|
## Constitution Check
|
|
|
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
|
|
|
- [x] **Inventory-first**: Admin roles scan captures "last observed" active role assignments from Graph. StoredReports are immutable evidence snapshots, not configuration. Findings represent current posture analysis on observed data.
|
|
- [x] **Read/write separation**: Scan is 100% read-only against Entra (no writes). StoredReports are immutable. Findings are system-generated — no manual create/edit. No preview/dry-run needed (non-destructive).
|
|
- [x] **Graph contract path**: Two new Graph endpoints registered in `config/graph_contracts.php`: `entraRoleDefinitions` and `entraRoleAssignments`. All calls go through `GraphClientInterface`. No hardcoded endpoints.
|
|
- [x] **Deterministic capabilities**: High-privilege classification is deterministic via `HighPrivilegeRoleCatalog` (static `template_id` → severity map). Severity derivation is testable via unit tests. New capabilities `ENTRA_ROLES_VIEW` and `ENTRA_ROLES_MANAGE` added to canonical `Capabilities` registry.
|
|
- [x] **RBAC-UX**: Tenant-context routes remain tenant-scoped. Non-member → 404. Member without `ENTRA_ROLES_VIEW` → 403 for Admin Roles card/report. Member without `ENTRA_ROLES_MANAGE` → 403 for scan trigger. Findings list uses existing `FINDINGS_VIEW` (no dual-gate). Capabilities referenced via constants only (no raw strings).
|
|
- [x] **Workspace isolation**: StoredReports include `workspace_id` (NOT NULL). Findings derive workspace via `DerivesWorkspaceIdFromTenant`. Widget resolves tenant via `Filament::getTenant()`. Non-member workspace access → 404.
|
|
- [x] **RBAC-UX (destructive confirmation)**: No destructive actions. "Scan now" is a non-destructive read-only operation — no confirmation required.
|
|
- [x] **RBAC-UX (global search)**: No new globally searchable resources. Existing search behavior unchanged.
|
|
- [x] **Tenant isolation**: All findings, stored reports, and operation runs are scoped via `tenant_id` (NOT NULL). Cross-tenant access impossible at query level. Widget renders only current tenant data.
|
|
- [x] **Run observability**: Each scan execution tracked as `OperationRun` with `type=entra.admin_roles.scan`. Start surfaces enqueue only (`ScanEntraAdminRolesJob` dispatched). OperationRun records status, timestamps, counts, and failures. Active-run uniqueness enforced per `(workspace_id, tenant_id, run_type)`.
|
|
- [x] **Automation**: `ScanEntraAdminRolesJob` uses `OperationRunService::ensureRunWithIdentity()` for dedup. Fingerprint-based upsert handles concurrency for findings. Graph throttling handled by existing `GraphClientInterface` retry logic (429/503 backoff+jitter).
|
|
- [x] **Data minimization**: Report payload contains only governance-relevant data (IDs, display names, principal types, scopes). No tokens, secrets, or excessive PII. Logs use stable error codes — no secrets/tokens in run failures.
|
|
- [x] **Badge semantics (BADGE-001)**: `finding_type=entra_admin_roles` added to centralized `FindingTypeBadge` mapper with `BadgeSpec('Entra admin roles', 'danger', 'heroicon-m-identification')`. Tests included.
|
|
- [x] **Filament UI Action Surface Contract**: No new Resources/RelationManagers/Pages. Widget is a simple card with "Scan now" header action and "View latest report" link. Exemption: Widget is not a full Resource — Action Surface Contract does not apply to simple stat/card widgets. Documented in spec UI Action Matrix.
|
|
- [x] **UX-001 (Layout & IA)**: No new Create/Edit/View pages. Widget follows existing card widget conventions. Report viewer reuses existing stored reports page. Exemption documented in spec.
|
|
- [x] **SCOPE-001 ownership**: StoredReports are tenant-owned (`workspace_id` + `tenant_id` NOT NULL). Findings are tenant-owned (existing). AlertRule is workspace-owned (existing, no change).
|
|
|
|
**Post-Phase-1 re-check**: All items pass. No violations found.
|
|
|
|
## Project Structure
|
|
|
|
### Documentation (this feature)
|
|
|
|
```text
|
|
specs/105-entra-admin-roles-evidence-findings/
|
|
├── plan.md # This file
|
|
├── spec.md # Feature specification
|
|
├── research.md # Phase 0: research decisions (R1-R10)
|
|
├── data-model.md # Phase 1: entity/table design
|
|
├── quickstart.md # Phase 1: implementation guide
|
|
├── contracts/
|
|
│ └── internal-services.md # Phase 1: service contracts
|
|
├── checklists/
|
|
│ └── requirements.md # Quality checklist (created by /speckit.tasks)
|
|
└── tasks.md # Phase 2 output (created by /speckit.tasks)
|
|
```
|
|
|
|
### Source Code (repository root)
|
|
|
|
```text
|
|
app/
|
|
├── Models/
|
|
│ ├── StoredReport.php # MODIFIED: +REPORT_TYPE_ENTRA_ADMIN_ROLES, +fingerprint fillables
|
|
│ ├── Finding.php # MODIFIED: +FINDING_TYPE_ENTRA_ADMIN_ROLES constant
|
|
│ └── AlertRule.php # MODIFIED: +EVENT_ENTRA_ADMIN_ROLES_HIGH constant
|
|
├── Services/
|
|
│ ├── EntraAdminRoles/
|
|
│ │ ├── HighPrivilegeRoleCatalog.php # NEW: role classification (template_id → severity)
|
|
│ │ ├── EntraAdminRolesReportService.php # NEW: Graph fetch + report creation + dedup
|
|
│ │ ├── EntraAdminRolesReportResult.php # NEW: value object
|
|
│ │ ├── EntraAdminRolesFindingGenerator.php # NEW: findings lifecycle (create/resolve/reopen)
|
|
│ │ └── EntraAdminRolesFindingResult.php # NEW: value object
|
|
│ └── Intune/
|
|
│ └── TenantPermissionService.php # MODIFIED: merge entra_permissions into getRequiredPermissions()
|
|
├── Jobs/
|
|
│ ├── ScanEntraAdminRolesJob.php # NEW: queued orchestrator job
|
|
│ └── Alerts/
|
|
│ └── EvaluateAlertsJob.php # MODIFIED: +entraAdminRolesHighEvents() method
|
|
├── Support/
|
|
│ ├── OperationRunType.php # MODIFIED: +EntraAdminRolesScan case
|
|
│ ├── Auth/
|
|
│ │ ├── Capabilities.php # MODIFIED: +ENTRA_ROLES_VIEW, +ENTRA_ROLES_MANAGE
|
|
│ │ └── RoleCapabilityMap.php # MODIFIED: map new capabilities to roles
|
|
│ └── Badges/Domains/
|
|
│ └── FindingTypeBadge.php # MODIFIED: +entra_admin_roles badge mapping
|
|
├── Filament/
|
|
│ ├── Resources/
|
|
│ │ └── AlertRuleResource.php # MODIFIED: +EVENT_ENTRA_ADMIN_ROLES_HIGH in dropdown
|
|
│ └── Widgets/Tenant/
|
|
│ └── AdminRolesSummaryWidget.php # NEW: tenant dashboard card widget
|
|
└── Providers/
|
|
└── (no changes)
|
|
|
|
config/
|
|
├── entra_permissions.php # NEW: Entra permission registry
|
|
└── graph_contracts.php # MODIFIED: +entraRoleDefinitions, +entraRoleAssignments
|
|
|
|
database/
|
|
├── migrations/
|
|
│ └── XXXX_add_fingerprint_to_stored_reports.php # NEW: fingerprint + previous_fingerprint columns
|
|
└── factories/
|
|
└── FindingFactory.php # MODIFIED: +entraAdminRoles() state
|
|
|
|
resources/views/filament/widgets/tenant/
|
|
└── admin-roles-summary.blade.php # NEW: card template
|
|
|
|
routes/
|
|
└── console.php # MODIFIED: schedule daily admin roles scan
|
|
|
|
tests/Feature/EntraAdminRoles/
|
|
├── HighPrivilegeRoleCatalogTest.php # NEW
|
|
├── EntraAdminRolesReportServiceTest.php # NEW
|
|
├── EntraAdminRolesFindingGeneratorTest.php # NEW
|
|
├── ScanEntraAdminRolesJobTest.php # NEW
|
|
├── AdminRolesAlertIntegrationTest.php # NEW
|
|
├── AdminRolesSummaryWidgetTest.php # NEW
|
|
├── EntraPermissionsRegistryTest.php # NEW
|
|
└── StoredReportFingerprintTest.php # NEW
|
|
```
|
|
|
|
**Structure Decision**: Standard Laravel monolith structure. New services go under `app/Services/EntraAdminRoles/`. Tests mirror the service structure under `tests/Feature/EntraAdminRoles/`. Widget follows existing tenant dashboard card pattern.
|
|
|
|
## Complexity Tracking
|
|
|
|
> No constitution violations. No complexity justifications needed.
|
|
|
|
## Implementation Phases
|
|
|
|
### Phase A — Foundation (Migration + Constants + Config)
|
|
|
|
**Goal**: Establish the data layer, config, and constants that all other phases depend on.
|
|
|
|
**Deliverables**:
|
|
1. Migration: `add_fingerprint_to_stored_reports` — add `fingerprint` (string(64), nullable), `previous_fingerprint` (string(64), nullable), unique index on `[tenant_id, report_type, fingerprint]`, index on `[tenant_id, report_type, created_at DESC]`
|
|
2. Config: `config/entra_permissions.php` — registry with `RoleManagement.Read.Directory` (type: application, features: ['entra-admin-roles'])
|
|
3. Config: Add `entraRoleDefinitions` and `entraRoleAssignments` entries to `config/graph_contracts.php`
|
|
4. Model: `StoredReport` — add `REPORT_TYPE_ENTRA_ADMIN_ROLES` constant, add `fingerprint` + `previous_fingerprint` to fillable
|
|
5. Model: `Finding` — add `FINDING_TYPE_ENTRA_ADMIN_ROLES` constant
|
|
6. Model: `AlertRule` — add `EVENT_ENTRA_ADMIN_ROLES_HIGH` constant
|
|
7. Enum: `OperationRunType` — add `EntraAdminRolesScan` case
|
|
8. Constants: `Capabilities` — add `ENTRA_ROLES_VIEW`, `ENTRA_ROLES_MANAGE`
|
|
9. Constants: `RoleCapabilityMap` — map new capabilities (Readonly/Operator → VIEW; Manager/Owner → MANAGE)
|
|
10. Badge: Add `entra_admin_roles` mapping to `FindingTypeBadge`
|
|
11. Factory: Add `entraAdminRoles()` state to `FindingFactory`
|
|
12. Tests: StoredReport fingerprint migration (column exists), badge rendering, capabilities registry
|
|
|
|
**Dependencies**: None (foundation layer).
|
|
|
|
### Phase B — High-Privilege Catalog + Report Service
|
|
|
|
**Goal**: Implement role classification and the Graph-backed report generation service.
|
|
|
|
**Deliverables**:
|
|
1. Service: `HighPrivilegeRoleCatalog` — static catalog with `classify()`, `isHighPrivilege()`, `isGlobalAdministrator()`, `allTemplateIds()`
|
|
2. Value object: `EntraAdminRolesReportResult` — `(created, storedReportId, fingerprint, payload)`
|
|
3. Service: `EntraAdminRolesReportService` — fetches Graph data, builds payload (FR-005), computes fingerprint, creates/deduplicates StoredReport, sets `previous_fingerprint`
|
|
4. Tests: Catalog classification (all 6 roles, display name fallback, unknown roles, null display name), report service (new report creation, dedup on identical fingerprint, previous_fingerprint chain, all-or-nothing on partial Graph failure, payload schema validation)
|
|
|
|
**Dependencies**: Phase A (constants, config, migration).
|
|
|
|
### Phase C — Finding Generator
|
|
|
|
**Goal**: Implement the finding lifecycle — create, upsert, auto-resolve, re-open, aggregate threshold.
|
|
|
|
**Deliverables**:
|
|
1. Value object: `EntraAdminRolesFindingResult` — `(created, resolved, reopened, unchanged, alertEventsProduced)`
|
|
2. Service: `EntraAdminRolesFindingGenerator` — per (principal, role) findings with fingerprint-based idempotency, auto-resolve stale findings, re-open resolved findings, aggregate "Too many Global Admins" finding, alert event production
|
|
3. Tests: Finding creation (severity mapping), idempotent upsert (`times_seen` / `last_seen_at` update), auto-resolve on removed assignment, re-open on re-assigned role, aggregate finding (threshold exceeded/within), evidence schema, alert event production for new/re-opened findings, no events for unchanged/resolved
|
|
|
|
**Dependencies**: Phase B (catalog, report service provides payload structure).
|
|
|
|
### Phase D — Scan Job + Scheduling + Permission Posture Integration
|
|
|
|
**Goal**: Wire everything together as a queued job with scheduling and integrate Entra permissions into the posture pipeline.
|
|
|
|
**Deliverables**:
|
|
1. Job: `ScanEntraAdminRolesJob` — creates OperationRun via `ensureRunWithIdentity()`, calls report service, calls finding generator, records outcome. Skips gracefully if no active provider connection.
|
|
2. Schedule: Register daily scan command in `routes/console.php` (iterate workspaces → tenants with active connections → dispatch per-tenant)
|
|
3. Modify: `TenantPermissionService::getRequiredPermissions()` — merge `config('entra_permissions.permissions', [])` alongside existing Intune permissions
|
|
4. Tests: Job dispatch + OperationRun lifecycle, skip-if-no-connection, error handling (Graph failure → run failure), permissions registry merge (Intune + Entra), posture score reflects Entra permission gaps
|
|
|
|
**Dependencies**: Phase C (finding generator).
|
|
|
|
### Phase E — Alerts Integration
|
|
|
|
**Goal**: Connect admin roles findings to the existing alert pipeline.
|
|
|
|
**Deliverables**:
|
|
1. Modify: `EvaluateAlertsJob` — add `entraAdminRolesHighEvents()` method (query `finding_type=entra_admin_roles`, `status IN (new)`, `severity IN (high, critical)`, `updated_at > $windowStart`). Call in `handle()` alongside existing event methods.
|
|
2. UI: Add `EVENT_ENTRA_ADMIN_ROLES_HIGH` to event type dropdown in `AlertRuleResource`
|
|
3. Tests: Alert event production, severity filtering, cooldown/dedupe, alert rule matching for new event type
|
|
|
|
**Dependencies**: Phase D (job produces findings that generate alert events).
|
|
|
|
### Phase F — Tenant Dashboard Widget
|
|
|
|
**Goal**: Provide a tenant dashboard card for admin roles posture at-a-glance.
|
|
|
|
**Deliverables**:
|
|
1. Widget: `AdminRolesSummaryWidget` — extends `Widget`, resolves tenant via `Filament::getTenant()`, queries latest `stored_report` (type=entra.admin_roles), displays last scan timestamp + high-privilege count
|
|
2. View: `admin-roles-summary.blade.php` — card template with summary stats, empty state ("No scan performed"), "Scan now" CTA (gated by `ENTRA_ROLES_MANAGE`), "View latest report" link (gated by `ENTRA_ROLES_VIEW`)
|
|
3. RBAC: `canView()` gated by `ENTRA_ROLES_VIEW`. "Scan now" action checks `ENTRA_ROLES_MANAGE` server-side.
|
|
4. Tests: Widget renders with report data, empty state rendering, "Scan now" dispatches job (with RBAC), widget hidden without `ENTRA_ROLES_VIEW`
|
|
|
|
**Dependencies**: Phases A-D complete (job, reports, constants).
|
|
|
|
## Filament v5 Agent Output Contract
|
|
|
|
1. **Livewire v4.0+ compliance**: Yes — `AdminRolesSummaryWidget` extends Filament's `Widget` (Livewire v4 component). No Livewire v3 references.
|
|
2. **Provider registration**: No new providers. Existing `AdminPanelProvider` in `bootstrap/providers.php` unchanged.
|
|
3. **Global search**: No new globally searchable resources. Existing Finding resource global search behavior unchanged.
|
|
4. **Destructive actions**: None introduced. "Scan now" is non-destructive (read-only Graph operation). No confirmation required.
|
|
5. **Asset strategy**: No new frontend assets. Badge mapping is PHP-only. Widget uses a simple Blade template. No `filament:assets` changes needed.
|
|
6. **Testing plan**: Pest feature tests for: HighPrivilegeRoleCatalog (classification), EntraAdminRolesReportService (Graph fetch + dedup), EntraAdminRolesFindingGenerator (create/resolve/reopen/aggregate/idempotency), ScanEntraAdminRolesJob (orchestration + OperationRun), AdminRolesAlertIntegration (event production + matching), AdminRolesSummaryWidget (Livewire component mount + rendering + RBAC), EntraPermissionsRegistry (merge correctness), StoredReportFingerprint (migration + dedup).
|