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 $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 $overrides * @return array */ 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 $base * @param array $overrides * @return array */ 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; }