Summary: - Baseline Compare landing: enterprise UI (stats grid, critical drift banner, better actions), navigation grouping under Governance, and Action Surface Contract declaration. - Baseline Profile view page: switches from disabled form fields to proper Infolist entries for a clean read-only view. - Fixes tenant name column usages (`display_name` → `name`) in baseline assignment flows. - Dashboard: improved baseline governance widget with severity breakdown + last compared. Notes: - Filament v5 / Livewire v4 compatible. - Destructive actions remain confirmed (`->requiresConfirmation()`). Tests: - `vendor/bin/sail artisan test --compact tests/Feature/Baselines` - `vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #123
114 lines
2.8 KiB
PHP
114 lines
2.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Baselines;
|
|
|
|
/**
|
|
* Value object for baseline scope resolution.
|
|
*
|
|
* A scope defines which policy types are included in a baseline profile.
|
|
* An empty policy_types array means "all types" (no filter).
|
|
*/
|
|
final class BaselineScope
|
|
{
|
|
/**
|
|
* @param array<string> $policyTypes
|
|
*/
|
|
public function __construct(
|
|
public readonly array $policyTypes = [],
|
|
) {}
|
|
|
|
/**
|
|
* Create from the scope_jsonb column value.
|
|
*
|
|
* @param array<string, mixed>|null $scopeJsonb
|
|
*/
|
|
public static function fromJsonb(?array $scopeJsonb): self
|
|
{
|
|
if ($scopeJsonb === null) {
|
|
return new self;
|
|
}
|
|
|
|
$policyTypes = $scopeJsonb['policy_types'] ?? [];
|
|
|
|
return new self(
|
|
policyTypes: is_array($policyTypes) ? array_values(array_filter($policyTypes, 'is_string')) : [],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Normalize the effective scope by intersecting profile scope with an optional override.
|
|
*
|
|
* Override can only narrow the profile scope (subset enforcement).
|
|
* If the profile scope is empty (all types), the override becomes the effective scope.
|
|
* If the override is empty or null, the profile scope is used as-is.
|
|
*/
|
|
public static function effective(self $profileScope, ?self $overrideScope): self
|
|
{
|
|
if ($overrideScope === null || $overrideScope->isEmpty()) {
|
|
return $profileScope;
|
|
}
|
|
|
|
if ($profileScope->isEmpty()) {
|
|
return $overrideScope;
|
|
}
|
|
|
|
$intersected = array_values(array_intersect($profileScope->policyTypes, $overrideScope->policyTypes));
|
|
|
|
return new self(policyTypes: $intersected);
|
|
}
|
|
|
|
/**
|
|
* An empty scope means "all types".
|
|
*/
|
|
public function isEmpty(): bool
|
|
{
|
|
return $this->policyTypes === [];
|
|
}
|
|
|
|
/**
|
|
* Check if a policy type is included in this scope.
|
|
*/
|
|
public function includes(string $policyType): bool
|
|
{
|
|
if ($this->isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
return in_array($policyType, $this->policyTypes, true);
|
|
}
|
|
|
|
/**
|
|
* Validate that override is a subset of the profile scope.
|
|
*/
|
|
public static function isValidOverride(self $profileScope, self $overrideScope): bool
|
|
{
|
|
if ($overrideScope->isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
if ($profileScope->isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
foreach ($overrideScope->policyTypes as $type) {
|
|
if (! in_array($type, $profileScope->policyTypes, true)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function toJsonb(): array
|
|
{
|
|
return [
|
|
'policy_types' => $this->policyTypes,
|
|
];
|
|
}
|
|
}
|