## Summary - add a shared baseline compare summary assessment and assessor for compact trust propagation - harden dashboard, landing, and banner baseline compare surfaces against false all-clear claims - add focused Pest coverage for dashboard, landing, banner, reason translation, and canonical detail parity ## Validation - vendor/bin/sail bin pint --dirty --format agent - vendor/bin/sail artisan test --compact tests/Feature/Baselines/BaselineCompareSummaryAssessmentTest.php tests/Feature/Baselines/BaselineCompareExplanationFallbackTest.php tests/Feature/Filament/BaselineCompareNowWidgetTest.php tests/Feature/Filament/NeedsAttentionWidgetTest.php tests/Feature/Filament/BaselineCompareExplanationSurfaceTest.php tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php tests/Feature/Filament/BaselineCompareCoverageBannerTest.php tests/Feature/Filament/BaselineCompareSummaryConsistencyTest.php tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php tests/Feature/ReasonTranslation/ReasonTranslationExplanationTest.php ## Notes - Livewire compliance: Filament v5 / Livewire v4 stack unchanged - Provider registration: unchanged, Laravel 12 providers remain in bootstrap/providers.php - Global search: no searchable resource behavior changed - Destructive actions: none introduced by this change - Assets: no new assets registered; existing deploy process remains unchanged Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #196
286 lines
13 KiB
PHP
286 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Baselines;
|
|
|
|
use App\Support\Ui\OperatorExplanation\OperatorExplanationPattern;
|
|
use App\Support\Ui\OperatorExplanation\TrustworthinessLevel;
|
|
use Carbon\CarbonImmutable;
|
|
|
|
final class BaselineCompareSummaryAssessor
|
|
{
|
|
private const int STALE_AFTER_DAYS = 7;
|
|
|
|
public function assess(BaselineCompareStats $stats): BaselineCompareSummaryAssessment
|
|
{
|
|
$explanation = $stats->operatorExplanation();
|
|
$findingsVisibleCount = (int) ($stats->findingsCount ?? 0);
|
|
$highSeverityCount = (int) ($stats->severityCounts['high'] ?? 0);
|
|
$reasonCode = is_string($stats->reasonCode) ? BaselineCompareReasonCode::tryFrom($stats->reasonCode) : null;
|
|
$evaluationResult = $stats->state === 'failed'
|
|
? 'failed_result'
|
|
: $explanation->evaluationResult;
|
|
$positiveClaimAllowed = $this->positiveClaimAllowed($stats, $explanation, $reasonCode, $evaluationResult);
|
|
$isStale = $this->hasStaleResult($stats, $evaluationResult);
|
|
$stateFamily = $this->stateFamily($stats, $findingsVisibleCount, $positiveClaimAllowed, $isStale);
|
|
|
|
return new BaselineCompareSummaryAssessment(
|
|
stateFamily: $stateFamily,
|
|
headline: $this->headline($stats, $stateFamily, $findingsVisibleCount, $highSeverityCount, $evaluationResult),
|
|
supportingMessage: $this->supportingMessage($stats, $stateFamily, $findingsVisibleCount, $evaluationResult),
|
|
tone: $this->tone($stats, $stateFamily),
|
|
positiveClaimAllowed: $positiveClaimAllowed,
|
|
trustworthinessLevel: $explanation->trustworthinessLevel->value,
|
|
evaluationResult: $evaluationResult,
|
|
evidenceImpact: $this->evidenceImpact($stats, $evaluationResult, $isStale),
|
|
findingsVisibleCount: $findingsVisibleCount,
|
|
highSeverityCount: $highSeverityCount,
|
|
nextAction: $this->nextAction($stats, $stateFamily, $findingsVisibleCount, $evaluationResult),
|
|
lastComparedLabel: $stats->lastComparedHuman,
|
|
reasonCode: $stats->reasonCode,
|
|
);
|
|
}
|
|
|
|
private function positiveClaimAllowed(
|
|
BaselineCompareStats $stats,
|
|
OperatorExplanationPattern $explanation,
|
|
?BaselineCompareReasonCode $reasonCode,
|
|
string $evaluationResult,
|
|
): bool {
|
|
if ($stats->state !== 'ready') {
|
|
return false;
|
|
}
|
|
|
|
if ((int) ($stats->findingsCount ?? 0) > 0) {
|
|
return false;
|
|
}
|
|
|
|
if ($evaluationResult !== 'no_result') {
|
|
return false;
|
|
}
|
|
|
|
if ($explanation->trustworthinessLevel !== TrustworthinessLevel::Trustworthy) {
|
|
return false;
|
|
}
|
|
|
|
if (in_array($stats->coverageStatus, ['warning', 'unproven'], true)) {
|
|
return false;
|
|
}
|
|
|
|
if ((int) ($stats->evidenceGapsCount ?? 0) > 0) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->hasStaleResult($stats, $evaluationResult)) {
|
|
return false;
|
|
}
|
|
|
|
if ($stats->reasonCode === null) {
|
|
return true;
|
|
}
|
|
|
|
return $reasonCode?->supportsPositiveClaim() ?? false;
|
|
}
|
|
|
|
private function stateFamily(
|
|
BaselineCompareStats $stats,
|
|
int $findingsVisibleCount,
|
|
bool $positiveClaimAllowed,
|
|
bool $isStale,
|
|
): string {
|
|
return match (true) {
|
|
$stats->state === 'comparing' => BaselineCompareSummaryAssessment::STATE_IN_PROGRESS,
|
|
$stats->state === 'failed',
|
|
$findingsVisibleCount > 0 => BaselineCompareSummaryAssessment::STATE_ACTION_REQUIRED,
|
|
in_array($stats->state, ['no_tenant', 'no_assignment', 'no_snapshot', 'idle'], true) => BaselineCompareSummaryAssessment::STATE_UNAVAILABLE,
|
|
$isStale => BaselineCompareSummaryAssessment::STATE_STALE,
|
|
$positiveClaimAllowed => BaselineCompareSummaryAssessment::STATE_POSITIVE,
|
|
default => BaselineCompareSummaryAssessment::STATE_CAUTION,
|
|
};
|
|
}
|
|
|
|
private function evidenceImpact(BaselineCompareStats $stats, string $evaluationResult, bool $isStale): string
|
|
{
|
|
if (in_array($stats->state, ['no_tenant', 'no_assignment', 'no_snapshot', 'idle', 'failed'], true)) {
|
|
return BaselineCompareSummaryAssessment::EVIDENCE_UNAVAILABLE;
|
|
}
|
|
|
|
if ($isStale) {
|
|
return BaselineCompareSummaryAssessment::EVIDENCE_STALE_RESULT;
|
|
}
|
|
|
|
if ($evaluationResult === 'suppressed_result') {
|
|
return BaselineCompareSummaryAssessment::EVIDENCE_SUPPRESSED_OUTPUT;
|
|
}
|
|
|
|
if ((int) ($stats->evidenceGapsCount ?? 0) > 0) {
|
|
return BaselineCompareSummaryAssessment::EVIDENCE_EVIDENCE_GAP;
|
|
}
|
|
|
|
if (in_array($stats->coverageStatus, ['warning', 'unproven'], true)) {
|
|
return BaselineCompareSummaryAssessment::EVIDENCE_COVERAGE_WARNING;
|
|
}
|
|
|
|
return BaselineCompareSummaryAssessment::EVIDENCE_NONE;
|
|
}
|
|
|
|
private function headline(
|
|
BaselineCompareStats $stats,
|
|
string $stateFamily,
|
|
int $findingsVisibleCount,
|
|
int $highSeverityCount,
|
|
string $evaluationResult,
|
|
): string {
|
|
return match ($stateFamily) {
|
|
BaselineCompareSummaryAssessment::STATE_POSITIVE => 'No confirmed drift in the latest baseline compare.',
|
|
BaselineCompareSummaryAssessment::STATE_CAUTION => match (true) {
|
|
$evaluationResult === 'suppressed_result' => 'The last compare finished, but normal result output was suppressed.',
|
|
(int) ($stats->evidenceGapsCount ?? 0) > 0 => 'No confirmed drift is visible, but evidence gaps still limit this result.',
|
|
in_array($stats->coverageStatus, ['warning', 'unproven'], true) => 'No confirmed drift is visible, but coverage limits this compare.',
|
|
default => 'The latest compare result needs caution before you treat it as an all-clear.',
|
|
},
|
|
BaselineCompareSummaryAssessment::STATE_STALE => 'The latest baseline compare result is stale.',
|
|
BaselineCompareSummaryAssessment::STATE_ACTION_REQUIRED => match (true) {
|
|
$stats->state === 'failed' || $evaluationResult === 'failed_result' => 'The latest baseline compare failed before it produced a usable result.',
|
|
$highSeverityCount > 0 => sprintf('%d high-severity drift finding%s need review.', $highSeverityCount, $highSeverityCount === 1 ? '' : 's'),
|
|
default => sprintf('%d open drift finding%s need review.', $findingsVisibleCount, $findingsVisibleCount === 1 ? '' : 's'),
|
|
},
|
|
BaselineCompareSummaryAssessment::STATE_IN_PROGRESS => 'Baseline compare is in progress.',
|
|
default => match ($stats->state) {
|
|
'no_assignment' => 'This tenant does not have an assigned baseline yet.',
|
|
'no_snapshot' => 'The current baseline snapshot is not available for compare.',
|
|
'idle' => 'A current baseline compare result is not available yet.',
|
|
default => 'A usable baseline compare result is not currently available.',
|
|
},
|
|
};
|
|
}
|
|
|
|
private function supportingMessage(
|
|
BaselineCompareStats $stats,
|
|
string $stateFamily,
|
|
int $findingsVisibleCount,
|
|
string $evaluationResult,
|
|
): ?string {
|
|
return match ($stateFamily) {
|
|
BaselineCompareSummaryAssessment::STATE_POSITIVE => $stats->lastComparedHuman !== null
|
|
? 'Last compared '.$stats->lastComparedHuman.'.'
|
|
: 'The latest compare result is trustworthy enough to treat zero findings as current.',
|
|
BaselineCompareSummaryAssessment::STATE_CAUTION => match (true) {
|
|
$evaluationResult === 'suppressed_result' => 'Review the run detail before treating zero visible findings as complete.',
|
|
(int) ($stats->evidenceGapsCount ?? 0) > 0 => 'Review the compare detail to see which evidence gaps still limit trust.',
|
|
in_array($stats->coverageStatus, ['warning', 'unproven'], true) => 'Coverage warnings mean zero visible findings are not an all-clear on their own.',
|
|
default => $stats->reasonMessage ?? $stats->message,
|
|
},
|
|
BaselineCompareSummaryAssessment::STATE_STALE => $stats->lastComparedHuman !== null
|
|
? 'Last compared '.$stats->lastComparedHuman.'. Refresh compare before relying on this posture.'
|
|
: 'Refresh compare before relying on this posture.',
|
|
BaselineCompareSummaryAssessment::STATE_ACTION_REQUIRED => match (true) {
|
|
$stats->state === 'failed' => $stats->failureReason,
|
|
$findingsVisibleCount > 0 => 'Open findings remain on this tenant and need review.',
|
|
default => $stats->message,
|
|
},
|
|
BaselineCompareSummaryAssessment::STATE_IN_PROGRESS => 'Current counts are diagnostic only until the compare run finishes.',
|
|
default => $stats->message,
|
|
};
|
|
}
|
|
|
|
private function tone(BaselineCompareStats $stats, string $stateFamily): string
|
|
{
|
|
return match ($stateFamily) {
|
|
BaselineCompareSummaryAssessment::STATE_POSITIVE => 'success',
|
|
BaselineCompareSummaryAssessment::STATE_ACTION_REQUIRED => 'danger',
|
|
BaselineCompareSummaryAssessment::STATE_IN_PROGRESS => 'info',
|
|
BaselineCompareSummaryAssessment::STATE_UNAVAILABLE => $stats->state === 'no_snapshot' ? 'warning' : 'gray',
|
|
default => 'warning',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @return array{label: string, target: string}
|
|
*/
|
|
private function nextAction(
|
|
BaselineCompareStats $stats,
|
|
string $stateFamily,
|
|
int $findingsVisibleCount,
|
|
string $evaluationResult,
|
|
): array {
|
|
if ($findingsVisibleCount > 0) {
|
|
return [
|
|
'label' => 'Open findings',
|
|
'target' => BaselineCompareSummaryAssessment::NEXT_TARGET_FINDINGS,
|
|
];
|
|
}
|
|
|
|
return match ($stateFamily) {
|
|
BaselineCompareSummaryAssessment::STATE_ACTION_REQUIRED => [
|
|
'label' => $evaluationResult === 'failed_result' ? 'Review the failed run' : 'Review compare detail',
|
|
'target' => $stats->operationRunId !== null
|
|
? BaselineCompareSummaryAssessment::NEXT_TARGET_RUN
|
|
: BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING,
|
|
],
|
|
BaselineCompareSummaryAssessment::STATE_CAUTION => [
|
|
'label' => 'Review compare detail',
|
|
'target' => $stats->operationRunId !== null
|
|
? BaselineCompareSummaryAssessment::NEXT_TARGET_RUN
|
|
: BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING,
|
|
],
|
|
BaselineCompareSummaryAssessment::STATE_STALE => [
|
|
'label' => 'Open Baseline Compare',
|
|
'target' => BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING,
|
|
],
|
|
BaselineCompareSummaryAssessment::STATE_IN_PROGRESS => [
|
|
'label' => $stats->operationRunId !== null ? 'View run' : 'Open Baseline Compare',
|
|
'target' => $stats->operationRunId !== null
|
|
? BaselineCompareSummaryAssessment::NEXT_TARGET_RUN
|
|
: BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING,
|
|
],
|
|
BaselineCompareSummaryAssessment::STATE_UNAVAILABLE => match ($stats->state) {
|
|
'no_assignment' => [
|
|
'label' => 'Assign a baseline first',
|
|
'target' => BaselineCompareSummaryAssessment::NEXT_TARGET_NONE,
|
|
],
|
|
'no_snapshot' => [
|
|
'label' => 'Review baseline prerequisites',
|
|
'target' => BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING,
|
|
],
|
|
'idle' => [
|
|
'label' => 'Open Baseline Compare',
|
|
'target' => BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING,
|
|
],
|
|
default => [
|
|
'label' => 'Review compare availability',
|
|
'target' => BaselineCompareSummaryAssessment::NEXT_TARGET_NONE,
|
|
],
|
|
},
|
|
default => [
|
|
'label' => 'No action needed',
|
|
'target' => BaselineCompareSummaryAssessment::NEXT_TARGET_NONE,
|
|
],
|
|
};
|
|
}
|
|
|
|
private function hasStaleResult(BaselineCompareStats $stats, string $evaluationResult): bool
|
|
{
|
|
if ($stats->state !== 'ready') {
|
|
return false;
|
|
}
|
|
|
|
if ($stats->lastComparedIso === null) {
|
|
return false;
|
|
}
|
|
|
|
if (! in_array($evaluationResult, ['full_result', 'no_result', 'incomplete_result', 'suppressed_result'], true)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$lastComparedAt = CarbonImmutable::parse($stats->lastComparedIso);
|
|
} catch (\Throwable) {
|
|
return false;
|
|
}
|
|
|
|
return $lastComparedAt->lt(now()->subDays(self::STALE_AFTER_DAYS));
|
|
}
|
|
}
|