$severityCounts * @param list $uncoveredTypes * @param array $evidenceGapsTopReasons */ private function __construct( public readonly string $state, public readonly ?string $message, public readonly ?string $profileName, public readonly ?int $profileId, public readonly ?int $snapshotId, public readonly ?int $duplicateNamePoliciesCount, public readonly ?int $operationRunId, public readonly ?int $findingsCount, public readonly array $severityCounts, public readonly ?string $lastComparedHuman, public readonly ?string $lastComparedIso, public readonly ?string $failureReason, public readonly ?string $reasonCode = null, public readonly ?string $reasonMessage = null, public readonly ?string $coverageStatus = null, public readonly ?int $uncoveredTypesCount = null, public readonly array $uncoveredTypes = [], public readonly ?string $fidelity = null, public readonly ?int $evidenceGapsCount = null, public readonly array $evidenceGapsTopReasons = [], public readonly ?array $rbacRoleDefinitionSummary = null, ) {} public static function forTenant(?Tenant $tenant): self { if (! $tenant instanceof Tenant) { return self::empty('no_tenant', 'No tenant selected.'); } $assignment = BaselineTenantAssignment::query() ->where('tenant_id', $tenant->getKey()) ->first(); if (! $assignment instanceof BaselineTenantAssignment) { return self::empty( 'no_assignment', 'This tenant has no baseline assignment. A workspace manager can assign a baseline profile to this tenant.', ); } $profile = $assignment->baselineProfile; if (! $profile instanceof BaselineProfile) { return self::empty( 'no_assignment', 'The assigned baseline profile no longer exists.', ); } $profileName = (string) $profile->name; $profileId = (int) $profile->getKey(); $snapshotId = $profile->active_snapshot_id !== null ? (int) $profile->active_snapshot_id : null; $profileScope = BaselineScope::fromJsonb( is_array($profile->scope_jsonb) ? $profile->scope_jsonb : null, ); $overrideScope = $assignment->override_scope_jsonb !== null ? BaselineScope::fromJsonb(is_array($assignment->override_scope_jsonb) ? $assignment->override_scope_jsonb : null) : null; $effectiveScope = BaselineScope::effective($profileScope, $overrideScope); $duplicateNamePoliciesCount = self::duplicateNamePoliciesCount($tenant, $effectiveScope); if ($snapshotId === null) { return self::empty( 'no_snapshot', 'The baseline profile has no active snapshot yet. A workspace manager needs to capture a snapshot first.', profileName: $profileName, profileId: $profileId, duplicateNamePoliciesCount: $duplicateNamePoliciesCount, ); } $latestRun = OperationRun::query() ->where('tenant_id', $tenant->getKey()) ->where('type', 'baseline_compare') ->latest('id') ->first(); [$coverageStatus, $uncoveredTypes, $fidelity] = self::coverageInfoForRun($latestRun); [$evidenceGapsCount, $evidenceGapsTopReasons] = self::evidenceGapSummaryForRun($latestRun); [$reasonCode, $reasonMessage] = self::reasonInfoForRun($latestRun); $rbacRoleDefinitionSummary = self::rbacRoleDefinitionSummaryForRun($latestRun); // Active run (queued/running) if ($latestRun instanceof OperationRun && in_array($latestRun->status, ['queued', 'running'], true)) { return new self( state: 'comparing', message: 'A baseline comparison is currently in progress.', profileName: $profileName, profileId: $profileId, snapshotId: $snapshotId, duplicateNamePoliciesCount: $duplicateNamePoliciesCount, operationRunId: (int) $latestRun->getKey(), findingsCount: null, severityCounts: [], lastComparedHuman: null, lastComparedIso: null, failureReason: null, reasonCode: $reasonCode, reasonMessage: $reasonMessage, coverageStatus: $coverageStatus, uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0, uncoveredTypes: $uncoveredTypes, fidelity: $fidelity, evidenceGapsCount: $evidenceGapsCount, evidenceGapsTopReasons: $evidenceGapsTopReasons, rbacRoleDefinitionSummary: $rbacRoleDefinitionSummary, ); } // Failed run — explicit error state if ($latestRun instanceof OperationRun && $latestRun->outcome === 'failed') { $failureSummary = is_array($latestRun->failure_summary) ? $latestRun->failure_summary : []; $failureReason = $failureSummary['message'] ?? $failureSummary['reason'] ?? 'The comparison job failed. Check the run details for more information.'; return new self( state: 'failed', message: (string) $failureReason, profileName: $profileName, profileId: $profileId, snapshotId: $snapshotId, duplicateNamePoliciesCount: $duplicateNamePoliciesCount, operationRunId: (int) $latestRun->getKey(), findingsCount: null, severityCounts: [], lastComparedHuman: $latestRun->finished_at?->diffForHumans(), lastComparedIso: $latestRun->finished_at?->toIso8601String(), failureReason: (string) $failureReason, reasonCode: $reasonCode, reasonMessage: $reasonMessage, coverageStatus: $coverageStatus, uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0, uncoveredTypes: $uncoveredTypes, fidelity: $fidelity, evidenceGapsCount: $evidenceGapsCount, evidenceGapsTopReasons: $evidenceGapsTopReasons, rbacRoleDefinitionSummary: $rbacRoleDefinitionSummary, ); } $lastComparedHuman = null; $lastComparedIso = null; if ($latestRun instanceof OperationRun && $latestRun->finished_at !== null) { $lastComparedHuman = $latestRun->finished_at->diffForHumans(); $lastComparedIso = $latestRun->finished_at->toIso8601String(); } $scopeKey = 'baseline_profile:'.$profile->getKey(); // Single grouped query instead of 4 separate COUNT queries $severityRows = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_DRIFT) ->where('source', 'baseline.compare') ->where('scope_key', $scopeKey) ->whereIn('status', Finding::openStatusesForQuery()) ->selectRaw('severity, count(*) as cnt') ->groupBy('severity') ->pluck('cnt', 'severity'); $totalFindings = (int) $severityRows->sum(); $severityCounts = [ 'high' => (int) ($severityRows[Finding::SEVERITY_HIGH] ?? 0), 'medium' => (int) ($severityRows[Finding::SEVERITY_MEDIUM] ?? 0), 'low' => (int) ($severityRows[Finding::SEVERITY_LOW] ?? 0), ]; if ($totalFindings > 0) { return new self( state: 'ready', message: null, profileName: $profileName, profileId: $profileId, snapshotId: $snapshotId, duplicateNamePoliciesCount: $duplicateNamePoliciesCount, operationRunId: $latestRun instanceof OperationRun ? (int) $latestRun->getKey() : null, findingsCount: $totalFindings, severityCounts: $severityCounts, lastComparedHuman: $lastComparedHuman, lastComparedIso: $lastComparedIso, failureReason: null, reasonCode: $reasonCode, reasonMessage: $reasonMessage, coverageStatus: $coverageStatus, uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0, uncoveredTypes: $uncoveredTypes, fidelity: $fidelity, evidenceGapsCount: $evidenceGapsCount, evidenceGapsTopReasons: $evidenceGapsTopReasons, rbacRoleDefinitionSummary: $rbacRoleDefinitionSummary, ); } if ($latestRun instanceof OperationRun && $latestRun->status === 'completed' && in_array($latestRun->outcome, ['succeeded', 'partially_succeeded'], true)) { return new self( state: 'ready', message: $latestRun->outcome === 'succeeded' ? 'No open drift findings for this baseline comparison. The tenant matches the baseline.' : 'Comparison completed with warnings. Findings may be incomplete due to missing coverage.', profileName: $profileName, profileId: $profileId, snapshotId: $snapshotId, duplicateNamePoliciesCount: $duplicateNamePoliciesCount, operationRunId: (int) $latestRun->getKey(), findingsCount: 0, severityCounts: $severityCounts, lastComparedHuman: $lastComparedHuman, lastComparedIso: $lastComparedIso, failureReason: null, reasonCode: $reasonCode, reasonMessage: $reasonMessage, coverageStatus: $coverageStatus, uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0, uncoveredTypes: $uncoveredTypes, fidelity: $fidelity, evidenceGapsCount: $evidenceGapsCount, evidenceGapsTopReasons: $evidenceGapsTopReasons, rbacRoleDefinitionSummary: $rbacRoleDefinitionSummary, ); } return new self( state: 'idle', message: 'Baseline profile is assigned and has a snapshot. Run "Compare Now" to check for drift.', profileName: $profileName, profileId: $profileId, snapshotId: $snapshotId, duplicateNamePoliciesCount: $duplicateNamePoliciesCount, operationRunId: null, findingsCount: null, severityCounts: $severityCounts, lastComparedHuman: $lastComparedHuman, lastComparedIso: $lastComparedIso, failureReason: null, reasonCode: $reasonCode, reasonMessage: $reasonMessage, coverageStatus: $coverageStatus, uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0, uncoveredTypes: $uncoveredTypes, fidelity: $fidelity, evidenceGapsCount: $evidenceGapsCount, evidenceGapsTopReasons: $evidenceGapsTopReasons, rbacRoleDefinitionSummary: $rbacRoleDefinitionSummary, ); } /** * Create a DTO for widget consumption (only open/new findings). */ public static function forWidget(?Tenant $tenant): self { if (! $tenant instanceof Tenant) { return self::empty('no_tenant', null); } $assignment = BaselineTenantAssignment::query() ->where('tenant_id', $tenant->getKey()) ->with('baselineProfile') ->first(); if (! $assignment instanceof BaselineTenantAssignment || $assignment->baselineProfile === null) { return self::empty('no_assignment', null); } $profile = $assignment->baselineProfile; $scopeKey = 'baseline_profile:'.$profile->getKey(); $severityRows = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_DRIFT) ->where('source', 'baseline.compare') ->where('scope_key', $scopeKey) ->where('status', Finding::STATUS_NEW) ->selectRaw('severity, count(*) as cnt') ->groupBy('severity') ->pluck('cnt', 'severity'); $totalFindings = (int) $severityRows->sum(); $latestRun = OperationRun::query() ->where('tenant_id', $tenant->getKey()) ->where('type', 'baseline_compare') ->where('context->baseline_profile_id', (string) $profile->getKey()) ->whereNotNull('completed_at') ->latest('completed_at') ->first(); return new self( state: $totalFindings > 0 ? 'ready' : 'idle', message: null, profileName: (string) $profile->name, profileId: (int) $profile->getKey(), snapshotId: $profile->active_snapshot_id !== null ? (int) $profile->active_snapshot_id : null, duplicateNamePoliciesCount: null, operationRunId: $latestRun instanceof OperationRun ? (int) $latestRun->getKey() : null, findingsCount: $totalFindings, severityCounts: [ 'high' => (int) ($severityRows[Finding::SEVERITY_HIGH] ?? 0), 'medium' => (int) ($severityRows[Finding::SEVERITY_MEDIUM] ?? 0), 'low' => (int) ($severityRows[Finding::SEVERITY_LOW] ?? 0), ], lastComparedHuman: $latestRun?->finished_at?->diffForHumans(), lastComparedIso: $latestRun?->finished_at?->toIso8601String(), failureReason: null, ); } private static function duplicateNamePoliciesCount(Tenant $tenant, BaselineScope $effectiveScope): int { $policyTypes = $effectiveScope->allTypes(); if ($policyTypes === []) { return 0; } $latestInventorySyncRunId = self::latestInventorySyncRunId($tenant); $compute = static function () use ($tenant, $policyTypes, $latestInventorySyncRunId): int { /** * @var array $countsByKey */ $countsByKey = []; $query = InventoryItem::query() ->where('tenant_id', (int) $tenant->getKey()) ->whereIn('policy_type', $policyTypes) ->whereNotNull('display_name') ->select(['id', 'policy_type', 'display_name']); if (is_int($latestInventorySyncRunId) && $latestInventorySyncRunId > 0) { $query->where('last_seen_operation_run_id', $latestInventorySyncRunId); } $query ->orderBy('id') ->chunkById(1_000, function ($inventoryItems) use (&$countsByKey): void { foreach ($inventoryItems as $inventoryItem) { $displayName = is_string($inventoryItem->display_name) ? $inventoryItem->display_name : null; $subjectKey = BaselineSubjectKey::fromDisplayName($displayName); if ($subjectKey === null) { continue; } $logicalKey = (string) $inventoryItem->policy_type.'|'.$subjectKey; $countsByKey[$logicalKey] = ($countsByKey[$logicalKey] ?? 0) + 1; } }); $duplicatePolicies = 0; foreach ($countsByKey as $count) { if ($count > 1) { $duplicatePolicies += $count; } } return $duplicatePolicies; }; if (app()->environment('testing')) { return $compute(); } $cacheKey = sprintf( 'baseline_compare:tenant:%d:duplicate_names:%s:%s', (int) $tenant->getKey(), hash('sha256', implode('|', $policyTypes)), $latestInventorySyncRunId ?? 'all', ); return (int) Cache::remember($cacheKey, now()->addSeconds(60), $compute); } private static function latestInventorySyncRunId(Tenant $tenant): ?int { $run = OperationRun::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('type', OperationRunType::InventorySync->value) ->where('status', OperationRunStatus::Completed->value) ->orderByDesc('completed_at') ->orderByDesc('id') ->first(['id']); return $run instanceof OperationRun ? (int) $run->getKey() : null; } /** * @return array{0: ?string, 1: list, 2: ?string} */ private static function coverageInfoForRun(?OperationRun $run): array { if (! $run instanceof OperationRun) { return [null, [], null]; } $context = is_array($run->context) ? $run->context : []; $baselineCompare = $context['baseline_compare'] ?? null; if (! is_array($baselineCompare)) { return [null, [], null]; } $coverage = $baselineCompare['coverage'] ?? null; $coverage = is_array($coverage) ? $coverage : []; $proof = $coverage['proof'] ?? null; $proof = is_bool($proof) ? $proof : null; $uncoveredTypes = $coverage['uncovered_types'] ?? null; $uncoveredTypes = is_array($uncoveredTypes) ? array_values(array_filter($uncoveredTypes, 'is_string')) : []; $uncoveredTypes = array_values(array_unique(array_filter(array_map('trim', $uncoveredTypes), fn (string $type): bool => $type !== ''))); sort($uncoveredTypes, SORT_STRING); $coverageStatus = null; if ($proof === false) { $coverageStatus = 'unproven'; } elseif ($uncoveredTypes !== []) { $coverageStatus = 'warning'; } elseif ($proof === true) { $coverageStatus = 'ok'; } $fidelity = $baselineCompare['fidelity'] ?? null; $fidelity = is_string($fidelity) ? trim($fidelity) : null; $fidelity = $fidelity !== '' ? $fidelity : null; return [$coverageStatus, $uncoveredTypes, $fidelity]; } /** * @return array{0: ?string, 1: ?string} */ private static function reasonInfoForRun(?OperationRun $run): array { if (! $run instanceof OperationRun) { return [null, null]; } $context = is_array($run->context) ? $run->context : []; $baselineCompare = $context['baseline_compare'] ?? null; if (! is_array($baselineCompare)) { return [null, null]; } $reasonCode = $baselineCompare['reason_code'] ?? null; $reasonCode = is_string($reasonCode) ? trim($reasonCode) : null; $reasonCode = $reasonCode !== '' ? $reasonCode : null; $enum = $reasonCode !== null ? BaselineCompareReasonCode::tryFrom($reasonCode) : null; return [$reasonCode, $enum?->message()]; } /** * @return array{0: ?int, 1: array} */ private static function evidenceGapSummaryForRun(?OperationRun $run): array { if (! $run instanceof OperationRun) { return [null, []]; } $context = is_array($run->context) ? $run->context : []; $baselineCompare = $context['baseline_compare'] ?? null; if (! is_array($baselineCompare)) { return [null, []]; } $gaps = $baselineCompare['evidence_gaps'] ?? null; if (! is_array($gaps)) { return [null, []]; } $count = $gaps['count'] ?? null; $count = is_numeric($count) ? (int) $count : null; $byReason = $gaps['by_reason'] ?? null; $byReason = is_array($byReason) ? $byReason : []; $normalized = []; foreach ($byReason as $reason => $value) { if (! is_string($reason) || trim($reason) === '' || ! is_numeric($value)) { continue; } $intValue = (int) $value; if ($intValue <= 0) { continue; } $normalized[trim($reason)] = $intValue; } if ($count === null) { $count = array_sum($normalized); } arsort($normalized); return [$count, array_slice($normalized, 0, 6, true)]; } /** * @return array{total_compared: int, unchanged: int, modified: int, missing: int, unexpected: int}|null */ private static function rbacRoleDefinitionSummaryForRun(?OperationRun $run): ?array { if (! $run instanceof OperationRun) { return null; } $context = is_array($run->context) ? $run->context : []; $baselineCompare = $context['baseline_compare'] ?? null; $summary = is_array($baselineCompare) ? ($baselineCompare['rbac_role_definitions'] ?? null) : null; if (! is_array($summary)) { return null; } return [ 'total_compared' => (int) ($summary['total_compared'] ?? 0), 'unchanged' => (int) ($summary['unchanged'] ?? 0), 'modified' => (int) ($summary['modified'] ?? 0), 'missing' => (int) ($summary['missing'] ?? 0), 'unexpected' => (int) ($summary['unexpected'] ?? 0), ]; } private static function empty( string $state, ?string $message, ?string $profileName = null, ?int $profileId = null, ?int $duplicateNamePoliciesCount = null, ): self { return new self( state: $state, message: $message, profileName: $profileName, profileId: $profileId, snapshotId: null, duplicateNamePoliciesCount: $duplicateNamePoliciesCount, operationRunId: null, findingsCount: null, severityCounts: [], lastComparedHuman: null, lastComparedIso: null, failureReason: null, ); } }