TenantAtlas/tests/Feature/Filament/FindingViewRbacEvidenceTest.php
2026-03-14 21:08:32 +01:00

311 lines
11 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\FindingResource;
use App\Filament\Widgets\Dashboard\RecentDriftFindings;
use App\Models\Finding;
use App\Models\InventoryItem;
use App\Support\Baselines\BaselineSubjectKey;
use Filament\Facades\Filament;
use Livewire\Livewire;
it('renders shared summary badges and RBAC row states on the finding detail page', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$finding = findingViewRbacFinding($tenant);
$this->actingAs($user)
->get(FindingResource::getUrl('view', ['record' => $finding], tenant: $tenant))
->assertOk()
->assertSee('Intune RBAC Role Definition drift')
->assertSee('Metadata-only change')
->assertSee('2 changed')
->assertSee('1 added')
->assertSee('1 removed')
->assertSee('4 unchanged')
->assertSee('Changed')
->assertSee('Added')
->assertSee('Removed')
->assertSee('Unchanged')
->assertSee('Changed value')
->assertSee('No material change')
->assertSee('Role definition > Description')
->assertSee('Permission block 1 > Denied actions')
->assertSee('Permission block 1 > Conditions')
->assertSee('Baseline description')
->assertSee('Updated description')
->assertSee('border-warning-200')
->assertSee('text-gray-500')
->assertSee('Role Assignments are not included')
->assertSee('RBAC restore is not supported');
});
it('renders Allowed Actions as inline added removed and unchanged list chips', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$finding = findingViewRbacFinding($tenant);
$this->actingAs($user)
->get(FindingResource::getUrl('view', ['record' => $finding], tenant: $tenant))
->assertOk()
->assertSee('Permission block 1 > Allowed actions')
->assertSee('Added items')
->assertSee('Removed items')
->assertSee('Unchanged items')
->assertSee('Microsoft.Intune/deviceConfigurations/create')
->assertSee('Microsoft.Intune/deviceConfigurations/delete')
->assertSee('Microsoft.Intune/deviceConfigurations/read')
->assertSee('@Resource[Microsoft.Intune/deviceConfigurations] Exists')
->assertSee('Microsoft.Intune/deviceConfigurations/wipe');
});
it('renders a no-change RBAC summary when the evidence only contains unchanged rows', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$finding = findingViewRbacFinding($tenant, [
'changed_keys' => [],
'metadata_keys' => [],
'permission_keys' => [],
'baseline' => [
'normalized' => [
'Role definition > Display name' => 'Security Reader',
'Role definition > Description' => 'Baseline description',
],
'is_built_in' => false,
'role_permission_count' => 1,
],
'current' => [
'normalized' => [
'Role definition > Display name' => 'Security Reader',
'Role definition > Description' => 'Baseline description',
],
'is_built_in' => false,
'role_permission_count' => 1,
],
]);
$this->actingAs($user)
->get(FindingResource::getUrl('view', ['record' => $finding], tenant: $tenant))
->assertOk()
->assertSee('0 changed')
->assertSee('0 added')
->assertSee('0 removed')
->assertSee('4 unchanged')
->assertSee('No changes detected.');
});
it('renders a stable sparse fallback when the RBAC evidence payload is empty', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$finding = findingViewRbacFinding($tenant, []);
$evidence = is_array($finding->evidence_jsonb) ? $finding->evidence_jsonb : [];
$evidence['rbac_role_definition'] = [];
$finding->forceFill([
'evidence_jsonb' => $evidence,
])->save();
$this->actingAs($user)
->get(FindingResource::getUrl('view', ['record' => $finding], tenant: $tenant))
->assertOk()
->assertSee('0 changed')
->assertSee('0 added')
->assertSee('0 removed')
->assertSee('0 unchanged')
->assertSee('No diff data available.')
->assertSee('RBAC restore is not supported');
});
it('shows RBAC labels and display-name fallback in the recent drift findings widget', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user);
$tenant->makeCurrent();
Filament::setCurrentPanel(Filament::getPanel('tenant'));
Filament::setTenant($tenant, true);
$subjectExternalId = 'rbac-role-1';
InventoryItem::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'external_id' => $subjectExternalId,
'policy_type' => 'intuneRoleDefinition',
'display_name' => 'Security Reader',
'meta_jsonb' => ['etag' => 'E1'],
'last_seen_at' => now(),
]);
$finding = Finding::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'finding_type' => Finding::FINDING_TYPE_DRIFT,
'source' => 'baseline.compare',
'subject_type' => 'policy',
'subject_external_id' => (string) $subjectExternalId,
'severity' => Finding::SEVERITY_HIGH,
'status' => Finding::STATUS_NEW,
'evidence_jsonb' => [
'change_type' => 'missing_policy',
'policy_type' => 'intuneRoleDefinition',
'subject_key' => hash('sha256', 'intuneRoleDefinition|rbac-role-1'),
'display_name' => 'Security Reader',
'summary' => [
'kind' => 'rbac_role_definition',
],
'baseline' => ['policy_version_id' => 10],
'current' => ['policy_version_id' => null],
'rbac_role_definition' => [
'diff_kind' => 'missing',
],
'fidelity' => 'mixed',
'provenance' => [
'baseline_profile_id' => 1,
'baseline_snapshot_id' => 1,
'compare_operation_run_id' => 1,
'inventory_sync_run_id' => null,
],
],
]);
Livewire::actingAs($user)->test(RecentDriftFindings::class)
->assertCanSeeTableRecords([$finding])
->assertSee('Security Reader')
->assertSee('Intune RBAC Role Definition drift');
});
/**
* @param array<string, mixed> $rbacOverrides
*/
function findingViewRbacFinding(\App\Models\Tenant $tenant, array $rbacOverrides = []): Finding
{
$subjectExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalIdForPolicy(
'intuneRoleDefinition',
'Security Reader',
'rbac-role-1',
);
return Finding::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'finding_type' => Finding::FINDING_TYPE_DRIFT,
'source' => 'baseline.compare',
'subject_type' => 'policy',
'subject_external_id' => (string) $subjectExternalId,
'evidence_fidelity' => 'content',
'severity' => Finding::SEVERITY_LOW,
'evidence_jsonb' => [
'change_type' => 'different_version',
'policy_type' => 'intuneRoleDefinition',
'subject_key' => hash('sha256', 'intuneRoleDefinition|rbac-role-1'),
'display_name' => 'Security Reader',
'summary' => [
'kind' => 'rbac_role_definition',
],
'baseline' => [
'policy_version_id' => 10,
'hash' => 'baseline',
],
'current' => [
'policy_version_id' => 11,
'hash' => 'current',
],
'rbac_role_definition' => findingViewRbacEvidenceFixture($rbacOverrides),
'fidelity' => 'content',
'provenance' => [
'baseline_profile_id' => 1,
'baseline_snapshot_id' => 1,
'compare_operation_run_id' => 1,
'inventory_sync_run_id' => 1,
],
],
]);
}
/**
* @param array<string, mixed> $overrides
* @return array<string, mixed>
*/
function findingViewRbacEvidenceFixture(array $overrides = []): array
{
return findingViewRbacFixtureMerge([
'diff_kind' => 'metadata_only',
'diff_fingerprint' => 'rbac-diff-1',
'changed_keys' => [
'Role definition > Description',
'Permission block 1 > Allowed actions',
],
'metadata_keys' => [
'Role definition > Description',
],
'permission_keys' => [
'Permission block 1 > Allowed actions',
],
'baseline' => [
'normalized' => [
'Role definition > Display name' => 'Security Reader',
'Role definition > Description' => 'Baseline description',
'Role definition > Scope tag IDs' => ['0', 'scope-1'],
'Permission block 1 > Allowed actions' => [
'Microsoft.Intune/deviceConfigurations/delete',
'Microsoft.Intune/deviceConfigurations/read',
],
'Permission block 1 > Denied actions' => [
'Microsoft.Intune/deviceConfigurations/wipe',
],
],
'is_built_in' => false,
'role_permission_count' => 1,
],
'current' => [
'normalized' => [
'Role definition > Display name' => 'Security Reader',
'Role definition > Description' => 'Updated description',
'Role definition > Scope tag IDs' => ['0', 'scope-1'],
'Permission block 1 > Allowed actions' => [
'Microsoft.Intune/deviceConfigurations/create',
'Microsoft.Intune/deviceConfigurations/read',
],
'Permission block 1 > Conditions' => [
'@Resource[Microsoft.Intune/deviceConfigurations] Exists',
],
],
'is_built_in' => false,
'role_permission_count' => 1,
],
], $overrides);
}
/**
* @param array<string, mixed> $base
* @param array<string, mixed> $overrides
* @return array<string, mixed>
*/
function findingViewRbacFixtureMerge(array $base, array $overrides): array
{
foreach ($overrides as $key => $value) {
if ($key === 'normalized') {
$base[$key] = $value;
continue;
}
if (
is_string($key)
&& array_key_exists($key, $base)
&& is_array($value)
&& is_array($base[$key])
&& ! array_is_list($value)
&& ! array_is_list($base[$key])
) {
$base[$key] = findingViewRbacFixtureMerge($base[$key], $value);
continue;
}
$base[$key] = $value;
}
return $base;
}