## Summary - add the Evidence Snapshot domain with immutable tenant-scoped snapshots, per-dimension items, queued generation, audit actions, badge mappings, and Filament list/detail surfaces - add the workspace evidence overview, capability and policy wiring, Livewire update-path hardening, and review-pack integration through explicit evidence snapshot resolution - add spec 153 artifacts, migrations, factories, and focused Pest coverage for evidence, review-pack reuse, authorization, action-surface regressions, and audit behavior ## Testing - `vendor/bin/sail artisan test --compact --stop-on-failure` - `CI=1 vendor/bin/sail artisan test --compact` - `vendor/bin/sail bin pint --dirty --format agent` ## Notes - branch: `153-evidence-domain-foundation` - commit: `b7dfa279` - spec: `specs/153-evidence-domain-foundation/` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #183
190 lines
7.3 KiB
PHP
190 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Support\Badges;
|
|
|
|
use App\Support\Tenants\TenantLifecyclePresentation;
|
|
use BackedEnum;
|
|
use Stringable;
|
|
use Throwable;
|
|
|
|
final class BadgeCatalog
|
|
{
|
|
/**
|
|
* @var array<string, class-string<BadgeMapper>>
|
|
*/
|
|
private const DOMAIN_MAPPERS = [
|
|
BadgeDomain::AuditOutcome->value => Domains\AuditOutcomeBadge::class,
|
|
BadgeDomain::AuditActorType->value => Domains\AuditActorTypeBadge::class,
|
|
BadgeDomain::BaselineSnapshotFidelity->value => Domains\BaselineSnapshotFidelityBadge::class,
|
|
BadgeDomain::BaselineSnapshotGapStatus->value => Domains\BaselineSnapshotGapStatusBadge::class,
|
|
BadgeDomain::OperationRunStatus->value => Domains\OperationRunStatusBadge::class,
|
|
BadgeDomain::OperationRunOutcome->value => Domains\OperationRunOutcomeBadge::class,
|
|
BadgeDomain::BackupSetStatus->value => Domains\BackupSetStatusBadge::class,
|
|
BadgeDomain::RestoreRunStatus->value => Domains\RestoreRunStatusBadge::class,
|
|
BadgeDomain::RestoreCheckSeverity->value => Domains\RestoreCheckSeverityBadge::class,
|
|
BadgeDomain::FindingStatus->value => Domains\FindingStatusBadge::class,
|
|
BadgeDomain::FindingSeverity->value => Domains\FindingSeverityBadge::class,
|
|
BadgeDomain::BooleanEnabled->value => Domains\BooleanEnabledBadge::class,
|
|
BadgeDomain::BooleanHasErrors->value => Domains\BooleanHasErrorsBadge::class,
|
|
BadgeDomain::TenantStatus->value => Domains\TenantStatusBadge::class,
|
|
BadgeDomain::TenantAppStatus->value => Domains\TenantAppStatusBadge::class,
|
|
BadgeDomain::TenantRbacStatus->value => Domains\TenantRbacStatusBadge::class,
|
|
BadgeDomain::TenantPermissionStatus->value => Domains\TenantPermissionStatusBadge::class,
|
|
BadgeDomain::PolicySnapshotMode->value => Domains\PolicySnapshotModeBadge::class,
|
|
BadgeDomain::PolicyRestoreMode->value => Domains\PolicyRestoreModeBadge::class,
|
|
BadgeDomain::PolicyRisk->value => Domains\PolicyRiskBadge::class,
|
|
BadgeDomain::IgnoredAt->value => Domains\IgnoredAtBadge::class,
|
|
BadgeDomain::RestorePreviewDecision->value => Domains\RestorePreviewDecisionBadge::class,
|
|
BadgeDomain::RestoreResultStatus->value => Domains\RestoreResultStatusBadge::class,
|
|
BadgeDomain::ProviderConnectionStatus->value => Domains\ProviderConnectionStatusBadge::class,
|
|
BadgeDomain::ProviderConnectionHealth->value => Domains\ProviderConnectionHealthBadge::class,
|
|
BadgeDomain::ManagedTenantOnboardingVerificationStatus->value => Domains\ManagedTenantOnboardingVerificationStatusBadge::class,
|
|
BadgeDomain::VerificationCheckStatus->value => Domains\VerificationCheckStatusBadge::class,
|
|
BadgeDomain::VerificationCheckSeverity->value => Domains\VerificationCheckSeverityBadge::class,
|
|
BadgeDomain::VerificationReportOverall->value => Domains\VerificationReportOverallBadge::class,
|
|
BadgeDomain::AlertDeliveryStatus->value => Domains\AlertDeliveryStatusBadge::class,
|
|
BadgeDomain::AlertDestinationLastTestStatus->value => Domains\AlertDestinationLastTestStatusBadge::class,
|
|
BadgeDomain::BaselineProfileStatus->value => Domains\BaselineProfileStatusBadge::class,
|
|
BadgeDomain::FindingType->value => Domains\FindingTypeBadge::class,
|
|
BadgeDomain::ReviewPackStatus->value => Domains\ReviewPackStatusBadge::class,
|
|
BadgeDomain::EvidenceSnapshotStatus->value => Domains\EvidenceSnapshotStatusBadge::class,
|
|
BadgeDomain::EvidenceCompleteness->value => Domains\EvidenceCompletenessBadge::class,
|
|
BadgeDomain::SystemHealth->value => Domains\SystemHealthBadge::class,
|
|
BadgeDomain::ReferenceResolutionState->value => Domains\ReferenceResolutionStateBadge::class,
|
|
BadgeDomain::DiffRowStatus->value => Domains\DiffRowStatusBadge::class,
|
|
];
|
|
|
|
/**
|
|
* @var array<string, BadgeMapper|null>
|
|
*/
|
|
private static array $mapperCache = [];
|
|
|
|
public static function spec(BadgeDomain $domain, mixed $value): BadgeSpec
|
|
{
|
|
$mapper = self::mapper($domain);
|
|
|
|
if (! $mapper) {
|
|
return BadgeSpec::unknown();
|
|
}
|
|
|
|
try {
|
|
return $mapper->spec($value);
|
|
} catch (Throwable) {
|
|
return BadgeSpec::unknown();
|
|
}
|
|
}
|
|
|
|
public static function mapper(BadgeDomain $domain): ?BadgeMapper
|
|
{
|
|
$key = $domain->value;
|
|
|
|
if (array_key_exists($key, self::$mapperCache)) {
|
|
return self::$mapperCache[$key];
|
|
}
|
|
|
|
$mapper = self::buildMapper($domain);
|
|
|
|
self::$mapperCache[$key] = $mapper;
|
|
|
|
return $mapper;
|
|
}
|
|
|
|
public static function normalizeState(mixed $value): ?string
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
if ($value instanceof BackedEnum) {
|
|
$value = $value->value;
|
|
}
|
|
|
|
if ($value instanceof Stringable) {
|
|
$value = (string) $value;
|
|
}
|
|
|
|
if (is_bool($value)) {
|
|
return $value ? 'true' : 'false';
|
|
}
|
|
|
|
if (is_int($value) || is_float($value)) {
|
|
return (string) $value;
|
|
}
|
|
|
|
if (! is_string($value)) {
|
|
return null;
|
|
}
|
|
|
|
$normalized = strtolower(trim($value));
|
|
$normalized = str_replace([' ', '-'], '_', $normalized);
|
|
|
|
return $normalized === '' ? null : $normalized;
|
|
}
|
|
|
|
public static function normalizeProviderConnectionStatus(mixed $value): ?string
|
|
{
|
|
$state = self::normalizeState($value);
|
|
|
|
return match ($state) {
|
|
'granted', 'connected' => 'connected',
|
|
'consent_required', 'required', 'needs_admin_consent', 'needs_consent', 'unknown' => 'needs_consent',
|
|
'failed', 'revoked', 'blocked' => 'error',
|
|
default => $state,
|
|
};
|
|
}
|
|
|
|
public static function normalizeProviderConnectionHealth(mixed $value): ?string
|
|
{
|
|
$state = self::normalizeState($value);
|
|
|
|
return match ($state) {
|
|
'healthy' => 'ok',
|
|
'blocked', 'error' => 'down',
|
|
default => $state,
|
|
};
|
|
}
|
|
|
|
public static function normalizeManagedTenantOnboardingVerificationStatus(mixed $value): ?string
|
|
{
|
|
$state = self::normalizeState($value);
|
|
|
|
return match ($state) {
|
|
'unknown', 'not_started' => 'not_started',
|
|
'pending', 'in_progress' => 'in_progress',
|
|
'degraded', 'needs_attention' => 'needs_attention',
|
|
'blocked', 'error' => 'blocked',
|
|
'healthy', 'ready' => 'ready',
|
|
default => $state,
|
|
};
|
|
}
|
|
|
|
public static function normalizeTenantLifecycle(mixed $value): ?string
|
|
{
|
|
$presentation = self::tenantLifecyclePresentation($value);
|
|
|
|
return $presentation->isInvalidFallback ? null : $presentation->value;
|
|
}
|
|
|
|
public static function tenantLifecyclePresentation(mixed $value): TenantLifecyclePresentation
|
|
{
|
|
return TenantLifecyclePresentation::fromValue($value);
|
|
}
|
|
|
|
private static function buildMapper(BadgeDomain $domain): ?BadgeMapper
|
|
{
|
|
$mapperClass = self::DOMAIN_MAPPERS[$domain->value] ?? null;
|
|
|
|
if (! $mapperClass) {
|
|
return null;
|
|
}
|
|
|
|
if (! class_exists($mapperClass)) {
|
|
return null;
|
|
}
|
|
|
|
$mapper = new $mapperClass;
|
|
|
|
return $mapper instanceof BadgeMapper ? $mapper : null;
|
|
}
|
|
}
|