TenantAtlas/tests/Feature/EntraAdminRoles/AdminRolesReportViewerTest.php
ahmido 6a15fe978a feat: Spec 105 — Entra Admin Roles Evidence + Findings (#128)
## 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
2026-02-22 02:37:36 +00:00

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());
});