Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m12s
Replaced legacy tenant and environment bindings in the BaselineDriftEngine with the new ProviderResourceIdentity framework as defined in Spec 382.
963 lines
42 KiB
PHP
963 lines
42 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Baselines;
|
|
|
|
use App\Models\BaselineProfile;
|
|
use App\Models\BaselineSnapshot;
|
|
use App\Models\BaselineTenantAssignment;
|
|
use App\Models\Finding;
|
|
use App\Models\InventoryItem;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Services\Baselines\BaselineSnapshotTruthResolver;
|
|
use App\Support\OperationCatalog;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\ReasonTranslation\ReasonPresenter;
|
|
use App\Support\Ui\OperatorExplanation\CountDescriptor;
|
|
use App\Support\Ui\OperatorExplanation\OperatorExplanationPattern;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use InvalidArgumentException;
|
|
|
|
final class BaselineCompareStats
|
|
{
|
|
/**
|
|
* @param array<string, int> $severityCounts
|
|
* @param list<string> $uncoveredTypes
|
|
* @param array<string, int> $evidenceGapsTopReasons
|
|
* @param array{
|
|
* summary: array{
|
|
* count: int,
|
|
* by_reason: array<string, int>,
|
|
* detail_state: string,
|
|
* recorded_subjects_total: int,
|
|
* missing_detail_count: int
|
|
* },
|
|
* buckets: list<array{
|
|
* reason_code: string,
|
|
* reason_label: string,
|
|
* count: int,
|
|
* recorded_count: int,
|
|
* missing_detail_count: int,
|
|
* detail_state: string,
|
|
* search_text: string,
|
|
* rows: list<array{
|
|
* reason_code: string,
|
|
* reason_label: string,
|
|
* policy_type: string,
|
|
* subject_key: string,
|
|
* search_text: string
|
|
* }>
|
|
* }>
|
|
* } $evidenceGapDetails
|
|
* @param array<string, mixed> $baselineCompareDiagnostics
|
|
*/
|
|
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 $duplicateNameSubjectsCount,
|
|
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 readonly array $evidenceGapDetails = [],
|
|
public readonly array $baselineCompareDiagnostics = [],
|
|
public readonly ?int $evidenceGapStructuralCount = null,
|
|
public readonly ?int $evidenceGapOperationalCount = null,
|
|
public readonly ?int $evidenceGapTransientCount = null,
|
|
public readonly ?bool $evidenceGapLegacyMode = null,
|
|
public readonly int $overdueOpenFindingsCount = 0,
|
|
public readonly int $expiringGovernanceCount = 0,
|
|
public readonly int $lapsedGovernanceCount = 0,
|
|
public readonly int $activeNonNewFindingsCount = 0,
|
|
public readonly int $highSeverityActiveFindingsCount = 0,
|
|
) {}
|
|
|
|
public static function forTenant(?ManagedEnvironment $tenant): self
|
|
{
|
|
if (! $tenant instanceof ManagedEnvironment) {
|
|
return self::empty('no_tenant', 'No environment selected.');
|
|
}
|
|
|
|
$findingAttentionCounts = self::findingAttentionCounts($tenant);
|
|
|
|
$assignment = BaselineTenantAssignment::query()
|
|
->with('baselineProfile')
|
|
->where('managed_environment_id', $tenant->getKey())
|
|
->first();
|
|
|
|
if (! $assignment instanceof BaselineTenantAssignment) {
|
|
return self::empty(
|
|
'no_assignment',
|
|
'This environment has no baseline assignment. A workspace manager can assign a baseline profile to this environment.',
|
|
findingAttentionCounts: $findingAttentionCounts,
|
|
);
|
|
}
|
|
|
|
$profile = $assignment->baselineProfile;
|
|
|
|
if (! $profile instanceof BaselineProfile) {
|
|
return self::empty(
|
|
'no_assignment',
|
|
'The assigned baseline profile no longer exists.',
|
|
findingAttentionCounts: $findingAttentionCounts,
|
|
);
|
|
}
|
|
|
|
$profileName = (string) $profile->name;
|
|
$profileId = (int) $profile->getKey();
|
|
$truthResolution = app(BaselineSnapshotTruthResolver::class)->resolveCompareSnapshot($profile);
|
|
$effectiveSnapshot = $truthResolution['effective_snapshot'] ?? null;
|
|
$snapshotId = $effectiveSnapshot instanceof BaselineSnapshot ? (int) $effectiveSnapshot->getKey() : null;
|
|
$snapshotReasonCode = is_string($truthResolution['reason_code'] ?? null) ? (string) $truthResolution['reason_code'] : null;
|
|
$latestCaptureRun = self::latestBaselineCaptureRun($profile);
|
|
$snapshotReasonMessage = self::missingSnapshotMessage($snapshotReasonCode, $latestCaptureRun);
|
|
|
|
try {
|
|
$profileScope = $profile->normalizedScope();
|
|
$overrideScope = $assignment->override_scope_jsonb !== null
|
|
? BaselineScope::fromJsonb(
|
|
is_array($assignment->override_scope_jsonb) ? $assignment->override_scope_jsonb : null,
|
|
allowEmptyLegacyAsNoOverride: true,
|
|
)
|
|
: null;
|
|
$effectiveScope = BaselineScope::effective($profileScope, $overrideScope);
|
|
} catch (InvalidArgumentException) {
|
|
return self::empty(
|
|
'invalid_scope',
|
|
'The assigned baseline scope is invalid or no longer supported. A workspace manager must review the baseline definition.',
|
|
findingAttentionCounts: $findingAttentionCounts,
|
|
);
|
|
}
|
|
|
|
$duplicateNameStats = self::duplicateNameStats($tenant, $effectiveScope);
|
|
$duplicateNamePoliciesCount = $duplicateNameStats['policy_count'];
|
|
$duplicateNameSubjectsCount = $duplicateNameStats['subject_count'];
|
|
|
|
if ($snapshotId === null) {
|
|
return new self(
|
|
state: 'no_snapshot',
|
|
message: $snapshotReasonMessage ?? 'The baseline profile has no complete snapshot yet. A workspace manager needs to capture a baseline first.',
|
|
profileName: $profileName,
|
|
profileId: $profileId,
|
|
snapshotId: null,
|
|
duplicateNamePoliciesCount: $duplicateNamePoliciesCount,
|
|
duplicateNameSubjectsCount: $duplicateNameSubjectsCount,
|
|
operationRunId: null,
|
|
findingsCount: null,
|
|
severityCounts: [],
|
|
lastComparedHuman: null,
|
|
lastComparedIso: null,
|
|
failureReason: null,
|
|
reasonCode: $snapshotReasonCode,
|
|
reasonMessage: $snapshotReasonMessage,
|
|
overdueOpenFindingsCount: $findingAttentionCounts['overdue_open_findings_count'],
|
|
expiringGovernanceCount: $findingAttentionCounts['expiring_governance_count'],
|
|
lapsedGovernanceCount: $findingAttentionCounts['lapsed_governance_count'],
|
|
activeNonNewFindingsCount: $findingAttentionCounts['active_non_new_findings_count'],
|
|
highSeverityActiveFindingsCount: $findingAttentionCounts['high_severity_active_findings_count'],
|
|
);
|
|
}
|
|
|
|
$latestRun = OperationRun::query()
|
|
->where('managed_environment_id', $tenant->getKey())
|
|
->whereIn('type', OperationCatalog::rawValuesForCanonical(OperationRunType::BaselineCompare->value))
|
|
->latest('id')
|
|
->first();
|
|
|
|
[$coverageStatus, $uncoveredTypes, $fidelity] = self::coverageInfoForRun($latestRun);
|
|
[$evidenceGapsCount, $evidenceGapsTopReasons] = self::evidenceGapSummaryForRun($latestRun);
|
|
[$reasonCode, $reasonMessage] = self::reasonInfoForRun($latestRun);
|
|
$rbacRoleDefinitionSummary = self::rbacRoleDefinitionSummaryForRun($latestRun);
|
|
$evidenceGapDetails = self::evidenceGapDetailsForRun($latestRun);
|
|
$baselineCompareDiagnostics = self::baselineCompareDiagnosticsForRun($latestRun);
|
|
$evidenceGapSummary = is_array($evidenceGapDetails['summary'] ?? null) ? $evidenceGapDetails['summary'] : [];
|
|
$evidenceGapStructuralCount = is_numeric($evidenceGapSummary['structural_count'] ?? null)
|
|
? (int) $evidenceGapSummary['structural_count']
|
|
: null;
|
|
$evidenceGapOperationalCount = is_numeric($evidenceGapSummary['operational_count'] ?? null)
|
|
? (int) $evidenceGapSummary['operational_count']
|
|
: null;
|
|
$evidenceGapTransientCount = is_numeric($evidenceGapSummary['transient_count'] ?? null)
|
|
? (int) $evidenceGapSummary['transient_count']
|
|
: null;
|
|
$evidenceGapLegacyMode = is_bool($evidenceGapSummary['legacy_mode'] ?? null)
|
|
? (bool) $evidenceGapSummary['legacy_mode']
|
|
: null;
|
|
|
|
// 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,
|
|
duplicateNameSubjectsCount: $duplicateNameSubjectsCount,
|
|
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,
|
|
evidenceGapDetails: $evidenceGapDetails,
|
|
baselineCompareDiagnostics: $baselineCompareDiagnostics,
|
|
evidenceGapStructuralCount: $evidenceGapStructuralCount,
|
|
evidenceGapOperationalCount: $evidenceGapOperationalCount,
|
|
evidenceGapTransientCount: $evidenceGapTransientCount,
|
|
evidenceGapLegacyMode: $evidenceGapLegacyMode,
|
|
overdueOpenFindingsCount: $findingAttentionCounts['overdue_open_findings_count'],
|
|
expiringGovernanceCount: $findingAttentionCounts['expiring_governance_count'],
|
|
lapsedGovernanceCount: $findingAttentionCounts['lapsed_governance_count'],
|
|
activeNonNewFindingsCount: $findingAttentionCounts['active_non_new_findings_count'],
|
|
highSeverityActiveFindingsCount: $findingAttentionCounts['high_severity_active_findings_count'],
|
|
);
|
|
}
|
|
|
|
// 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,
|
|
duplicateNameSubjectsCount: $duplicateNameSubjectsCount,
|
|
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,
|
|
evidenceGapDetails: $evidenceGapDetails,
|
|
baselineCompareDiagnostics: $baselineCompareDiagnostics,
|
|
evidenceGapStructuralCount: $evidenceGapStructuralCount,
|
|
evidenceGapOperationalCount: $evidenceGapOperationalCount,
|
|
evidenceGapTransientCount: $evidenceGapTransientCount,
|
|
evidenceGapLegacyMode: $evidenceGapLegacyMode,
|
|
overdueOpenFindingsCount: $findingAttentionCounts['overdue_open_findings_count'],
|
|
expiringGovernanceCount: $findingAttentionCounts['expiring_governance_count'],
|
|
lapsedGovernanceCount: $findingAttentionCounts['lapsed_governance_count'],
|
|
activeNonNewFindingsCount: $findingAttentionCounts['active_non_new_findings_count'],
|
|
highSeverityActiveFindingsCount: $findingAttentionCounts['high_severity_active_findings_count'],
|
|
);
|
|
}
|
|
|
|
$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('managed_environment_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,
|
|
duplicateNameSubjectsCount: $duplicateNameSubjectsCount,
|
|
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,
|
|
evidenceGapDetails: $evidenceGapDetails,
|
|
baselineCompareDiagnostics: $baselineCompareDiagnostics,
|
|
evidenceGapStructuralCount: $evidenceGapStructuralCount,
|
|
evidenceGapOperationalCount: $evidenceGapOperationalCount,
|
|
evidenceGapTransientCount: $evidenceGapTransientCount,
|
|
evidenceGapLegacyMode: $evidenceGapLegacyMode,
|
|
overdueOpenFindingsCount: $findingAttentionCounts['overdue_open_findings_count'],
|
|
expiringGovernanceCount: $findingAttentionCounts['expiring_governance_count'],
|
|
lapsedGovernanceCount: $findingAttentionCounts['lapsed_governance_count'],
|
|
activeNonNewFindingsCount: $findingAttentionCounts['active_non_new_findings_count'],
|
|
highSeverityActiveFindingsCount: $findingAttentionCounts['high_severity_active_findings_count'],
|
|
);
|
|
}
|
|
|
|
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,
|
|
duplicateNameSubjectsCount: $duplicateNameSubjectsCount,
|
|
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,
|
|
evidenceGapDetails: $evidenceGapDetails,
|
|
baselineCompareDiagnostics: $baselineCompareDiagnostics,
|
|
evidenceGapStructuralCount: $evidenceGapStructuralCount,
|
|
evidenceGapOperationalCount: $evidenceGapOperationalCount,
|
|
evidenceGapTransientCount: $evidenceGapTransientCount,
|
|
evidenceGapLegacyMode: $evidenceGapLegacyMode,
|
|
overdueOpenFindingsCount: $findingAttentionCounts['overdue_open_findings_count'],
|
|
expiringGovernanceCount: $findingAttentionCounts['expiring_governance_count'],
|
|
lapsedGovernanceCount: $findingAttentionCounts['lapsed_governance_count'],
|
|
activeNonNewFindingsCount: $findingAttentionCounts['active_non_new_findings_count'],
|
|
highSeverityActiveFindingsCount: $findingAttentionCounts['high_severity_active_findings_count'],
|
|
);
|
|
}
|
|
|
|
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,
|
|
duplicateNameSubjectsCount: $duplicateNameSubjectsCount,
|
|
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,
|
|
evidenceGapDetails: $evidenceGapDetails,
|
|
baselineCompareDiagnostics: $baselineCompareDiagnostics,
|
|
evidenceGapStructuralCount: $evidenceGapStructuralCount,
|
|
evidenceGapOperationalCount: $evidenceGapOperationalCount,
|
|
evidenceGapTransientCount: $evidenceGapTransientCount,
|
|
evidenceGapLegacyMode: $evidenceGapLegacyMode,
|
|
overdueOpenFindingsCount: $findingAttentionCounts['overdue_open_findings_count'],
|
|
expiringGovernanceCount: $findingAttentionCounts['expiring_governance_count'],
|
|
lapsedGovernanceCount: $findingAttentionCounts['lapsed_governance_count'],
|
|
activeNonNewFindingsCount: $findingAttentionCounts['active_non_new_findings_count'],
|
|
highSeverityActiveFindingsCount: $findingAttentionCounts['high_severity_active_findings_count'],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create a DTO for widget consumption (only open/new findings).
|
|
*/
|
|
public static function forWidget(?ManagedEnvironment $tenant): self
|
|
{
|
|
if (! $tenant instanceof ManagedEnvironment) {
|
|
return self::empty('no_tenant', null);
|
|
}
|
|
|
|
$assignment = BaselineTenantAssignment::query()
|
|
->where('managed_environment_id', $tenant->getKey())
|
|
->with('baselineProfile')
|
|
->first();
|
|
|
|
if (! $assignment instanceof BaselineTenantAssignment || $assignment->baselineProfile === null) {
|
|
return self::empty('no_assignment', null);
|
|
}
|
|
|
|
$profile = $assignment->baselineProfile;
|
|
$truthResolution = app(BaselineSnapshotTruthResolver::class)->resolveCompareSnapshot($profile);
|
|
$effectiveSnapshot = $truthResolution['effective_snapshot'] ?? null;
|
|
$snapshotId = $effectiveSnapshot instanceof BaselineSnapshot ? (int) $effectiveSnapshot->getKey() : null;
|
|
$snapshotReasonCode = is_string($truthResolution['reason_code'] ?? null) ? (string) $truthResolution['reason_code'] : null;
|
|
$snapshotReasonMessage = self::missingSnapshotMessage($snapshotReasonCode);
|
|
$scopeKey = 'baseline_profile:'.$profile->getKey();
|
|
|
|
$severityRows = Finding::query()
|
|
->where('managed_environment_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('managed_environment_id', $tenant->getKey())
|
|
->whereIn('type', OperationCatalog::rawValuesForCanonical(OperationRunType::BaselineCompare->value))
|
|
->where('context->baseline_profile_id', (string) $profile->getKey())
|
|
->whereNotNull('completed_at')
|
|
->latest('completed_at')
|
|
->first();
|
|
|
|
return new self(
|
|
state: $snapshotId === null ? 'no_snapshot' : ($totalFindings > 0 ? 'ready' : 'idle'),
|
|
message: $snapshotId === null ? $snapshotReasonMessage : null,
|
|
profileName: (string) $profile->name,
|
|
profileId: (int) $profile->getKey(),
|
|
snapshotId: $snapshotId,
|
|
duplicateNamePoliciesCount: null,
|
|
duplicateNameSubjectsCount: 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,
|
|
reasonCode: $snapshotReasonCode,
|
|
reasonMessage: $snapshotReasonMessage,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array{policy_count: int, subject_count: int}
|
|
*/
|
|
private static function duplicateNameStats(ManagedEnvironment $tenant, BaselineScope $effectiveScope): array
|
|
{
|
|
$policyTypes = $effectiveScope->allTypes();
|
|
|
|
if ($policyTypes === []) {
|
|
return [
|
|
'policy_count' => 0,
|
|
'subject_count' => 0,
|
|
];
|
|
}
|
|
|
|
$latestInventorySyncRunId = self::latestInventorySyncRunId($tenant);
|
|
|
|
$compute = static function () use ($tenant, $policyTypes, $latestInventorySyncRunId): array {
|
|
/**
|
|
* @var array<string, int> $countsByKey
|
|
*/
|
|
$countsByKey = [];
|
|
|
|
$query = InventoryItem::query()
|
|
->where('managed_environment_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) {
|
|
$displayLabel = is_string($inventoryItem->display_name) ? trim(mb_strtolower($inventoryItem->display_name)) : '';
|
|
|
|
if ($displayLabel === '') {
|
|
continue;
|
|
}
|
|
|
|
$logicalKey = (string) $inventoryItem->policy_type.'|'.$displayLabel;
|
|
$countsByKey[$logicalKey] = ($countsByKey[$logicalKey] ?? 0) + 1;
|
|
}
|
|
});
|
|
|
|
$duplicatePolicies = 0;
|
|
$duplicateSubjects = 0;
|
|
|
|
foreach ($countsByKey as $count) {
|
|
if ($count > 1) {
|
|
$duplicateSubjects++;
|
|
$duplicatePolicies += $count;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'policy_count' => $duplicatePolicies,
|
|
'subject_count' => $duplicateSubjects,
|
|
];
|
|
};
|
|
|
|
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',
|
|
);
|
|
|
|
/** @var array{policy_count: int, subject_count: int} $stats */
|
|
$stats = Cache::remember($cacheKey, now()->addSeconds(60), $compute);
|
|
|
|
return $stats;
|
|
}
|
|
|
|
private static function latestInventorySyncRunId(ManagedEnvironment $tenant): ?int
|
|
{
|
|
$run = OperationRun::query()
|
|
->where('managed_environment_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<string>, 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<string, int>}
|
|
*/
|
|
private static function evidenceGapSummaryForRun(?OperationRun $run): array
|
|
{
|
|
if (! $run instanceof OperationRun) {
|
|
return [null, []];
|
|
}
|
|
|
|
$details = self::evidenceGapDetailsForRun($run);
|
|
$summary = is_array($details['summary'] ?? null) ? $details['summary'] : [];
|
|
$count = is_numeric($summary['count'] ?? null) ? (int) $summary['count'] : null;
|
|
$byReason = is_array($summary['by_reason'] ?? null) ? $summary['by_reason'] : [];
|
|
|
|
return [$count, array_slice($byReason, 0, 6, true)];
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* summary: array{
|
|
* count: int,
|
|
* by_reason: array<string, int>,
|
|
* detail_state: string,
|
|
* recorded_subjects_total: int,
|
|
* missing_detail_count: int
|
|
* },
|
|
* buckets: list<array{
|
|
* reason_code: string,
|
|
* reason_label: string,
|
|
* count: int,
|
|
* recorded_count: int,
|
|
* missing_detail_count: int,
|
|
* detail_state: string,
|
|
* search_text: string,
|
|
* rows: list<array{
|
|
* reason_code: string,
|
|
* reason_label: string,
|
|
* policy_type: string,
|
|
* subject_key: string,
|
|
* search_text: string
|
|
* }>
|
|
* }>
|
|
* }
|
|
*/
|
|
private static function evidenceGapDetailsForRun(?OperationRun $run): array
|
|
{
|
|
if (! $run instanceof OperationRun) {
|
|
return BaselineCompareEvidenceGapDetails::fromContext([]);
|
|
}
|
|
|
|
return BaselineCompareEvidenceGapDetails::fromOperationRun($run);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function baselineCompareDiagnosticsForRun(?OperationRun $run): array
|
|
{
|
|
if (! $run instanceof OperationRun) {
|
|
return [];
|
|
}
|
|
|
|
$context = is_array($run->context) ? $run->context : [];
|
|
$baselineCompare = $context['baseline_compare'] ?? null;
|
|
|
|
if (! is_array($baselineCompare)) {
|
|
return [];
|
|
}
|
|
|
|
return BaselineCompareEvidenceGapDetails::diagnosticsPayload($baselineCompare);
|
|
}
|
|
|
|
/**
|
|
* @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),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* overdue_open_findings_count: int,
|
|
* expiring_governance_count: int,
|
|
* lapsed_governance_count: int,
|
|
* active_non_new_findings_count: int,
|
|
* high_severity_active_findings_count: int
|
|
* }
|
|
*/
|
|
private static function findingAttentionCounts(ManagedEnvironment $tenant): array
|
|
{
|
|
$tenantId = (int) $tenant->getKey();
|
|
|
|
$overdueOpenFindingsCount = Finding::query()
|
|
->where('managed_environment_id', $tenantId)
|
|
->whereIn('status', Finding::openStatusesForQuery())
|
|
->whereNotNull('due_at')
|
|
->where('due_at', '<', now())
|
|
->count();
|
|
|
|
$expiringGovernanceCount = Finding::query()
|
|
->where('managed_environment_id', $tenantId)
|
|
->where('status', Finding::STATUS_RISK_ACCEPTED)
|
|
->whereHas('findingException', function ($query): void {
|
|
$query->where('current_validity_state', \App\Models\FindingException::VALIDITY_EXPIRING);
|
|
})
|
|
->count();
|
|
|
|
$lapsedGovernanceCount = Finding::query()
|
|
->where('managed_environment_id', $tenantId)
|
|
->where('status', Finding::STATUS_RISK_ACCEPTED)
|
|
->where(function ($query): void {
|
|
$query
|
|
->whereDoesntHave('findingException')
|
|
->orWhereHas('findingException', function ($exceptionQuery): void {
|
|
$exceptionQuery->whereIn('current_validity_state', [
|
|
\App\Models\FindingException::VALIDITY_EXPIRED,
|
|
\App\Models\FindingException::VALIDITY_REVOKED,
|
|
\App\Models\FindingException::VALIDITY_REJECTED,
|
|
\App\Models\FindingException::VALIDITY_MISSING_SUPPORT,
|
|
]);
|
|
});
|
|
})
|
|
->count();
|
|
|
|
$activeNonNewFindingsCount = Finding::query()
|
|
->where('managed_environment_id', $tenantId)
|
|
->whereIn('status', [
|
|
Finding::STATUS_TRIAGED,
|
|
Finding::STATUS_IN_PROGRESS,
|
|
Finding::STATUS_REOPENED,
|
|
])
|
|
->count();
|
|
|
|
$highSeverityActiveFindingsCount = Finding::query()
|
|
->where('managed_environment_id', $tenantId)
|
|
->whereIn('status', Finding::openStatusesForQuery())
|
|
->whereIn('severity', [
|
|
Finding::SEVERITY_HIGH,
|
|
Finding::SEVERITY_CRITICAL,
|
|
])
|
|
->count();
|
|
|
|
return [
|
|
'overdue_open_findings_count' => $overdueOpenFindingsCount,
|
|
'expiring_governance_count' => $expiringGovernanceCount,
|
|
'lapsed_governance_count' => $lapsedGovernanceCount,
|
|
'active_non_new_findings_count' => $activeNonNewFindingsCount,
|
|
'high_severity_active_findings_count' => $highSeverityActiveFindingsCount,
|
|
];
|
|
}
|
|
|
|
public function operatorExplanation(): OperatorExplanationPattern
|
|
{
|
|
/** @var BaselineCompareExplanationRegistry $registry */
|
|
$registry = app(BaselineCompareExplanationRegistry::class);
|
|
|
|
return $registry->forStats($this);
|
|
}
|
|
|
|
public function summaryAssessment(): BaselineCompareSummaryAssessment
|
|
{
|
|
/** @var BaselineCompareSummaryAssessor $assessor */
|
|
$assessor = app(BaselineCompareSummaryAssessor::class);
|
|
|
|
return $assessor->assess($this);
|
|
}
|
|
|
|
public function toTenantGovernanceAggregate(ManagedEnvironment $tenant): TenantGovernanceAggregate
|
|
{
|
|
$summaryAssessment = $this->summaryAssessment();
|
|
|
|
return new TenantGovernanceAggregate(
|
|
tenantId: (int) $tenant->getKey(),
|
|
workspaceId: (int) $tenant->workspace_id,
|
|
profileName: $this->profileName,
|
|
compareState: $this->state,
|
|
stateFamily: $summaryAssessment->stateFamily,
|
|
tone: $summaryAssessment->tone,
|
|
headline: $summaryAssessment->headline,
|
|
supportingMessage: $summaryAssessment->supportingMessage,
|
|
reasonCode: $summaryAssessment->reasonCode,
|
|
lastComparedLabel: $summaryAssessment->lastComparedLabel,
|
|
visibleDriftFindingsCount: $summaryAssessment->findingsVisibleCount,
|
|
overdueOpenFindingsCount: $this->overdueOpenFindingsCount,
|
|
expiringGovernanceCount: $this->expiringGovernanceCount,
|
|
lapsedGovernanceCount: $this->lapsedGovernanceCount,
|
|
activeNonNewFindingsCount: $this->activeNonNewFindingsCount,
|
|
highSeverityActiveFindingsCount: $this->highSeverityActiveFindingsCount,
|
|
nextActionLabel: $summaryAssessment->nextActionLabel(),
|
|
nextActionTarget: $summaryAssessment->nextActionTarget(),
|
|
positiveClaimAllowed: $summaryAssessment->positiveClaimAllowed,
|
|
stats: $this,
|
|
summaryAssessment: $summaryAssessment,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array{
|
|
* label: string,
|
|
* value: int,
|
|
* role: string,
|
|
* qualifier: ?string,
|
|
* visibilityTier: string
|
|
* }>
|
|
*/
|
|
public function explanationCountDescriptors(): array
|
|
{
|
|
return array_map(
|
|
static fn (CountDescriptor $descriptor): array => $descriptor->toArray(),
|
|
$this->operatorExplanation()->countDescriptors,
|
|
);
|
|
}
|
|
|
|
private static function empty(
|
|
string $state,
|
|
?string $message,
|
|
?string $profileName = null,
|
|
?int $profileId = null,
|
|
?int $duplicateNamePoliciesCount = null,
|
|
?int $duplicateNameSubjectsCount = null,
|
|
array $findingAttentionCounts = [],
|
|
): self {
|
|
return new self(
|
|
state: $state,
|
|
message: $message,
|
|
profileName: $profileName,
|
|
profileId: $profileId,
|
|
snapshotId: null,
|
|
duplicateNamePoliciesCount: $duplicateNamePoliciesCount,
|
|
duplicateNameSubjectsCount: $duplicateNameSubjectsCount,
|
|
operationRunId: null,
|
|
findingsCount: null,
|
|
severityCounts: [],
|
|
lastComparedHuman: null,
|
|
lastComparedIso: null,
|
|
failureReason: null,
|
|
overdueOpenFindingsCount: (int) ($findingAttentionCounts['overdue_open_findings_count'] ?? 0),
|
|
expiringGovernanceCount: (int) ($findingAttentionCounts['expiring_governance_count'] ?? 0),
|
|
lapsedGovernanceCount: (int) ($findingAttentionCounts['lapsed_governance_count'] ?? 0),
|
|
activeNonNewFindingsCount: (int) ($findingAttentionCounts['active_non_new_findings_count'] ?? 0),
|
|
highSeverityActiveFindingsCount: (int) ($findingAttentionCounts['high_severity_active_findings_count'] ?? 0),
|
|
);
|
|
}
|
|
|
|
private static function latestBaselineCaptureRun(BaselineProfile $profile): ?OperationRun
|
|
{
|
|
return OperationRun::query()
|
|
->where('workspace_id', (int) $profile->workspace_id)
|
|
->where('type', OperationRunType::BaselineCapture->value)
|
|
->where('context->baseline_profile_id', (int) $profile->getKey())
|
|
->where('status', OperationRunStatus::Completed->value)
|
|
->orderByDesc('completed_at')
|
|
->orderByDesc('id')
|
|
->first();
|
|
}
|
|
|
|
private static function missingSnapshotMessage(?string $reasonCode, ?OperationRun $latestCaptureRun = null): ?string
|
|
{
|
|
$latestCaptureEnvelope = $latestCaptureRun instanceof OperationRun
|
|
? app(ReasonPresenter::class)->forOperationRun($latestCaptureRun, 'artifact_truth')
|
|
: null;
|
|
|
|
if ($latestCaptureEnvelope !== null
|
|
&& in_array($latestCaptureEnvelope->internalCode, [
|
|
BaselineReasonCodes::CAPTURE_INVENTORY_BLOCKED,
|
|
BaselineReasonCodes::CAPTURE_INVENTORY_FAILED,
|
|
BaselineReasonCodes::CAPTURE_UNUSABLE_COVERAGE,
|
|
BaselineReasonCodes::CAPTURE_ZERO_SUBJECTS,
|
|
], true)
|
|
&& trim($latestCaptureEnvelope->shortExplanation) !== '') {
|
|
return $latestCaptureEnvelope->shortExplanation;
|
|
}
|
|
|
|
return match ($reasonCode) {
|
|
BaselineReasonCodes::COMPARE_SNAPSHOT_BUILDING => 'The latest baseline capture is still building. Compare becomes available after a complete snapshot is finalized.',
|
|
BaselineReasonCodes::COMPARE_SNAPSHOT_INCOMPLETE => 'The latest baseline capture is incomplete and there is no current complete snapshot to compare against.',
|
|
BaselineReasonCodes::COMPARE_NO_CONSUMABLE_SNAPSHOT,
|
|
BaselineReasonCodes::COMPARE_NO_ACTIVE_SNAPSHOT => 'The baseline profile has no complete snapshot yet. A workspace manager needs to capture a baseline first.',
|
|
default => null,
|
|
};
|
|
}
|
|
}
|