## 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
141 lines
5.1 KiB
PHP
141 lines
5.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\StoredReport;
|
|
use App\Models\Tenant;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function createEntraReport(Tenant $tenant, array $summaryOverrides = [], ?string $fingerprint = null): StoredReport
|
|
{
|
|
$totals = array_merge([
|
|
'roles_total' => 8,
|
|
'assignments_total' => 12,
|
|
'high_privilege_assignments' => 5,
|
|
], $summaryOverrides);
|
|
|
|
return StoredReport::query()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'report_type' => StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES,
|
|
'fingerprint' => $fingerprint ?? hash('sha256', (string) now()->timestamp.random_int(0, 99999)),
|
|
'payload' => [
|
|
'provider_key' => 'microsoft',
|
|
'domain' => 'entra.admin_roles',
|
|
'measured_at' => now()->toIso8601String(),
|
|
'role_definitions' => [],
|
|
'role_assignments' => [],
|
|
'totals' => $totals,
|
|
'high_privilege' => [
|
|
[
|
|
'role_display_name' => 'Global Administrator',
|
|
'role_template_id' => '62e90394-69f5-4237-9190-012177145e10',
|
|
'principal_id' => 'user-ga-1',
|
|
'principal_display_name' => 'Admin User',
|
|
'assignment_scope' => '/',
|
|
'severity' => 'critical',
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// StoredReport query by report type
|
|
// ---------------------------------------------------------------------------
|
|
|
|
it('returns stored reports with report_type entra.admin_roles', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$report = createEntraReport($tenant);
|
|
|
|
// Also create a report of a different type to ensure filtering works
|
|
StoredReport::query()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'report_type' => 'permission_posture',
|
|
'fingerprint' => hash('sha256', 'other-report'),
|
|
'payload' => ['summary' => ['score' => 85]],
|
|
]);
|
|
|
|
$results = StoredReport::query()
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
|
|
->get();
|
|
|
|
expect($results)
|
|
->toHaveCount(1)
|
|
->and((int) $results->first()->getKey())->toBe((int) $report->getKey());
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Report payload contains summary and high-privilege assignments table
|
|
// ---------------------------------------------------------------------------
|
|
|
|
it('report payload contains summary totals and high-privilege assignments', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$report = createEntraReport($tenant, [
|
|
'roles_total' => 10,
|
|
'assignments_total' => 20,
|
|
'high_privilege_assignments' => 8,
|
|
]);
|
|
|
|
$report->refresh();
|
|
$payload = $report->payload;
|
|
|
|
expect($payload)
|
|
->toBeArray()
|
|
->toHaveKey('totals')
|
|
->toHaveKey('high_privilege');
|
|
|
|
$totals = $payload['totals'];
|
|
expect($totals)
|
|
->toHaveKey('roles_total', 10)
|
|
->toHaveKey('assignments_total', 20)
|
|
->toHaveKey('high_privilege_assignments', 8);
|
|
|
|
$assignments = $payload['high_privilege'];
|
|
expect($assignments)
|
|
->toBeArray()
|
|
->toHaveCount(1);
|
|
|
|
expect($assignments[0])
|
|
->toHaveKey('role_display_name', 'Global Administrator')
|
|
->toHaveKey('severity', 'critical')
|
|
->toHaveKey('principal_display_name', 'Admin User');
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Multiple reports ordered by creation date descending
|
|
// ---------------------------------------------------------------------------
|
|
|
|
it('orders multiple reports by creation date descending', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$older = createEntraReport($tenant, ['high_privilege_assignments' => 3], 'fp-older');
|
|
$older->forceFill(['created_at' => now()->subHours(2)])->save();
|
|
|
|
$newer = createEntraReport($tenant, ['high_privilege_assignments' => 7], 'fp-newer');
|
|
$newer->forceFill(['created_at' => now()->subHour()])->save();
|
|
|
|
$latest = createEntraReport($tenant, ['high_privilege_assignments' => 1], 'fp-latest');
|
|
|
|
$results = StoredReport::query()
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
|
|
->orderByDesc('created_at')
|
|
->get();
|
|
|
|
expect($results)->toHaveCount(3);
|
|
expect((int) $results[0]->getKey())->toBe((int) $latest->getKey());
|
|
expect((int) $results[1]->getKey())->toBe((int) $newer->getKey());
|
|
expect((int) $results[2]->getKey())->toBe((int) $older->getKey());
|
|
});
|