'baseline_compare_landing', 'surfaceType' => 'launch_context_support', 'stateFields' => [ [ 'stateKey' => 'baseline_profile_id', 'stateClass' => 'contextual_prefilter', 'carrier' => 'query_param', 'queryRole' => 'scoped_deeplink', 'shareable' => true, 'restorableOnRefresh' => true, 'tenantSensitive' => false, 'invalidFallback' => 'discard_and_continue', ], [ 'stateKey' => 'subject_key', 'stateClass' => 'contextual_prefilter', 'carrier' => 'query_param', 'queryRole' => 'scoped_deeplink', 'shareable' => true, 'restorableOnRefresh' => true, 'tenantSensitive' => false, 'invalidFallback' => 'discard_and_continue', ], [ 'stateKey' => 'nav', 'stateClass' => 'contextual_prefilter', 'carrier' => 'query_param', 'queryRole' => 'durable_restorable', 'shareable' => true, 'restorableOnRefresh' => true, 'tenantSensitive' => false, 'invalidFallback' => 'discard_and_continue', ], ], 'hydrationRule' => [ 'precedenceOrder' => ['query', 'default'], 'appliesOnInitialMountOnly' => true, 'activeStateBecomesAuthoritativeAfterMount' => true, 'clearsOnTenantSwitch' => ['baseline_profile_id', 'subject_key', 'nav'], 'invalidRequestedStateFallback' => 'discard_and_continue', ], 'inspectContract' => [ 'primaryModel' => 'none', 'selectedStateKey' => null, 'openedBy' => ['launch_context'], 'presentation' => 'none', 'shareable' => true, 'invalidSelectionFallback' => 'discard_and_continue', ], 'shareableStateKeys' => ['baseline_profile_id', 'subject_key', 'nav'], 'localOnlyStateKeys' => [], ]; private const LEGACY_SCOPE_QUERY_KEYS = [ 'environment_id', 'tenant', 'tenant_id', 'managed_environment_id', 'environment', 'tenant_scope', 'tableFilters', ]; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-scale'; protected static string|UnitEnum|null $navigationGroup = 'Governance'; protected static ?string $navigationLabel = 'Baseline Compare'; protected static ?int $navigationSort = 10; protected static ?string $title = 'Baseline Compare'; protected static ?string $slug = 'workspaces/{workspace}/environments/{environment}/baseline-compare'; protected string $view = 'filament.pages.baseline-compare-landing'; public static function shouldRegisterNavigation(): bool { return NavigationScope::shouldRegisterEnvironmentNavigation() && parent::shouldRegisterNavigation(); } public ?string $state = null; public ?string $message = null; public ?string $reasonCode = null; public ?string $reasonMessage = null; public ?string $profileName = null; public ?int $profileId = null; public ?int $snapshotId = null; public ?int $duplicateNamePoliciesCount = null; public ?int $duplicateNameSubjectsCount = null; public ?int $operationRunId = null; public ?int $findingsCount = null; /** @var array|null */ public ?array $severityCounts = null; public ?string $lastComparedAt = null; public ?string $lastComparedIso = null; public ?string $failureReason = null; public ?string $coverageStatus = null; public ?int $uncoveredTypesCount = null; /** @var list|null */ public ?array $uncoveredTypes = null; public ?string $fidelity = null; public ?int $evidenceGapsCount = null; /** @var array|null */ public ?array $evidenceGapsTopReasons = null; /** @var array|null */ public ?array $evidenceGapSummary = null; /** @var list>|null */ public ?array $evidenceGapBuckets = null; /** @var array|null */ public ?array $baselineCompareDiagnostics = null; /** @var array|null */ public ?array $rbacRoleDefinitionSummary = null; /** @var array|null */ public ?array $operatorExplanation = null; /** @var array|null */ public ?array $summaryAssessment = null; /** @var array|null */ public ?array $navigationContextPayload = null; public ?int $matrixBaselineProfileId = null; public ?string $matrixSubjectKey = null; #[Locked] public ?int $scopedEnvironmentId = null; /** * @param array $parameters */ public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string { $panelId = $panel ?? Filament::getCurrentOrDefaultPanel()?->getId() ?? 'admin'; if ($panelId !== 'admin') { return parent::getUrl($parameters, $isAbsolute, $panelId, $tenant); } $environment = static::resolveAdminUrlEnvironment($parameters, $tenant); if (! $environment instanceof ManagedEnvironment) { return url('/admin'); } $workspace = static::resolveAdminUrlWorkspace($environment, $parameters); if (! $workspace instanceof Workspace && ! is_string($workspace) && ! is_int($workspace)) { return url('/admin'); } $parameters = static::withoutLegacyScopeQuery($parameters); $parameters['environment'] = $environment; $parameters['workspace'] = $workspace instanceof Workspace ? static::workspaceRouteKey($workspace) : $workspace; return parent::getUrl($parameters, $isAbsolute, $panelId, null); } public static function canAccess(): bool { return static::hasEnvironmentAccess(static::resolveRouteOwnedEnvironment()); } /** * @return array */ public static function monitoringPageStateContract(): array { return self::MONITORING_PAGE_STATE_CONTRACT; } public function mount(ManagedEnvironment|string|null $environment = null): void { $tenant = static::resolveRouteOwnedEnvironment($environment); if (! $tenant instanceof ManagedEnvironment || ! static::hasEnvironmentAccess($tenant)) { abort(404); } $this->scopedEnvironmentId = (int) $tenant->getKey(); $this->navigationContextPayload = is_array(request()->query('nav')) ? request()->query('nav') : null; $baselineProfileId = request()->query('baseline_profile_id'); $subjectKey = request()->query('subject_key'); $this->matrixBaselineProfileId = is_numeric($baselineProfileId) ? (int) $baselineProfileId : null; $this->matrixSubjectKey = is_string($subjectKey) && trim($subjectKey) !== '' ? trim($subjectKey) : null; $this->refreshStatsForEnvironment($tenant); } public function refreshStats(): void { $this->refreshStatsForEnvironment($this->currentEnvironment()); } private function refreshStatsForEnvironment(?ManagedEnvironment $tenant): void { $stats = BaselineCompareStats::forTenant($tenant); $aggregate = $tenant instanceof ManagedEnvironment ? $this->governanceAggregate($tenant, $stats) : null; $this->state = $stats->state; $this->message = $stats->message; $this->profileName = $stats->profileName; $this->profileId = $stats->profileId; $this->snapshotId = $stats->snapshotId; $this->duplicateNamePoliciesCount = $stats->duplicateNamePoliciesCount; $this->duplicateNameSubjectsCount = $stats->duplicateNameSubjectsCount; $this->operationRunId = $stats->operationRunId; $this->findingsCount = $stats->findingsCount; $this->severityCounts = $stats->severityCounts !== [] ? $stats->severityCounts : null; $this->lastComparedAt = $stats->lastComparedHuman; $this->lastComparedIso = $stats->lastComparedIso; $this->failureReason = $stats->failureReason; $this->reasonCode = $stats->reasonCode; $this->reasonMessage = $stats->reasonMessage; $this->coverageStatus = $stats->coverageStatus; $this->uncoveredTypesCount = $stats->uncoveredTypesCount; $this->uncoveredTypes = $stats->uncoveredTypes !== [] ? $stats->uncoveredTypes : null; $this->fidelity = $stats->fidelity; $this->evidenceGapsCount = $stats->evidenceGapsCount; $this->evidenceGapsTopReasons = $stats->evidenceGapsTopReasons !== [] ? $stats->evidenceGapsTopReasons : null; $this->evidenceGapSummary = is_array($stats->evidenceGapDetails['summary'] ?? null) ? $stats->evidenceGapDetails['summary'] : null; $this->evidenceGapBuckets = is_array($stats->evidenceGapDetails['buckets'] ?? null) && $stats->evidenceGapDetails['buckets'] !== [] ? $stats->evidenceGapDetails['buckets'] : null; $this->baselineCompareDiagnostics = $stats->baselineCompareDiagnostics !== [] ? $stats->baselineCompareDiagnostics : null; $this->rbacRoleDefinitionSummary = $stats->rbacRoleDefinitionSummary !== [] ? $stats->rbacRoleDefinitionSummary : null; $this->operatorExplanation = $stats->operatorExplanation()->toArray(); $this->summaryAssessment = $aggregate?->summaryAssessment->toArray(); } /** * Computed view data exposed to the Blade template. * * Moves presentational logic out of Blade `@php` blocks so the * template only receives ready-to-render values. * * @return array */ protected function getViewData(): array { $evidenceGapSummary = is_array($this->evidenceGapSummary) ? $this->evidenceGapSummary : []; $reasonPresenter = app(ReasonPresenter::class); $reasonSemantics = $reasonPresenter->semantics( $reasonPresenter->forArtifactTruth($this->reasonCode, 'baseline_compare_landing'), ); $hasCoverageWarnings = in_array($this->coverageStatus, ['warning', 'unproven'], true); $evidenceGapsCountValue = is_numeric($evidenceGapSummary['count'] ?? null) ? (int) $evidenceGapSummary['count'] : (int) ($this->evidenceGapsCount ?? 0); $hasEvidenceGaps = $evidenceGapsCountValue > 0; $hasWarnings = $hasCoverageWarnings || $hasEvidenceGaps; $hasRbacRoleDefinitionSummary = is_array($this->rbacRoleDefinitionSummary) && array_sum($this->rbacRoleDefinitionSummary) > 0; $evidenceGapDetailState = is_string($evidenceGapSummary['detail_state'] ?? null) ? (string) $evidenceGapSummary['detail_state'] : 'no_gaps'; $hasEvidenceGapDetailSection = $evidenceGapDetailState !== 'no_gaps'; $hasEvidenceGapDiagnostics = is_array($this->baselineCompareDiagnostics) && $this->baselineCompareDiagnostics !== []; $evidenceGapsSummary = null; $evidenceGapsTooltip = null; if ($hasEvidenceGaps) { $parts = array_map( static fn (array $reason): string => $reason['reason_label'].' ('.$reason['count'].')', BaselineCompareEvidenceGapDetails::topReasons( is_array($evidenceGapSummary['by_reason'] ?? null) ? $evidenceGapSummary['by_reason'] : [], 5, ), ); if ($parts !== []) { $evidenceGapsSummary = implode(', ', $parts); $evidenceGapsTooltip = __('baseline-compare.evidence_gaps_tooltip', ['summary' => $evidenceGapsSummary]); } } // Derive the colour class for the findings-count stat card. // Only show danger-red when high-severity findings exist; // use warning-orange for low/medium-only, and success-green for zero. $findingsColorClass = $this->resolveFindingsColorClass($hasWarnings); // "Why no findings" explanation when count is zero. $whyNoFindingsMessage = filled($this->reasonMessage) ? (string) $this->reasonMessage : null; $whyNoFindingsFallback = ! $hasWarnings ? __('baseline-compare.no_findings_all_clear') : ($hasCoverageWarnings ? __('baseline-compare.no_findings_coverage_warnings') : ($hasEvidenceGaps ? __('baseline-compare.no_findings_evidence_gaps') : __('baseline-compare.no_findings_default'))); $whyNoFindingsColor = $hasWarnings ? 'text-warning-600 dark:text-warning-400' : 'text-success-600 dark:text-success-400'; if ($this->reasonCode === 'no_subjects_in_scope') { $whyNoFindingsColor = 'text-gray-600 dark:text-gray-400'; } return [ 'navigationContext' => $this->navigationContext()?->toQuery()['nav'] ?? null, 'matrixBaselineProfileId' => $this->matrixBaselineProfileId, 'matrixSubjectKey' => $this->matrixSubjectKey, 'openCompareMatrixUrl' => $this->openCompareMatrixUrl(), 'decisionCard' => $this->decisionCard($hasWarnings, $hasCoverageWarnings, $hasEvidenceGaps), 'decisionSummaryItems' => $this->decisionSummaryItems($hasWarnings, $hasCoverageWarnings, $hasEvidenceGaps), 'proofPanelItems' => $this->proofPanelItems($hasWarnings, $hasCoverageWarnings, $hasEvidenceGaps), 'compareReadinessFlow' => $this->compareReadinessFlow($hasWarnings, $hasCoverageWarnings), 'availableCompareInputs' => $this->availableCompareInputs($hasWarnings, $hasCoverageWarnings, $hasEvidenceGaps), 'assignmentUnlocks' => $this->assignmentUnlocks(), 'diagnosticsDisclosure' => $this->diagnosticsDisclosure($hasEvidenceGapDiagnostics), 'hasCoverageWarnings' => $hasCoverageWarnings, 'evidenceGapsCountValue' => $evidenceGapsCountValue, 'hasEvidenceGaps' => $hasEvidenceGaps, 'hasWarnings' => $hasWarnings, 'hasRbacRoleDefinitionSummary' => $hasRbacRoleDefinitionSummary, 'evidenceGapDetailState' => $evidenceGapDetailState, 'hasEvidenceGapDetailSection' => $hasEvidenceGapDetailSection, 'hasEvidenceGapDiagnostics' => $hasEvidenceGapDiagnostics, 'evidenceGapsSummary' => $evidenceGapsSummary, 'evidenceGapsTooltip' => $evidenceGapsTooltip, 'findingsColorClass' => $findingsColorClass, 'whyNoFindingsMessage' => $whyNoFindingsMessage, 'whyNoFindingsFallback' => $whyNoFindingsFallback, 'whyNoFindingsColor' => $whyNoFindingsColor, 'summaryAssessment' => is_array($this->summaryAssessment) ? $this->summaryAssessment : null, 'reasonSemantics' => $reasonSemantics, ]; } /** * @return array */ private function decisionCard(bool $hasWarnings, bool $hasCoverageWarnings, bool $hasEvidenceGaps): array { $state = (string) ($this->state ?? 'unknown'); $findingsCount = (int) ($this->findingsCount ?? 0); $hasHighSeverity = ($this->severityCounts['high'] ?? 0) > 0; $primaryAction = $this->primaryDecisionAction($state, $findingsCount); return [ 'question' => 'Which baseline drift requires action?', 'statusLabel' => 'Status', 'status' => $this->decisionStatus($state, $findingsCount, $hasWarnings), 'tone' => match (true) { $state === 'failed' || $hasHighSeverity => 'danger', in_array($state, ['no_assignment', 'no_snapshot', 'invalid_scope'], true) || $hasWarnings || $findingsCount > 0 => 'warning', $state === 'comparing' => 'info', $state === 'ready' => 'success', default => 'gray', }, 'reasonLabel' => 'Reason', 'reason' => $this->decisionReason($state, $findingsCount), 'impactLabel' => 'Impact', 'impact' => $this->decisionImpact($state, $findingsCount, $hasCoverageWarnings, $hasEvidenceGaps), 'evidenceLabel' => 'Evidence path', 'evidence' => $this->evidencePathSummary($hasCoverageWarnings, $hasEvidenceGaps), 'nextActionLabel' => 'Next action', ...$primaryAction, ]; } private function decisionStatus(string $state, int $findingsCount, bool $hasWarnings): string { return match ($state) { 'no_assignment' => 'Baseline not assigned', 'no_snapshot' => 'Baseline snapshot required', 'invalid_scope' => 'Baseline scope requires review', 'comparing' => 'Compare in progress', 'failed' => 'Compare failed', 'idle' => 'Compare run required', 'ready' => $findingsCount > 0 ? 'Drift findings available' : ($hasWarnings ? 'Decision output needs review' : 'No drift detected'), default => 'Compare unavailable', }; } private function decisionReason(string $state, int $findingsCount): string { $reason = trim((string) ($this->reasonMessage ?? '')); $message = trim((string) ($this->message ?? '')); return match ($state) { 'no_assignment' => 'This environment does not have an assigned baseline.', 'no_snapshot' => 'A baseline is assigned, but no usable baseline snapshot is available.' .($reason !== '' ? ' '.$reason : ($message !== '' ? ' '.$message : '')), 'invalid_scope' => 'A baseline is assigned, but its scope cannot be used safely for compare.' .($reason !== '' ? ' '.$reason : ($message !== '' ? ' '.$message : '')), 'failed' => $this->failureReason ?: ($reason !== '' ? $reason : 'The compare operation ended with errors.'), 'comparing' => 'Baseline comparison is currently running.', 'idle' => 'Required inputs exist, but no compare run has been created for the current state.', 'ready' => $findingsCount > 0 ? 'Baseline comparison found governance-relevant differences. Drift requires review before a decision is recorded.' : 'Current environment state matches the assigned baseline within available compare coverage.', default => 'Compare state is derived from the latest baseline assignment, snapshot, and operation proof.', }; } private function decisionImpact(string $state, int $findingsCount, bool $hasCoverageWarnings, bool $hasEvidenceGaps): string { if ($state === 'no_assignment') { return 'Baseline drift cannot be used for governance decisions until a baseline assignment exists.'; } if ($state === 'no_snapshot') { return 'Compare cannot run until baseline snapshot input exists.'; } if ($state === 'invalid_scope') { return 'Compare cannot run safely until the assigned baseline scope is reviewed.'; } if ($state === 'failed') { return 'Drift findings cannot be trusted until the failure is resolved. Review operation proof before retrying.'; } if ($state === 'comparing') { return 'Drift findings are not final yet. Wait for operation proof before acting on drift or evidence state.'; } if ($state === 'idle') { return 'Drift findings are not available yet. Run compare after the required inputs are confirmed.'; } if ($findingsCount > 0) { return 'Review findings and decide the next governance action before presenting this environment as aligned to baseline.'; } if ($hasCoverageWarnings || $hasEvidenceGaps) { return 'Zero findings must not be treated as final while coverage or evidence gaps remain.'; } return 'No governance action is required from this compare result within available compare coverage.'; } /** * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string,actionName?:string} */ private function primaryDecisionAction(string $state, int $findingsCount): array { if ($state === 'no_assignment') { $canOpenBaselines = BaselineProfileResource::canViewAny(); return [ 'actionLabel' => $canOpenBaselines ? 'Open baseline profiles' : 'Baseline assignment unavailable', 'actionUrl' => $canOpenBaselines ? BaselineProfileResource::getUrl('index', panel: 'admin') : null, 'actionDisabled' => ! $canOpenBaselines, 'helperText' => $canOpenBaselines ? 'Open baseline profiles to assign a baseline to this environment.' : 'No authorized baseline assignment path is available from this page.', ]; } if (in_array($state, ['no_snapshot', 'invalid_scope'], true)) { $profileUrl = $this->baselineProfileUrl(); return [ 'actionLabel' => $profileUrl !== null ? 'Open baseline profile' : 'Baseline profile unavailable', 'actionUrl' => $profileUrl, 'actionDisabled' => $profileUrl === null, 'helperText' => $profileUrl !== null ? 'Open the assigned baseline profile to review capture and snapshot state.' : 'No authorized baseline profile path is available from this page.', ]; } if ($state === 'idle') { $canRunCompare = $this->canRunCompareAction(); return [ 'actionLabel' => $canRunCompare ? 'Compare now' : 'Compare unavailable', 'actionUrl' => null, 'actionDisabled' => ! $canRunCompare, 'actionName' => 'compareNow', 'helperText' => $canRunCompare ? 'Use the confirmed Compare now action to generate drift findings.' : 'You are not authorized to start baseline compare from this environment.', ]; } if ($state === 'comparing' && $this->getRunUrl() !== null) { return [ 'actionLabel' => 'View operation progress', 'actionUrl' => $this->getRunUrl(), 'actionDisabled' => false, 'helperText' => null, ]; } if ($state === 'failed' && $this->getRunUrl() !== null) { return [ 'actionLabel' => 'Review compare failure', 'actionUrl' => $this->getRunUrl(), 'actionDisabled' => false, 'helperText' => null, ]; } if ($state === 'ready' && $findingsCount > 0 && $this->getFindingsUrl() !== null) { return [ 'actionLabel' => 'Review drift findings', 'actionUrl' => $this->getFindingsUrl(), 'actionDisabled' => false, 'helperText' => null, ]; } if ($state === 'ready' && $this->getRunUrl() !== null) { return [ 'actionLabel' => 'Review evidence', 'actionUrl' => $this->getRunUrl(), 'actionDisabled' => false, 'helperText' => null, ]; } return [ 'actionLabel' => 'Review compare state', 'actionUrl' => $this->getRunUrl(), 'actionDisabled' => $this->getRunUrl() === null, 'helperText' => $this->getRunUrl() === null ? 'No operation proof link is currently available.' : null, ]; } /** * @return list> */ private function decisionSummaryItems(bool $hasWarnings, bool $hasCoverageWarnings, bool $hasEvidenceGaps): array { if ($this->state === 'no_assignment') { return []; } return [ [ 'label' => 'Assigned baseline', 'value' => $this->profileName ?? 'Baseline not assigned', 'description' => $this->snapshotId !== null ? 'Snapshot #'.$this->snapshotId : 'No consumable snapshot proof is linked.', ], [ 'label' => 'Compare trust', 'value' => $this->compareTrustLabel($hasWarnings), 'description' => $this->coverageDescription($hasCoverageWarnings, $hasEvidenceGaps), ], [ 'label' => 'Drift impact', 'value' => $this->driftImpactLabel(), 'description' => 'Derived from latest findings, severity counts, and compare result state.', ], ]; } /** * @return list> */ private function proofPanelItems(bool $hasWarnings, bool $hasCoverageWarnings, bool $hasEvidenceGaps): array { if ($this->state === 'no_assignment') { return []; } $environmentSnapshotState = $this->environmentSnapshotState($hasCoverageWarnings); $operationProofAvailable = $this->operationRunId !== null && $this->getRunUrl() !== null; $baselineSnapshotState = $this->snapshotId !== null ? 'Available' : ($this->state === 'no_snapshot' ? 'Missing' : 'Unavailable'); $driftFindingsAvailable = $this->state === 'ready'; return [ [ 'key' => 'assigned_baseline', 'label' => 'Assigned baseline', 'value' => $this->profileName ?? 'Baseline not assigned', 'tone' => $this->profileName !== null ? 'success' : 'warning', 'description' => $this->state === 'invalid_scope' ? 'Assignment exists, but the baseline scope requires review.' : 'Environment-owned baseline assignment state.', 'actionLabel' => $this->openCompareMatrixUrl() !== null ? 'Open compare matrix' : null, 'actionUrl' => $this->openCompareMatrixUrl(), ], [ 'key' => 'baseline_snapshot', 'label' => 'Baseline snapshot', 'value' => $baselineSnapshotState, 'tone' => $this->flowTone($baselineSnapshotState), 'description' => $this->snapshotId !== null ? 'Snapshot #'.$this->snapshotId.' is the baseline compare input.' : 'No usable baseline snapshot input is linked.', 'actionLabel' => $this->baselineProfileUrl() !== null ? 'Open baseline profile' : null, 'actionUrl' => $this->baselineProfileUrl(), ], [ 'key' => 'environment_snapshot', 'label' => 'Environment snapshot', 'value' => $environmentSnapshotState, 'tone' => $this->flowTone($environmentSnapshotState), 'description' => $this->environmentSnapshotDescription($environmentSnapshotState), 'actionLabel' => null, 'actionUrl' => null, ], [ 'key' => 'operation_run_proof', 'label' => 'OperationRun proof', 'value' => $operationProofAvailable ? 'Available' : 'Unavailable', 'tone' => $operationProofAvailable ? 'success' : 'gray', 'description' => $operationProofAvailable ? 'Compare proof is linked to an OperationRun.' : 'No compare OperationRun proof is available yet.', 'actionLabel' => $operationProofAvailable ? 'Open operation proof' : null, 'actionUrl' => $operationProofAvailable ? $this->getRunUrl() : null, ], [ 'key' => 'drift_findings', 'label' => 'Drift findings', 'value' => $driftFindingsAvailable ? $this->driftImpactLabel() : 'Unavailable', 'tone' => $driftFindingsAvailable && ((int) ($this->findingsCount ?? 0)) > 0 ? 'warning' : ($driftFindingsAvailable ? 'success' : 'gray'), 'description' => $this->driftFindingsDescription(), 'actionLabel' => $this->getFindingsUrl() !== null && ((int) ($this->findingsCount ?? 0)) > 0 ? 'Review findings' : null, 'actionUrl' => ((int) ($this->findingsCount ?? 0)) > 0 ? $this->getFindingsUrl() : null, ], [ 'key' => 'evidence_path', 'label' => 'Evidence path', 'value' => $this->evidencePathSummary($hasCoverageWarnings, $hasEvidenceGaps), 'tone' => ($hasCoverageWarnings || $hasEvidenceGaps) ? 'warning' : 'gray', 'description' => $this->evidenceInputDescription($hasCoverageWarnings, $hasEvidenceGaps), 'actionLabel' => null, 'actionUrl' => null, ], ]; } /** * @return list> */ private function compareReadinessFlow(bool $hasWarnings, bool $hasCoverageWarnings): array { $state = (string) ($this->state ?? 'unknown'); $environmentSnapshotState = $this->environmentSnapshotState($hasCoverageWarnings); $baselineAssignedState = match ($state) { 'no_assignment' => 'Missing', 'invalid_scope' => 'Needs review', 'no_tenant' => 'Unavailable', default => 'Complete', }; $baselineSnapshotState = match ($state) { 'no_assignment' => 'Unavailable', 'no_snapshot' => 'Missing', 'invalid_scope' => 'Unavailable', default => $this->snapshotId !== null ? 'Available' : 'Unavailable', }; $compareRunState = match ($state) { 'idle' => 'Required', 'comparing' => 'In progress', 'failed' => 'Failed', 'ready' => 'Available', default => 'Unavailable', }; $decisionOutputState = match ($state) { 'ready' => $hasWarnings ? 'Needs review' : 'Available', 'idle' => 'Required', default => 'Unavailable', }; return [ [ 'label' => 'Baseline assigned', 'state' => $baselineAssignedState, 'tone' => $this->flowTone($baselineAssignedState), 'description' => match ($baselineAssignedState) { 'Complete' => 'Baseline assignment exists.', 'Needs review' => 'Assignment scope needs review.', 'Missing' => 'No baseline is assigned.', default => 'Assignment unavailable.', }, 'currentBlocker' => in_array($state, ['no_assignment', 'invalid_scope'], true), ], [ 'label' => 'Baseline snapshot', 'state' => $baselineSnapshotState, 'tone' => $this->flowTone($baselineSnapshotState), 'description' => match ($baselineSnapshotState) { 'Available' => 'Snapshot #'.$this->snapshotId.' is available.', 'Missing' => 'No usable snapshot.', 'Needs review' => 'Snapshot needs review.', default => 'No snapshot linked.', }, 'currentBlocker' => $state === 'no_snapshot', ], [ 'label' => 'Environment snapshot', 'state' => $environmentSnapshotState, 'tone' => $this->flowTone($environmentSnapshotState), 'description' => $this->environmentSnapshotDescription($environmentSnapshotState), 'currentBlocker' => false, ], [ 'label' => 'Compare run', 'state' => $compareRunState, 'tone' => $this->flowTone($compareRunState), 'description' => match ($compareRunState) { 'Available' => 'Completed run available.', 'Required' => 'Run compare.', 'In progress' => 'Queued or running.', 'Failed' => 'Latest run failed.', default => 'Blocked by missing inputs.', }, 'currentBlocker' => in_array($state, ['idle', 'comparing', 'failed'], true), ], [ 'label' => 'Decision output', 'state' => $decisionOutputState, 'tone' => $this->flowTone($decisionOutputState), 'description' => match ($decisionOutputState) { 'Available' => 'Decision output available.', 'Needs review' => 'Evidence or coverage needs review.', 'Required' => 'Run compare first.', default => 'No decision output.', }, 'currentBlocker' => $state === 'ready' && $hasWarnings, ], ]; } /** * @return list> */ private function availableCompareInputs(bool $hasWarnings, bool $hasCoverageWarnings, bool $hasEvidenceGaps): array { if ($this->state !== 'no_assignment') { $environmentSnapshotState = $this->environmentSnapshotState($hasCoverageWarnings); $baselineSnapshotState = $this->state === 'no_snapshot' ? 'Missing' : ($this->snapshotId !== null ? 'Available' : 'Unavailable'); $operationProofState = $this->operationRunId !== null && $this->getRunUrl() !== null ? 'Available' : 'Unavailable'; $driftFindingsState = $this->state === 'ready' ? 'Available' : 'Unavailable'; $evidencePathState = $this->evidenceInputState($hasWarnings); return [ [ 'label' => 'Assigned baseline', 'state' => $this->state === 'invalid_scope' ? 'Needs review' : 'Available', 'tone' => $this->state === 'invalid_scope' ? 'warning' : 'success', 'description' => $this->profileName !== null ? 'Assigned baseline: '.$this->profileName.'.' : 'Baseline assignment exists but requires review.', ], [ 'label' => 'Baseline snapshot', 'state' => $baselineSnapshotState, 'tone' => $this->flowTone($baselineSnapshotState), 'description' => $this->snapshotId !== null ? 'Snapshot #'.$this->snapshotId.' is the compare input.' : 'No usable baseline snapshot input is linked.', ], [ 'label' => 'Environment snapshot', 'state' => $environmentSnapshotState, 'tone' => $this->flowTone($environmentSnapshotState), 'description' => $this->environmentSnapshotDescription($environmentSnapshotState), ], [ 'label' => 'OperationRun proof', 'state' => $operationProofState, 'tone' => $this->flowTone($operationProofState), 'description' => $operationProofState === 'Available' ? 'A compare OperationRun proof link is available.' : 'No compare OperationRun proof is available yet.', ], [ 'label' => 'Drift findings', 'state' => $driftFindingsState, 'tone' => $driftFindingsState === 'Available' && ((int) ($this->findingsCount ?? 0)) > 0 ? 'warning' : $this->flowTone($driftFindingsState), 'description' => $this->driftFindingsDescription(), ], [ 'label' => 'Evidence path', 'state' => $evidencePathState, 'tone' => $this->flowTone($evidencePathState), 'description' => $this->evidenceInputDescription($hasCoverageWarnings, $hasEvidenceGaps), ], ]; } $environmentSnapshotState = $this->hasEnvironmentSnapshot() ? 'Available' : 'Unavailable'; $operationProofState = $this->operationRunId !== null && $this->getRunUrl() !== null ? 'Available' : 'Unavailable'; return [ [ 'label' => 'Environment snapshot', 'state' => $environmentSnapshotState, 'tone' => $environmentSnapshotState === 'Available' ? 'success' : 'gray', 'description' => $environmentSnapshotState === 'Available' ? 'Current environment evidence is present.' : 'No repo-backed environment snapshot is available yet.', ], [ 'label' => 'Operation proof', 'state' => $operationProofState, 'tone' => $operationProofState === 'Available' ? 'success' : 'gray', 'description' => $operationProofState === 'Available' ? 'A compare operation proof link is available.' : 'No compare operation proof is available yet.', ], [ 'label' => 'Baseline snapshot', 'state' => 'Unavailable', 'tone' => 'gray', 'description' => 'Unavailable because no baseline assigned.', ], ]; } /** * @return list */ private function assignmentUnlocks(): array { if ($this->state !== 'no_assignment') { return []; } return [ 'Actionable drift categories', 'Evidence-backed compare', 'Governance decision path', ]; } private function hasEnvironmentSnapshot(): bool { $environment = $this->currentEnvironment(); if (! $environment instanceof ManagedEnvironment) { return false; } $environmentId = (int) $environment->getKey(); if (InventoryItem::query()->where('managed_environment_id', $environmentId)->exists()) { return true; } return OperationRun::query() ->where('managed_environment_id', $environmentId) ->whereIn('type', OperationCatalog::rawValuesForCanonical(OperationRunType::InventorySync->value)) ->where('status', OperationRunStatus::Completed->value) ->exists(); } private function compareTrustLabel(bool $hasWarnings): string { if ($this->state === 'ready' && ! $hasWarnings) { return 'Usable latest compare'; } if ($this->state === 'ready') { return 'Usable with warnings'; } return match ($this->state) { 'idle' => 'Not generated yet', 'comparing' => 'Generating', 'failed' => 'Failed', 'no_assignment' => 'Unavailable', 'no_snapshot' => 'Unavailable', 'invalid_scope' => 'Needs review', default => 'Unavailable', }; } private function coverageDescription(bool $hasCoverageWarnings, bool $hasEvidenceGaps): string { if ($this->state === 'no_assignment') { return 'Compare trust is unavailable until a baseline assignment exists.'; } if ($hasCoverageWarnings && $hasEvidenceGaps) { return 'Coverage warnings and evidence gaps limit the latest compare.'; } if ($hasCoverageWarnings) { return 'Coverage warnings limit the latest compare.'; } if ($hasEvidenceGaps) { return 'Evidence gaps still need review before governance use.'; } return 'No coverage warning is currently reported for the latest compare.'; } private function driftImpactLabel(): string { $findingsCount = (int) ($this->findingsCount ?? 0); if ($findingsCount > 0) { return $findingsCount.' '.\Illuminate\Support\Str::plural('finding', $findingsCount).' need review'; } if ($this->state === 'ready') { return 'No confirmed drift'; } return 'No usable drift result'; } private function evidencePathSummary(bool $hasCoverageWarnings, bool $hasEvidenceGaps): string { if ($hasEvidenceGaps) { return 'Evidence unavailable - Evidence gaps need review'; } if ($hasCoverageWarnings) { return 'Evidence unavailable - Coverage warning recorded'; } if ($this->operationRunId !== null) { return 'Evidence unavailable - Operation proof available'; } return 'Evidence unavailable'; } private function baselineProfileUrl(): ?string { if (! BaselineProfileResource::canViewAny()) { return null; } if ($this->profileId !== null) { return BaselineProfileResource::getUrl('view', ['record' => $this->profileId], panel: 'admin'); } return BaselineProfileResource::getUrl('index', panel: 'admin'); } private function canRunCompareAction(): bool { if (! in_array($this->state, ['idle', 'ready', 'failed'], true)) { return false; } $user = auth()->user(); $tenant = $this->currentEnvironment(); if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment) { return false; } return app(CapabilityResolver::class)->can($user, $tenant, Capabilities::TENANT_SYNC); } private function environmentSnapshotState(bool $hasCoverageWarnings): string { if ($hasCoverageWarnings) { return 'Needs review'; } return $this->hasEnvironmentSnapshot() ? 'Available' : 'Unavailable'; } private function environmentSnapshotDescription(string $environmentSnapshotState): string { return match ($environmentSnapshotState) { 'Available' => 'Environment snapshot evidence is present.', 'Needs review' => 'Coverage proof has warnings.', default => 'No environment snapshot yet.', }; } private function flowTone(string $state): string { return match ($state) { 'Available', 'Complete' => 'success', 'Missing', 'Required', 'Needs review' => 'warning', 'In progress' => 'info', 'Failed' => 'danger', default => 'gray', }; } private function driftFindingsDescription(): string { if ($this->state !== 'ready') { return 'Run compare after required inputs exist.'; } $findingsCount = (int) ($this->findingsCount ?? 0); if ($findingsCount > 0) { return $findingsCount.' '.\Illuminate\Support\Str::plural('open drift finding', $findingsCount).' available for review.'; } return 'Zero open drift findings are recorded for the latest compare result.'; } private function evidenceInputState(bool $hasWarnings): string { if ($this->operationRunId === null) { return 'Unavailable'; } return $hasWarnings ? 'Needs review' : 'Unavailable'; } private function evidenceInputDescription(bool $hasCoverageWarnings, bool $hasEvidenceGaps): string { if ($hasEvidenceGaps) { return 'Compare result exists, but evidence output is not available. Evidence gaps need review.'; } if ($hasCoverageWarnings) { return 'Compare result exists, but evidence output is not available. Coverage warnings need review.'; } if ($this->operationRunId !== null) { return 'Compare result exists, but no evidence output is linked yet.'; } return 'No evidence output is linked yet.'; } /** * @return array */ private function diagnosticsDisclosure(bool $hasEvidenceGapDiagnostics): array { return [ 'label' => 'Diagnostics - Collapsed', 'summary' => $hasEvidenceGapDiagnostics ? 'Support diagnostics exist for this compare and stay closed behind the disclosure until needed.' : 'No support diagnostics are available for this compare state.', ]; } /** * Resolve the Tailwind colour class for the Total Findings stat. * * - Red (danger) only when high-severity findings exist * - Orange (warning) for medium/low-only findings or when warnings present * - Green (success) when fully clear */ private function resolveFindingsColorClass(bool $hasWarnings): string { $count = (int) ($this->findingsCount ?? 0); if ($count === 0) { return $hasWarnings ? 'text-warning-600 dark:text-warning-400' : 'text-success-600 dark:text-success-400'; } $hasHigh = ($this->severityCounts['high'] ?? 0) > 0; return $hasHigh ? 'text-danger-600 dark:text-danger-400' : 'text-warning-600 dark:text-warning-400'; } public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly) ->satisfy(ActionSurfaceSlot::ListHeader, 'Header action: Compare Now (confirmation modal, capability-gated).') ->exempt(ActionSurfaceSlot::InspectAffordance, 'This is a tenant-scoped landing page, not a record inspect surface.') ->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'This page does not render table rows with secondary actions.') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'This page has no bulk actions.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'Page renders explicit empty states for missing tenant, missing assignment, and missing snapshot, with guidance messaging.') ->exempt(ActionSurfaceSlot::DetailHeader, 'This page does not have a record detail header; it uses a page header action instead.'); } /** * @return array */ protected function getHeaderActions(): array { $actions = []; $navigationContext = $this->navigationContext(); if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) { $actions[] = Action::make('backToOrigin') ->label($navigationContext->backLinkLabel) ->color('gray') ->url($navigationContext->backLinkUrl); } $actions[] = $this->compareNowAction(); return $actions; } private function compareNowAction(): Action { $isFullContent = false; if (is_int($this->profileId) && $this->profileId > 0) { $profile = \App\Models\BaselineProfile::query()->find($this->profileId); $mode = $profile?->capture_mode instanceof BaselineCaptureMode ? $profile->capture_mode : (is_string($profile?->capture_mode) ? BaselineCaptureMode::tryFrom($profile->capture_mode) : null); $isFullContent = $mode === BaselineCaptureMode::FullContent; } $label = $isFullContent ? 'Compare now (full content)' : 'Compare now'; $modalDescription = $isFullContent ? 'This will refresh content evidence on demand (redacted) before comparing the current tenant inventory against the assigned baseline snapshot.' : 'This will compare the current tenant inventory against the assigned baseline snapshot and generate drift findings.'; $action = Action::make('compareNow') ->label($label) ->icon('heroicon-o-play') ->requiresConfirmation() ->modalHeading($label) ->modalDescription($modalDescription) ->disabled(fn (): bool => ! in_array($this->state, ['idle', 'ready', 'failed'], true)) ->action(function (): void { $user = auth()->user(); if (! $user instanceof User) { Notification::make()->title('Not authenticated')->danger()->send(); return; } $tenant = $this->currentEnvironment(); if (! $tenant instanceof ManagedEnvironment) { Notification::make()->title('Open an environment to compare baselines')->danger()->send(); return; } $service = app(BaselineCompareService::class); $result = $service->startCompare($tenant, $user); if (! ($result['ok'] ?? false)) { $reasonCode = is_string($result['reason_code'] ?? null) ? (string) $result['reason_code'] : 'unknown'; $translation = is_array($result['reason_translation'] ?? null) ? $result['reason_translation'] : []; $message = is_string($translation['short_explanation'] ?? null) && trim((string) $translation['short_explanation']) !== '' ? trim((string) $translation['short_explanation']) : match ($reasonCode) { \App\Support\Baselines\BaselineReasonCodes::COMPARE_ROLLOUT_DISABLED => 'Full-content baseline compare is currently disabled for controlled rollout.', \App\Support\Baselines\BaselineReasonCodes::COMPARE_PROFILE_NOT_ACTIVE => 'The assigned baseline profile is not active.', \App\Support\Baselines\BaselineReasonCodes::COMPARE_NO_ACTIVE_SNAPSHOT, \App\Support\Baselines\BaselineReasonCodes::COMPARE_NO_CONSUMABLE_SNAPSHOT => 'No complete baseline snapshot is currently available for compare.', \App\Support\Baselines\BaselineReasonCodes::COMPARE_SNAPSHOT_BUILDING => 'The latest baseline capture is still building. Compare will be available after it completes.', \App\Support\Baselines\BaselineReasonCodes::COMPARE_SNAPSHOT_INCOMPLETE => 'The latest baseline capture is incomplete. Capture a new baseline before comparing.', \App\Support\Baselines\BaselineReasonCodes::COMPARE_SNAPSHOT_SUPERSEDED => 'A newer complete baseline snapshot is current. Compare uses the latest complete baseline only.', \App\Support\Baselines\BaselineReasonCodes::COMPARE_INVALID_SCOPE => 'The assigned baseline scope is invalid and must be reviewed before compare can start.', \App\Support\Baselines\BaselineReasonCodes::COMPARE_UNSUPPORTED_SCOPE => 'The selected governed subjects are not supported by any compare strategy.', \App\Support\Baselines\BaselineReasonCodes::COMPARE_MIXED_SCOPE => 'The selected governed subjects span multiple compare strategy families and must be narrowed before compare can start.', default => 'Reason: '.$reasonCode, }; Notification::make() ->title('Cannot start comparison') ->body($message) ->danger() ->send(); return; } $run = $result['run'] ?? null; if ($run instanceof OperationRun) { $this->operationRunId = (int) $run->getKey(); } $this->state = 'comparing'; OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::queuedToast($run instanceof OperationRun ? (string) $run->type : OperationRunType::BaselineCompare->value) ->actions($run instanceof OperationRun ? [ Action::make('view_run') ->label('Open operation') ->url(OperationRunLinks::view($run, $tenant, $this->navigationContext())), ] : []) ->send(); }); return UiEnforcement::forAction($action) ->requireCapability(Capabilities::TENANT_SYNC) ->preserveDisabled() ->apply(); } public function getFindingsUrl(): ?string { $tenant = $this->currentEnvironment(); if (! $tenant instanceof ManagedEnvironment) { return null; } return FindingResource::getUrl('index', tenant: $tenant); } public function getRunUrl(): ?string { if ($this->operationRunId === null) { return null; } $tenant = $this->currentEnvironment(); if (! $tenant instanceof ManagedEnvironment) { return null; } return OperationRunLinks::view($this->operationRunId, $tenant); } public function openCompareMatrixUrl(): ?string { $profile = $this->resolveCompareMatrixProfile(); if (! $profile instanceof BaselineProfile) { return null; } $url = BaselineProfileResource::compareMatrixUrl($profile); $query = array_filter([ 'subject_key' => $this->matrixSubjectKey, ], static fn (mixed $value): bool => $value !== null && $value !== ''); if ($query === []) { return $url; } return $url.(str_contains($url, '?') ? '&' : '?').http_build_query($query); } private function governanceAggregate(ManagedEnvironment $tenant, BaselineCompareStats $stats): TenantGovernanceAggregate { /** @var TenantGovernanceAggregateResolver $resolver */ $resolver = app(TenantGovernanceAggregateResolver::class); /** @var TenantGovernanceAggregate $aggregate */ $aggregate = $resolver->fromStats($tenant, $stats); return $aggregate; } private function navigationContext(): ?CanonicalNavigationContext { if (! is_array($this->navigationContextPayload)) { return CanonicalNavigationContext::fromRequest(request()); } return CanonicalNavigationContext::fromPayload($this->navigationContextPayload); } private function resolveCompareMatrixProfile(): ?BaselineProfile { $tenant = $this->currentEnvironment(); if (! $tenant instanceof ManagedEnvironment) { return null; } $candidateIds = array_values(array_filter([ $this->matrixBaselineProfileId, $this->profileId, ], static fn (mixed $value): bool => is_int($value) && $value > 0)); foreach ($candidateIds as $profileId) { $profile = BaselineProfile::query() ->whereKey($profileId) ->where('workspace_id', (int) $tenant->workspace_id) ->first(); if ($profile instanceof BaselineProfile) { return $profile; } } return null; } private function currentEnvironment(): ?ManagedEnvironment { if ($this->scopedEnvironmentId !== null) { $tenant = ManagedEnvironment::query()->whereKey($this->scopedEnvironmentId)->first(); return $tenant instanceof ManagedEnvironment && static::hasEnvironmentAccess($tenant) ? $tenant : null; } $tenant = static::resolveRouteOwnedEnvironment(); return $tenant instanceof ManagedEnvironment && static::hasEnvironmentAccess($tenant) ? $tenant : null; } protected static function resolveRouteOwnedEnvironment(ManagedEnvironment|string|null $environment = null): ?ManagedEnvironment { if ($environment instanceof ManagedEnvironment) { return $environment; } if (is_string($environment) && $environment !== '') { return ManagedEnvironment::query() ->where('slug', $environment) ->first(); } $routeEnvironment = request()->route('environment'); if ($routeEnvironment instanceof ManagedEnvironment) { return $routeEnvironment; } if (is_string($routeEnvironment) && $routeEnvironment !== '') { return ManagedEnvironment::query() ->where('slug', $routeEnvironment) ->first(); } return static::resolveRefererOwnedEnvironment(); } private static function hasEnvironmentAccess(?ManagedEnvironment $environment): bool { $user = auth()->user(); if (! $environment instanceof ManagedEnvironment || ! $user instanceof User) { return false; } $routeWorkspace = request()->route('workspace'); if ($routeWorkspace instanceof Workspace && (int) $routeWorkspace->getKey() !== (int) $environment->workspace_id) { return false; } if (is_string($routeWorkspace) && $routeWorkspace !== '') { $workspace = $environment->workspace instanceof Workspace ? $environment->workspace : $environment->workspace()->first(); if (! $workspace instanceof Workspace) { return false; } if ($routeWorkspace !== static::workspaceRouteKey($workspace) && $routeWorkspace !== (string) $workspace->getKey()) { return false; } } $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); if ($workspaceId !== null && (int) $workspaceId !== (int) $environment->workspace_id) { return false; } /** @var CapabilityResolver $resolver */ $resolver = app(CapabilityResolver::class); return $resolver->isMember($user, $environment) && $resolver->can($user, $environment, Capabilities::TENANT_VIEW); } /** * @param array $parameters */ private static function resolveAdminUrlEnvironment(array $parameters, ?Model $tenant = null): ?ManagedEnvironment { $parameterEnvironment = $parameters['tenant'] ?? $parameters['environment'] ?? null; if ($parameterEnvironment instanceof ManagedEnvironment) { return $parameterEnvironment; } if ($tenant instanceof ManagedEnvironment) { return $tenant; } $routeEnvironment = static::resolveRouteOwnedEnvironment(); if ($routeEnvironment instanceof ManagedEnvironment) { return $routeEnvironment; } $filamentTenant = Filament::getTenant(); return $filamentTenant instanceof ManagedEnvironment ? $filamentTenant : null; } /** * @param array $parameters */ private static function resolveAdminUrlWorkspace(ManagedEnvironment $environment, array $parameters): Workspace|string|int|null { $workspace = $parameters['workspace'] ?? null; if ($workspace instanceof Workspace || is_string($workspace) || is_int($workspace)) { return $workspace; } $environmentWorkspace = $environment->workspace; if ($environmentWorkspace instanceof Workspace) { return $environmentWorkspace; } return $environment->workspace()->first(); } /** * @param array $parameters * @return array */ private static function withoutLegacyScopeQuery(array $parameters): array { foreach (self::LEGACY_SCOPE_QUERY_KEYS as $key) { unset($parameters[$key]); } return $parameters; } private static function workspaceRouteKey(Workspace $workspace): string { $slug = $workspace->getAttribute('slug'); return is_string($slug) && $slug !== '' ? $slug : (string) $workspace->getKey(); } private static function resolveRefererOwnedEnvironment(): ?ManagedEnvironment { $referer = request()->headers->get('referer'); if (! is_string($referer) || $referer === '') { return null; } $path = parse_url($referer, PHP_URL_PATH); if (! is_string($path)) { return null; } if (preg_match('#^/admin/workspaces/([^/]+)/environments/([^/]+)/baseline-compare$#', $path, $matches) !== 1) { return null; } $workspaceRouteKey = rawurldecode($matches[1]); $environmentRouteKey = rawurldecode($matches[2]); $environment = ManagedEnvironment::query() ->where('slug', $environmentRouteKey) ->first(); if (! $environment instanceof ManagedEnvironment) { return null; } $workspace = $environment->workspace; if (! $workspace instanceof Workspace) { $workspace = $environment->workspace()->first(); } if (! $workspace instanceof Workspace) { return null; } if ($workspaceRouteKey !== static::workspaceRouteKey($workspace) && $workspaceRouteKey !== (string) $workspace->getKey()) { return null; } return $environment; } }