346 lines
13 KiB
PHP
346 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
|
use App\Filament\Resources\EvidenceSnapshotResource\Pages\ListEvidenceSnapshots;
|
|
use App\Filament\Resources\EvidenceSnapshotResource\Pages\ViewEvidenceSnapshot;
|
|
use App\Jobs\GenerateEvidenceSnapshotJob;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\EvidenceSnapshotItem;
|
|
use App\Models\Finding;
|
|
use App\Models\OperationRun;
|
|
use App\Models\StoredReport;
|
|
use App\Models\Tenant;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Evidence\EvidenceCompletenessState;
|
|
use App\Support\Evidence\EvidenceSnapshotStatus;
|
|
use Filament\Actions\ActionGroup;
|
|
use Filament\Facades\Filament;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function seedEvidenceDomain(Tenant $tenant): void
|
|
{
|
|
StoredReport::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE,
|
|
'fingerprint' => hash('sha256', 'permission-'.$tenant->getKey()),
|
|
]);
|
|
|
|
StoredReport::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'report_type' => StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES,
|
|
'fingerprint' => hash('sha256', 'entra-'.$tenant->getKey()),
|
|
]);
|
|
|
|
Finding::factory()->count(2)->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
|
|
Finding::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
]);
|
|
|
|
OperationRun::factory()->forTenant($tenant)->create();
|
|
}
|
|
|
|
it('renders the evidence list page for an authorized user', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
|
|
$this->actingAs($user)
|
|
->get(EvidenceSnapshotResource::getUrl('index', tenant: $tenant))
|
|
->assertOk();
|
|
});
|
|
|
|
it('disables evidence global search while keeping the view page available', function (): void {
|
|
$reflection = new ReflectionClass(EvidenceSnapshotResource::class);
|
|
|
|
expect($reflection->getStaticPropertyValue('isGloballySearchable'))->toBeFalse()
|
|
->and(array_keys(EvidenceSnapshotResource::getPages()))->toContain('view');
|
|
});
|
|
|
|
it('returns 404 for non members on the evidence list route', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
[$user] = createUserWithTenant(role: 'owner');
|
|
|
|
$this->actingAs($user)
|
|
->get(EvidenceSnapshotResource::getUrl('index', tenant: $tenant))
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('returns 403 for tenant members without evidence view capability on the evidence list route', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
|
|
Gate::define(Capabilities::EVIDENCE_VIEW, fn (): bool => false);
|
|
|
|
$this->actingAs($user)
|
|
->get(EvidenceSnapshotResource::getUrl('index', tenant: $tenant))
|
|
->assertForbidden();
|
|
});
|
|
|
|
it('disables create snapshot for readonly members', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
|
|
|
$tenant->makeCurrent();
|
|
Filament::setTenant($tenant, true);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(ListEvidenceSnapshots::class)
|
|
->assertActionVisible('create_snapshot')
|
|
->assertActionDisabled('create_snapshot');
|
|
});
|
|
|
|
it('queues snapshot generation from the list header action', function (): void {
|
|
Queue::fake();
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
|
|
seedEvidenceDomain($tenant);
|
|
|
|
$tenant->makeCurrent();
|
|
Filament::setTenant($tenant, true);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(ListEvidenceSnapshots::class)
|
|
->callAction('create_snapshot')
|
|
->assertNotified();
|
|
|
|
expect(EvidenceSnapshot::query()->count())->toBe(1);
|
|
Queue::assertPushed(GenerateEvidenceSnapshotJob::class);
|
|
});
|
|
|
|
it('renders the view page for an active snapshot', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
|
|
$snapshot = EvidenceSnapshot::query()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'status' => EvidenceSnapshotStatus::Active->value,
|
|
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
|
'summary' => ['finding_count' => 2],
|
|
'generated_at' => now(),
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant))
|
|
->assertOk();
|
|
|
|
$tenant->makeCurrent();
|
|
Filament::setTenant($tenant, true);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(ViewEvidenceSnapshot::class, ['record' => $snapshot->getKey()])
|
|
->assertActionVisible('refresh_snapshot')
|
|
->assertActionVisible('expire_snapshot');
|
|
});
|
|
|
|
it('renders readable evidence dimension summaries and keeps raw json available', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
|
|
$snapshot = EvidenceSnapshot::query()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'status' => EvidenceSnapshotStatus::Active->value,
|
|
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
|
'summary' => [
|
|
'finding_count' => 3,
|
|
'report_count' => 2,
|
|
'operation_count' => 1,
|
|
],
|
|
'generated_at' => now(),
|
|
]);
|
|
|
|
EvidenceSnapshotItem::query()->create([
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'dimension_key' => 'findings_summary',
|
|
'state' => EvidenceCompletenessState::Complete->value,
|
|
'required' => true,
|
|
'source_kind' => 'model_summary',
|
|
'summary_payload' => [
|
|
'count' => 3,
|
|
'open_count' => 2,
|
|
'severity_counts' => [
|
|
'critical' => 1,
|
|
'high' => 1,
|
|
'medium' => 1,
|
|
'low' => 0,
|
|
],
|
|
'entries' => [
|
|
[
|
|
'title' => 'Admin consent missing',
|
|
'severity' => 'critical',
|
|
'status' => 'open',
|
|
],
|
|
],
|
|
],
|
|
'sort_order' => 10,
|
|
]);
|
|
|
|
EvidenceSnapshotItem::query()->create([
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'dimension_key' => 'entra_admin_roles',
|
|
'state' => EvidenceCompletenessState::Complete->value,
|
|
'required' => true,
|
|
'source_kind' => 'stored_report',
|
|
'summary_payload' => [
|
|
'role_count' => 2,
|
|
'roles' => [
|
|
['display_name' => 'Global Administrator'],
|
|
['display_name' => 'Security Administrator'],
|
|
],
|
|
],
|
|
'sort_order' => 20,
|
|
]);
|
|
|
|
EvidenceSnapshotItem::query()->create([
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'dimension_key' => 'operations_summary',
|
|
'state' => EvidenceCompletenessState::Complete->value,
|
|
'required' => true,
|
|
'source_kind' => 'operation_rollup',
|
|
'summary_payload' => [
|
|
'operation_count' => 3,
|
|
'failed_count' => 0,
|
|
'partial_count' => 0,
|
|
'entries' => [
|
|
[
|
|
'type' => 'tenant.evidence.snapshot.generate',
|
|
'outcome' => 'pending',
|
|
'status' => 'running',
|
|
],
|
|
[
|
|
'type' => 'tenant.review_pack.generate',
|
|
'outcome' => 'succeeded',
|
|
'status' => 'completed',
|
|
],
|
|
[
|
|
'type' => 'backup_schedule_purge',
|
|
'outcome' => 'pending',
|
|
'status' => 'queued',
|
|
],
|
|
],
|
|
],
|
|
'sort_order' => 30,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant))
|
|
->assertOk()
|
|
->assertSeeText('3 findings, 2 open.')
|
|
->assertSeeText('Open findings')
|
|
->assertSeeText('Admin consent missing')
|
|
->assertSeeText('2 privileged Entra roles captured.')
|
|
->assertSeeText('Global Administrator')
|
|
->assertSeeText('Evidence snapshot generation · In progress')
|
|
->assertSeeText('Review pack generation · Completed successfully')
|
|
->assertSeeText('Backup schedule purge · Queued for execution')
|
|
->assertDontSeeText('Tenant.evidence.snapshot.generate · Pending · Running')
|
|
->assertSeeText('Copy JSON');
|
|
});
|
|
|
|
it('hides expire actions for expired snapshots on list and detail surfaces', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
|
|
$snapshot = EvidenceSnapshot::query()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'status' => EvidenceSnapshotStatus::Expired->value,
|
|
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
|
'summary' => ['finding_count' => 2],
|
|
'generated_at' => now()->subDay(),
|
|
'expires_at' => now(),
|
|
]);
|
|
|
|
$tenant->makeCurrent();
|
|
Filament::setTenant($tenant, true);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(ListEvidenceSnapshots::class)
|
|
->assertCanSeeTableRecords([$snapshot])
|
|
->assertTableActionHidden('expire', $snapshot);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(ViewEvidenceSnapshot::class, ['record' => $snapshot->getKey()])
|
|
->assertActionHidden('expire_snapshot');
|
|
});
|
|
|
|
it('disables refresh and expire actions for readonly members on snapshot detail', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
|
|
|
$snapshot = EvidenceSnapshot::query()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'status' => EvidenceSnapshotStatus::Active->value,
|
|
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
|
'summary' => ['finding_count' => 2],
|
|
'generated_at' => now(),
|
|
]);
|
|
|
|
$tenant->makeCurrent();
|
|
Filament::setTenant($tenant, true);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(ViewEvidenceSnapshot::class, ['record' => $snapshot->getKey()])
|
|
->assertActionVisible('refresh_snapshot')
|
|
->assertActionDisabled('refresh_snapshot')
|
|
->assertActionVisible('expire_snapshot')
|
|
->assertActionDisabled('expire_snapshot');
|
|
});
|
|
|
|
it('keeps evidence list actions within the declared action surface contract', function (): void {
|
|
$tenant = Tenant::factory()->create();
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
|
|
$snapshot = EvidenceSnapshot::query()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'status' => EvidenceSnapshotStatus::Active->value,
|
|
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
|
'summary' => ['finding_count' => 2, 'missing_dimensions' => 0],
|
|
'generated_at' => now(),
|
|
]);
|
|
|
|
$tenant->makeCurrent();
|
|
Filament::setTenant($tenant, true);
|
|
|
|
$livewire = Livewire::actingAs($user)
|
|
->test(ListEvidenceSnapshots::class)
|
|
->assertCanSeeTableRecords([$snapshot]);
|
|
|
|
$table = $livewire->instance()->getTable();
|
|
$rowActions = $table->getActions();
|
|
|
|
$primaryRowActionNames = collect($rowActions)
|
|
->reject(static fn ($action): bool => $action instanceof ActionGroup)
|
|
->map(static fn ($action): ?string => $action->getName())
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
|
|
expect($primaryRowActionNames)->toBe(['view_snapshot', 'expire'])
|
|
->and($table->getBulkActions())->toBeEmpty()
|
|
->and($table->getRecordUrl($snapshot))->toBe(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot]));
|
|
});
|