## Summary - add a workspace-scoped baseline compare matrix page under baseline profiles - derive matrix tenant summaries, subject rows, cell states, freshness, and trust from existing snapshots, compare runs, and findings - add confirmation-gated `Compare assigned tenants` actions on the baseline detail and matrix surfaces without introducing a workspace umbrella run - preserve matrix navigation context into tenant compare and finding drilldowns and add centralized matrix badge semantics - include spec, plan, data model, contracts, quickstart, tasks, and focused feature/browser coverage for Spec 190 ## Verification - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Badges/BaselineCompareMatrixBadgesTest.php tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php tests/Feature/Baselines/BaselineComparePerformanceGuardTest.php tests/Feature/Filament/BaselineCompareMatrixPageTest.php tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Guards/NoAdHocStatusBadgesTest.php tests/Feature/Guards/NoDiagnosticWarningBadgesTest.php` - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - completed an integrated-browser smoke flow locally for matrix render, differ filter, finding drilldown round-trip, and `Compare assigned tenants` confirmation/action Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #221
273 lines
9.7 KiB
PHP
273 lines
9.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Concerns;
|
|
|
|
use App\Models\BaselineProfile;
|
|
use App\Models\BaselineSnapshot;
|
|
use App\Models\BaselineSnapshotItem;
|
|
use App\Models\BaselineTenantAssignment;
|
|
use App\Models\Finding;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Support\Baselines\BaselineCaptureMode;
|
|
use App\Support\Baselines\BaselineProfileStatus;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Filament\Facades\Filament;
|
|
use Tests\Feature\Baselines\Support\BaselineSubjectResolutionFixtures;
|
|
|
|
trait BuildsBaselineCompareMatrixFixtures
|
|
{
|
|
/**
|
|
* @return array{
|
|
* user: User,
|
|
* workspace: Workspace,
|
|
* profile: BaselineProfile,
|
|
* snapshot: BaselineSnapshot,
|
|
* visibleTenant: Tenant,
|
|
* visibleTenantTwo: Tenant,
|
|
* hiddenTenant: Tenant,
|
|
* subjects: array<string, BaselineSnapshotItem>
|
|
* }
|
|
*/
|
|
protected function makeBaselineCompareMatrixFixture(
|
|
string $viewerRole = 'owner',
|
|
?string $workspaceRole = null,
|
|
): array {
|
|
[$user, $visibleTenant] = createUserWithTenant(role: $viewerRole, workspaceRole: $workspaceRole ?? $viewerRole);
|
|
|
|
$workspace = Workspace::query()->findOrFail((int) $visibleTenant->workspace_id);
|
|
|
|
$profile = BaselineProfile::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'status' => BaselineProfileStatus::Active->value,
|
|
'capture_mode' => BaselineCaptureMode::Opportunistic->value,
|
|
'name' => 'Visible-set baseline',
|
|
'scope_jsonb' => [
|
|
'policy_types' => ['deviceConfiguration', 'compliancePolicy'],
|
|
'foundation_types' => [],
|
|
],
|
|
]);
|
|
|
|
$snapshot = BaselineSnapshot::factory()->complete()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'captured_at' => now()->subHours(2),
|
|
'completed_at' => now()->subHours(2),
|
|
]);
|
|
|
|
$profile->forceFill([
|
|
'active_snapshot_id' => (int) $snapshot->getKey(),
|
|
])->save();
|
|
|
|
$visibleTenantTwo = Tenant::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'name' => 'Northwind',
|
|
]);
|
|
|
|
$hiddenTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'name' => 'Hidden Fabrikam',
|
|
]);
|
|
|
|
$user->tenants()->syncWithoutDetaching([
|
|
(int) $visibleTenantTwo->getKey() => ['role' => 'owner'],
|
|
]);
|
|
|
|
WorkspaceMembership::query()->updateOrCreate([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'user_id' => (int) $user->getKey(),
|
|
], [
|
|
'role' => $workspaceRole ?? $viewerRole,
|
|
]);
|
|
|
|
$this->assignTenantToBaselineProfile($profile, $visibleTenant);
|
|
$this->assignTenantToBaselineProfile($profile, $visibleTenantTwo);
|
|
$this->assignTenantToBaselineProfile($profile, $hiddenTenant);
|
|
|
|
$subjects = [
|
|
'wifi-corp-profile' => $this->makeBaselineCompareMatrixSubject(
|
|
$snapshot,
|
|
'deviceConfiguration',
|
|
'wifi-corp-profile',
|
|
'WiFi Corp Profile',
|
|
'dc:wifi-corp-profile',
|
|
),
|
|
'windows-compliance' => $this->makeBaselineCompareMatrixSubject(
|
|
$snapshot,
|
|
'compliancePolicy',
|
|
'windows-compliance',
|
|
'Windows Compliance',
|
|
'cp:windows-compliance',
|
|
),
|
|
];
|
|
|
|
return [
|
|
'user' => $user,
|
|
'workspace' => $workspace,
|
|
'profile' => $profile,
|
|
'snapshot' => $snapshot,
|
|
'visibleTenant' => $visibleTenant,
|
|
'visibleTenantTwo' => $visibleTenantTwo,
|
|
'hiddenTenant' => $hiddenTenant,
|
|
'subjects' => $subjects,
|
|
];
|
|
}
|
|
|
|
protected function makeBaselineCompareMatrixSubject(
|
|
BaselineSnapshot $snapshot,
|
|
string $policyType,
|
|
string $subjectKey,
|
|
string $displayName,
|
|
?string $subjectExternalId = null,
|
|
): BaselineSnapshotItem {
|
|
return BaselineSnapshotItem::factory()->create([
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'policy_type' => $policyType,
|
|
'subject_key' => $subjectKey,
|
|
'subject_external_id' => $subjectExternalId ?? $policyType.':'.$subjectKey,
|
|
'meta_jsonb' => ['display_name' => $displayName],
|
|
]);
|
|
}
|
|
|
|
protected function assignTenantToBaselineProfile(BaselineProfile $profile, Tenant $tenant): BaselineTenantAssignment
|
|
{
|
|
return BaselineTenantAssignment::factory()->create([
|
|
'workspace_id' => (int) $profile->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $contextOverrides
|
|
* @param array<string, mixed> $attributes
|
|
*/
|
|
protected function makeBaselineCompareMatrixRun(
|
|
Tenant $tenant,
|
|
BaselineProfile $profile,
|
|
BaselineSnapshot $snapshot,
|
|
array $contextOverrides = [],
|
|
array $attributes = [],
|
|
): OperationRun {
|
|
$defaults = [
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'initiator_name' => 'Spec190 Matrix',
|
|
'summary_counts' => [
|
|
'matched_items' => 1,
|
|
'different_items' => 0,
|
|
'missing_items' => 0,
|
|
'unexpected_items' => 0,
|
|
],
|
|
'context' => array_replace_recursive([
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'baseline_compare' => [
|
|
'reason_code' => null,
|
|
'subjects_total' => 2,
|
|
'fidelity' => 'content',
|
|
'coverage' => [
|
|
'proof' => true,
|
|
'effective_types' => ['deviceConfiguration', 'compliancePolicy'],
|
|
'covered_types' => ['deviceConfiguration', 'compliancePolicy'],
|
|
'uncovered_types' => [],
|
|
],
|
|
'evidence_gaps' => [
|
|
'count' => 0,
|
|
'by_reason' => [],
|
|
'subjects' => [],
|
|
],
|
|
],
|
|
], $contextOverrides),
|
|
'started_at' => now()->subMinutes(5),
|
|
'completed_at' => now()->subMinute(),
|
|
];
|
|
|
|
return OperationRun::factory()->create(array_replace_recursive($defaults, $attributes));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $overrides
|
|
*/
|
|
protected function makeBaselineCompareMatrixFinding(
|
|
Tenant $tenant,
|
|
BaselineProfile $profile,
|
|
OperationRun $run,
|
|
string $subjectKey,
|
|
array $overrides = [],
|
|
): Finding {
|
|
$defaults = [
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
'source' => 'baseline.compare',
|
|
'scope_key' => 'baseline_profile:'.(int) $profile->getKey(),
|
|
'baseline_operation_run_id' => (int) $run->getKey(),
|
|
'current_operation_run_id' => (int) $run->getKey(),
|
|
'subject_type' => 'policy',
|
|
'subject_external_id' => 'subject:'.$subjectKey,
|
|
'severity' => Finding::SEVERITY_HIGH,
|
|
'status' => Finding::STATUS_NEW,
|
|
'evidence_jsonb' => [
|
|
'subject_key' => $subjectKey,
|
|
'change_type' => 'different_version',
|
|
],
|
|
];
|
|
|
|
return Finding::factory()->create(array_replace_recursive($defaults, $overrides));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $overrides
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function baselineCompareMatrixGap(string $policyType, string $subjectKey, array $overrides = []): array
|
|
{
|
|
return BaselineSubjectResolutionFixtures::structuredGap(array_replace([
|
|
'policy_type' => $policyType,
|
|
'subject_key' => $subjectKey,
|
|
], $overrides));
|
|
}
|
|
|
|
protected function setAdminWorkspaceContext(User $user, Workspace|int $workspace, ?Tenant $rememberedTenant = null): array
|
|
{
|
|
$workspaceId = $workspace instanceof Workspace ? (int) $workspace->getKey() : (int) $workspace;
|
|
|
|
$session = [
|
|
WorkspaceContext::SESSION_KEY => $workspaceId,
|
|
];
|
|
|
|
if ($rememberedTenant instanceof Tenant) {
|
|
$session[WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY] = [
|
|
(string) $workspaceId => (int) $rememberedTenant->getKey(),
|
|
];
|
|
}
|
|
|
|
$this->actingAs($user)->withSession($session);
|
|
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
|
|
|
if ($rememberedTenant instanceof Tenant) {
|
|
session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [
|
|
(string) $workspaceId => (int) $rememberedTenant->getKey(),
|
|
]);
|
|
}
|
|
|
|
Filament::setCurrentPanel('admin');
|
|
Filament::setTenant(null, true);
|
|
Filament::bootCurrentPanel();
|
|
|
|
return $session;
|
|
}
|
|
}
|