TenantAtlas/apps/platform/app/Support/ReviewPublicationResolution/ReviewPublicationProofResolver.php
ahmido 83c679cf85 feat: add review publication proof currentness contract (#459)
Automated PR created by Codex via Gitea API.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #459
2026-06-19 19:10:35 +00:00

1077 lines
46 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\ReviewPublicationResolution;
use App\Models\EnvironmentReview;
use App\Models\EvidenceSnapshot;
use App\Models\EvidenceSnapshotItem;
use App\Models\OperationRun;
use App\Models\ReviewPack;
use App\Models\ReviewPublicationResolutionStep;
use App\Models\StoredReport;
use App\Services\ReviewPackService;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\ReviewPackStatus;
use Carbon\CarbonInterface;
use Illuminate\Support\Collection;
final class ReviewPublicationProofResolver
{
private const array REPORT_DIMENSION_TYPES = [
'permission_posture' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE,
'entra_admin_roles' => StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES,
];
private const string PROVIDER_CONNECTION_CHECK_OPERATION_TYPE = 'provider.connection.check';
public function __construct(
private readonly ReviewPackService $reviewPacks,
) {}
/**
* @return array{
* proof_type:?string,
* proof_id:?int,
* proof_status:?string,
* operation_run_id:?int,
* proof_currentness:string,
* proof_usability:string,
* proof_visibility:string,
* proof_reason_code:string,
* proof_evaluated_at:?string,
* proof_timestamp:?string,
* proof_summary:array<string, mixed>
* }
*/
public function proofFor(
ReviewPublicationResolutionStepKey $stepKey,
EnvironmentReview $review,
array $readiness = [],
?ReviewPublicationResolutionStep $existingStep = null,
): array {
return $this->evaluationFor($stepKey, $review, $readiness, $existingStep)->toStepPayload();
}
public function evaluationFor(
ReviewPublicationResolutionStepKey $stepKey,
EnvironmentReview $review,
array $readiness = [],
?ReviewPublicationResolutionStep $existingStep = null,
): ResolutionProofEvaluation {
$review->loadMissing([
'evidenceSnapshot.items',
'currentExportReviewPack.operationRun',
'operationRun',
]);
return match ($stepKey) {
ReviewPublicationResolutionStepKey::ValidateReviewReadiness => $this->reviewProof(
stepKey: $stepKey,
review: $review,
readiness: $readiness,
usable: true,
reasonCode: 'proof.review_readiness_evaluated',
),
ReviewPublicationResolutionStepKey::CompleteRequiredReports => $this->requiredReportsProof($review, $readiness, $existingStep),
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => $this->evidenceProof($review, $readiness, $existingStep),
ReviewPublicationResolutionStepKey::RefreshReviewComposition => $this->reviewOutputProof($stepKey, $review, $readiness, $existingStep),
ReviewPublicationResolutionStepKey::ReturnToPublication => $this->reviewProof(
stepKey: $stepKey,
review: $review,
readiness: $readiness,
usable: ! (bool) ($readiness['review_requires_refresh'] ?? false),
reasonCode: ! (bool) ($readiness['review_requires_refresh'] ?? false)
? 'proof.review_output_current'
: 'proof.review_output_stale',
),
ReviewPublicationResolutionStepKey::GenerateReviewPack => $this->reviewPackProof($review, $readiness, $existingStep),
};
}
public function evidenceProof(
EnvironmentReview|EvidenceSnapshot|null $subject,
array $readiness = [],
?ReviewPublicationResolutionStep $existingStep = null,
): ResolutionProofEvaluation|array {
if ($subject instanceof EnvironmentReview) {
$review = $subject;
$snapshot = $review->evidenceSnapshot;
} else {
$review = null;
$snapshot = $subject;
}
if (! $snapshot instanceof EvidenceSnapshot) {
if ($review instanceof EnvironmentReview && $existingStep instanceof ReviewPublicationResolutionStep) {
return $this->operationProofOrMissing($existingStep, $review, $readiness, 'proof.evidence_missing');
}
$missing = ResolutionProofEvaluation::missing(
actionKey: ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot,
review: $review ?? new EnvironmentReview,
reasonCode: 'proof.evidence_missing',
);
return $review instanceof EnvironmentReview ? $missing : $missing->toStepPayload();
}
if (! $review instanceof EnvironmentReview) {
return [
'proof_type' => 'evidence_snapshot',
'proof_id' => (int) $snapshot->getKey(),
'proof_status' => (string) $snapshot->status,
'operation_run_id' => is_numeric($snapshot->operation_run_id) ? (int) $snapshot->operation_run_id : null,
];
}
if (! $this->sameReviewScope($review, $snapshot)) {
return $this->notAccessible($review, ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot, 'proof.evidence_scope_mismatch');
}
$latestReports = $this->latestRequiredReports($review);
$staleDimensions = $this->staleSnapshotDimensions($snapshot, $latestReports);
$missingCurrentReportReferenceDimensions = $this->missingCurrentReportReferenceDimensions($snapshot, $latestReports);
$staleOrMissingCurrentReportReferenceDimensions = array_values(array_unique(array_merge(
$staleDimensions,
$missingCurrentReportReferenceDimensions,
)));
$unavailableReportDimensions = $this->unavailableRequiredReportDimensions($latestReports);
$snapshotComplete = (string) $snapshot->status === EvidenceSnapshotStatus::Active->value
&& (string) $snapshot->completeness_state === EvidenceCompletenessState::Complete->value
&& (int) data_get($snapshot->summary, 'missing_dimensions', 0) === 0
&& (int) data_get($snapshot->summary, 'stale_dimensions', 0) === 0;
$usable = $snapshotComplete
&& $staleOrMissingCurrentReportReferenceDimensions === []
&& $unavailableReportDimensions === [];
$evaluation = new ResolutionProofEvaluation(
actionKey: ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot,
subjectType: EnvironmentReview::class,
subjectId: (int) $review->getKey(),
status: $this->artifactStatus((string) $snapshot->status, [
EvidenceSnapshotStatus::Active->value => ResolutionProofStatus::Available,
EvidenceSnapshotStatus::Queued->value => ResolutionProofStatus::Running,
EvidenceSnapshotStatus::Generating->value => ResolutionProofStatus::Running,
EvidenceSnapshotStatus::Failed->value => ResolutionProofStatus::Failed,
EvidenceSnapshotStatus::Superseded->value => ResolutionProofStatus::Unavailable,
EvidenceSnapshotStatus::Expired->value => ResolutionProofStatus::Unavailable,
]),
currentness: $usable ? ResolutionProofCurrentness::Current : ResolutionProofCurrentness::Stale,
usability: $usable ? ResolutionProofUsability::Usable : ResolutionProofUsability::NotUsable,
visibility: ResolutionProofVisibility::OperatorVisible,
reasonCode: $usable ? 'proof.evidence_current' : 'proof.evidence_stale',
reference: new ResolutionProofReference(
proofType: 'evidence_snapshot',
proofId: (int) $snapshot->getKey(),
sourceStatus: (string) $snapshot->status,
proofTimestamp: $snapshot->generated_at ?? $snapshot->updated_at,
),
operationRunId: is_numeric($snapshot->operation_run_id) ? (int) $snapshot->operation_run_id : null,
evaluatedAt: now(),
safeSummary: [
'label' => $usable ? 'Current evidence proof' : 'Outdated evidence proof',
'stale_dimensions' => $staleOrMissingCurrentReportReferenceDimensions,
'missing_current_report_reference_dimensions' => $missingCurrentReportReferenceDimensions,
'unavailable_report_dimensions' => $unavailableReportDimensions,
'evidence_state' => (string) $snapshot->completeness_state,
],
);
if (! $evaluation->canCompleteStep() && $existingStep instanceof ReviewPublicationResolutionStep) {
return $this->operationProofOrFallback($existingStep, $review, $readiness, $evaluation);
}
return $evaluation;
}
public function reviewPackProof(
EnvironmentReview|ReviewPack|null $subject,
array $readiness = [],
?ReviewPublicationResolutionStep $existingStep = null,
): ResolutionProofEvaluation|array {
if ($subject instanceof EnvironmentReview) {
$review = $subject;
$reviewPack = $review->currentExportReviewPack;
} else {
$review = null;
$reviewPack = $subject;
}
if (! $reviewPack instanceof ReviewPack) {
if ($review instanceof EnvironmentReview && $existingStep instanceof ReviewPublicationResolutionStep) {
return $this->operationProofOrMissing($existingStep, $review, $readiness, 'proof.review_pack_missing');
}
$missing = ResolutionProofEvaluation::missing(
actionKey: ReviewPublicationResolutionStepKey::GenerateReviewPack,
review: $review ?? new EnvironmentReview,
reasonCode: 'proof.review_pack_missing',
);
return $review instanceof EnvironmentReview ? $missing : $missing->toStepPayload();
}
if (! $review instanceof EnvironmentReview) {
return [
'proof_type' => 'review_pack',
'proof_id' => (int) $reviewPack->getKey(),
'proof_status' => (string) $reviewPack->status,
'operation_run_id' => is_numeric($reviewPack->operation_run_id) ? (int) $reviewPack->operation_run_id : null,
];
}
if (! $this->sameReviewScope($review, $reviewPack)
|| (int) $reviewPack->environment_review_id !== (int) $review->getKey()) {
return $this->notAccessible($review, ReviewPublicationResolutionStepKey::GenerateReviewPack, 'proof.review_pack_scope_mismatch');
}
$packMatchesCurrentReviewOutput = $this->reviewPackMatchesCurrentOutput($review, $reviewPack);
$usable = (string) $reviewPack->status === ReviewPackStatus::Ready->value
&& (bool) ($readiness['has_ready_export'] ?? false)
&& (int) $review->current_export_review_pack_id === (int) $reviewPack->getKey()
&& filled($reviewPack->file_path)
&& filled($reviewPack->file_disk)
&& ($reviewPack->expires_at === null || ! $reviewPack->expires_at->isPast())
&& $packMatchesCurrentReviewOutput;
$evaluation = new ResolutionProofEvaluation(
actionKey: ReviewPublicationResolutionStepKey::GenerateReviewPack,
subjectType: EnvironmentReview::class,
subjectId: (int) $review->getKey(),
status: $this->artifactStatus((string) $reviewPack->status, [
ReviewPackStatus::Ready->value => ResolutionProofStatus::Available,
ReviewPackStatus::Queued->value => ResolutionProofStatus::Running,
ReviewPackStatus::Generating->value => ResolutionProofStatus::Running,
ReviewPackStatus::Failed->value => ResolutionProofStatus::Failed,
ReviewPackStatus::Expired->value => ResolutionProofStatus::Unavailable,
]),
currentness: $usable ? ResolutionProofCurrentness::Current : ResolutionProofCurrentness::Stale,
usability: $usable ? ResolutionProofUsability::Usable : ResolutionProofUsability::NotUsable,
visibility: ResolutionProofVisibility::OperatorVisible,
reasonCode: $usable ? 'proof.review_pack_current' : 'proof.review_pack_stale',
reference: new ResolutionProofReference(
proofType: 'review_pack',
proofId: (int) $reviewPack->getKey(),
sourceStatus: (string) $reviewPack->status,
proofTimestamp: $reviewPack->generated_at ?? $reviewPack->updated_at,
),
operationRunId: is_numeric($reviewPack->operation_run_id) ? (int) $reviewPack->operation_run_id : null,
evaluatedAt: now(),
safeSummary: [
'label' => $usable ? 'Current review-pack proof' : 'Outdated review-pack proof',
],
);
if (! $evaluation->canCompleteStep() && $existingStep instanceof ReviewPublicationResolutionStep) {
return $this->operationProofOrFallback($existingStep, $review, $readiness, $evaluation);
}
return $evaluation;
}
private function requiredReportsProof(
EnvironmentReview $review,
array $readiness,
?ReviewPublicationResolutionStep $existingStep,
): ResolutionProofEvaluation {
$latestReports = $this->latestRequiredReports($review);
$snapshot = $review->evidenceSnapshot;
$missingDimensions = collect((array) ($readiness['missing_report_dimensions'] ?? []))
->filter(static fn (mixed $dimension): bool => is_string($dimension) && array_key_exists($dimension, self::REPORT_DIMENSION_TYPES))
->values()
->all();
$staleDimensions = [];
$proofReport = null;
$operationRun = $existingStep?->operationRun;
$directOperationRunId = $this->supersededOperationRunId($existingStep);
$supersededRun = $this->validSupersededOperationRunId($existingStep, $review);
$previouslySupersededRun = $this->validPreviouslySupersededOperationRunId($existingStep, $review);
$previouslySupersededOperationRun = $previouslySupersededRun !== null
? OperationRun::query()->find($previouslySupersededRun)
: null;
$operationRunForSupersede = $operationRun instanceof OperationRun
? $operationRun
: ($previouslySupersededOperationRun instanceof OperationRun ? $previouslySupersededOperationRun : null);
$canUseSupersedingReport = $supersededRun !== null || $previouslySupersededRun !== null;
$operationSupersededByReport = false;
if ($directOperationRunId !== null && $supersededRun === null && $this->shouldRejectDirectOperationBeforeArtifactProof($existingStep, $review)) {
return $this->operationProofOrFallback(
existingStep: $existingStep,
review: $review,
readiness: $readiness,
fallback: ResolutionProofEvaluation::missing(
actionKey: ReviewPublicationResolutionStepKey::CompleteRequiredReports,
review: $review,
reasonCode: 'proof.required_report_missing',
),
);
}
foreach (self::REPORT_DIMENSION_TYPES as $dimension => $reportType) {
$report = $latestReports->get($dimension);
$item = $snapshot instanceof EvidenceSnapshot
? $snapshot->items->firstWhere('dimension_key', $dimension)
: null;
$itemComplete = $item instanceof EvidenceSnapshotItem
&& (string) $item->state === EvidenceCompletenessState::Complete->value;
if (! $report instanceof StoredReport || (string) $report->status !== StoredReport::STATUS_READY) {
$missingDimensions[] = $dimension;
continue;
}
$proofReport = $report;
$dimensionRequiresReportAction = in_array($dimension, $missingDimensions, true);
$hasCurrentProof = $itemComplete && is_numeric($item->source_record_id) && (int) $item->source_record_id === (int) $report->getKey();
$hasSupersedingProof = $canUseSupersedingReport
&& $this->reportCanSupersedeOperation($report, $operationRunForSupersede);
$hasReadyReportProof = ! $dimensionRequiresReportAction;
if ($hasSupersedingProof) {
$operationSupersededByReport = true;
}
if ($hasCurrentProof || $hasSupersedingProof || $hasReadyReportProof) {
$missingDimensions = array_values(array_diff($missingDimensions, [$dimension]));
continue;
}
$staleDimensions[] = $dimension;
}
$missingDimensions = array_values(array_unique($missingDimensions));
$staleDimensions = array_values(array_unique($staleDimensions));
if ($missingDimensions !== [] || $staleDimensions !== [] || ! $proofReport instanceof StoredReport) {
$fallback = ResolutionProofEvaluation::missing(
actionKey: ReviewPublicationResolutionStepKey::CompleteRequiredReports,
review: $review,
reasonCode: $missingDimensions !== [] ? 'proof.required_report_missing' : 'proof.required_report_stale',
);
$fallback = new ResolutionProofEvaluation(
actionKey: ReviewPublicationResolutionStepKey::CompleteRequiredReports,
subjectType: EnvironmentReview::class,
subjectId: (int) $review->getKey(),
status: $missingDimensions !== [] ? ResolutionProofStatus::Missing : ResolutionProofStatus::Available,
currentness: $staleDimensions !== [] ? ResolutionProofCurrentness::Stale : ResolutionProofCurrentness::Unknown,
usability: ResolutionProofUsability::NotUsable,
visibility: ResolutionProofVisibility::OperatorVisible,
reasonCode: $fallback->reasonCode,
evaluatedAt: now(),
safeSummary: [
'missing_dimensions' => $missingDimensions,
'stale_dimensions' => $staleDimensions,
],
);
if ($existingStep instanceof ReviewPublicationResolutionStep) {
return $this->operationProofOrFallback($existingStep, $review, $readiness, $fallback);
}
return $fallback;
}
$supersededRun ??= $previouslySupersededRun;
$effectiveSupersededRun = $operationSupersededByReport ? $supersededRun : null;
return new ResolutionProofEvaluation(
actionKey: ReviewPublicationResolutionStepKey::CompleteRequiredReports,
subjectType: EnvironmentReview::class,
subjectId: (int) $review->getKey(),
status: ResolutionProofStatus::Available,
currentness: ResolutionProofCurrentness::Current,
usability: ResolutionProofUsability::Usable,
visibility: ResolutionProofVisibility::OperatorVisible,
reasonCode: $effectiveSupersededRun === null
? 'proof.required_reports_current'
: 'proof.required_reports_supersede_operation',
reference: new ResolutionProofReference(
proofType: 'stored_report',
proofId: (int) $proofReport->getKey(),
sourceStatus: (string) $proofReport->status,
proofTimestamp: $proofReport->generated_at ?? $proofReport->updated_at,
),
operationRunId: is_numeric($proofReport->operation_run_id) ? (int) $proofReport->operation_run_id : null,
evaluatedAt: now(),
safeSummary: [
'label' => $effectiveSupersededRun === null ? 'Current required-report proof' : 'Superseded by newer required-report proof',
'report_dimensions' => array_keys(self::REPORT_DIMENSION_TYPES),
'superseded_operation_run_id' => $effectiveSupersededRun,
],
);
}
private function reviewProof(
ReviewPublicationResolutionStepKey $stepKey,
EnvironmentReview $review,
array $readiness,
bool $usable,
string $reasonCode,
): ResolutionProofEvaluation {
return new ResolutionProofEvaluation(
actionKey: $stepKey,
subjectType: EnvironmentReview::class,
subjectId: (int) $review->getKey(),
status: ResolutionProofStatus::Available,
currentness: $usable ? ResolutionProofCurrentness::Current : ResolutionProofCurrentness::Stale,
usability: $usable ? ResolutionProofUsability::Usable : ResolutionProofUsability::NotUsable,
visibility: ResolutionProofVisibility::OperatorVisible,
reasonCode: $reasonCode,
reference: new ResolutionProofReference(
proofType: 'environment_review',
proofId: (int) $review->getKey(),
sourceStatus: (string) $review->status,
proofTimestamp: $review->generated_at ?? $review->updated_at,
),
operationRunId: is_numeric($review->operation_run_id) ? (int) $review->operation_run_id : null,
evaluatedAt: now(),
safeSummary: [
'label' => $usable ? 'Current review proof' : 'Outdated review proof',
'review_status' => (string) ($readiness['review_status'] ?? $review->status),
'review_completeness_state' => (string) ($readiness['review_completeness_state'] ?? $review->completeness_state),
],
);
}
private function reviewOutputProof(
ReviewPublicationResolutionStepKey $stepKey,
EnvironmentReview $review,
array $readiness,
?ReviewPublicationResolutionStep $existingStep,
): ResolutionProofEvaluation {
$evaluation = $this->reviewProof(
stepKey: $stepKey,
review: $review,
readiness: $readiness,
usable: ! (bool) ($readiness['review_requires_refresh'] ?? false),
reasonCode: ! (bool) ($readiness['review_requires_refresh'] ?? false)
? 'proof.review_output_current'
: 'proof.review_output_stale',
);
if (! $evaluation->canCompleteStep() && $existingStep instanceof ReviewPublicationResolutionStep) {
return $this->operationProofOrFallback($existingStep, $review, $readiness, $evaluation);
}
return $evaluation;
}
private function operationProofOrMissing(
ReviewPublicationResolutionStep $existingStep,
EnvironmentReview $review,
array $readiness,
string $missingReasonCode,
): ResolutionProofEvaluation {
$fallback = ResolutionProofEvaluation::missing($existingStep->stepKeyEnum() ?? ReviewPublicationResolutionStepKey::ValidateReviewReadiness, $review, $missingReasonCode);
return $this->operationProofOrFallback($existingStep, $review, $readiness, $fallback);
}
private function operationProofOrFallback(
ReviewPublicationResolutionStep $existingStep,
EnvironmentReview $review,
array $readiness,
ResolutionProofEvaluation $fallback,
): ResolutionProofEvaluation {
if (! in_array($existingStep->statusEnum(), [
ReviewPublicationResolutionStepStatus::Running,
ReviewPublicationResolutionStepStatus::Failed,
], true)) {
return $fallback;
}
$operationRun = $existingStep->operationRun;
if (! $operationRun instanceof OperationRun) {
return $fallback;
}
$stepKey = $existingStep->stepKeyEnum() ?? ReviewPublicationResolutionStepKey::ValidateReviewReadiness;
if (! $this->sameOperationScope($review, $operationRun)) {
return $this->notAccessible($review, $stepKey, 'proof.operation_scope_mismatch');
}
$operationMismatchReason = $this->operationMismatchReason($stepKey, $review, $existingStep, $operationRun);
if ($operationMismatchReason !== null) {
return $this->operationMismatch($review, $stepKey, $operationRun, $operationMismatchReason);
}
$hasMatchingReadinessFingerprint = (string) data_get($existingStep->metadata, 'readiness_fingerprint') === (string) ($readiness['fingerprint'] ?? null);
$hasSourceOwnedContext = $this->operationHasCurrentResolutionContext($stepKey, $review, $existingStep, $operationRun)
|| $this->operationSourceArtifactMatchesStep($stepKey, $review, $existingStep, $operationRun);
$currentness = $hasMatchingReadinessFingerprint || $hasSourceOwnedContext
? ResolutionProofCurrentness::Current
: ResolutionProofCurrentness::Stale;
if ((string) $operationRun->status !== OperationRunStatus::Completed->value) {
return new ResolutionProofEvaluation(
actionKey: $stepKey,
subjectType: EnvironmentReview::class,
subjectId: (int) $review->getKey(),
status: ResolutionProofStatus::Running,
currentness: $currentness,
usability: ResolutionProofUsability::InspectionOnly,
visibility: ResolutionProofVisibility::OperatorVisible,
reasonCode: $currentness === ResolutionProofCurrentness::Current
? 'proof.operation_running'
: 'proof.operation_running_stale',
reference: new ResolutionProofReference(
proofType: 'operation_run',
proofId: (int) $operationRun->getKey(),
sourceStatus: (string) $operationRun->status,
proofTimestamp: $operationRun->started_at ?? $operationRun->created_at,
),
operationRunId: (int) $operationRun->getKey(),
evaluatedAt: now(),
safeSummary: [
'label' => 'Operation running',
'operation_type' => (string) $operationRun->type,
],
);
}
$status = match ((string) $operationRun->outcome) {
OperationRunOutcome::Succeeded->value,
OperationRunOutcome::PartiallySucceeded->value => ResolutionProofStatus::Succeeded,
OperationRunOutcome::Cancelled->value => ResolutionProofStatus::Cancelled,
OperationRunOutcome::Failed->value,
OperationRunOutcome::Blocked->value => ResolutionProofStatus::Failed,
default => ResolutionProofStatus::Unknown,
};
return new ResolutionProofEvaluation(
actionKey: $stepKey,
subjectType: EnvironmentReview::class,
subjectId: (int) $review->getKey(),
status: $status,
currentness: $currentness,
usability: ResolutionProofUsability::InspectionOnly,
visibility: ResolutionProofVisibility::OperatorVisible,
reasonCode: $status === ResolutionProofStatus::Succeeded
? 'proof.operation_succeeded_without_artifact'
: 'proof.operation_terminal_without_current_artifact',
reference: new ResolutionProofReference(
proofType: 'operation_run',
proofId: (int) $operationRun->getKey(),
sourceStatus: (string) $operationRun->outcome,
proofTimestamp: $operationRun->completed_at ?? $operationRun->updated_at,
),
operationRunId: (int) $operationRun->getKey(),
evaluatedAt: now(),
safeSummary: [
'label' => $status === ResolutionProofStatus::Succeeded
? 'Operation finished but artifact proof is still missing'
: 'Action failed',
'operation_type' => (string) $operationRun->type,
],
);
}
private function notAccessible(EnvironmentReview $review, ReviewPublicationResolutionStepKey $stepKey, string $reasonCode): ResolutionProofEvaluation
{
return new ResolutionProofEvaluation(
actionKey: $stepKey,
subjectType: EnvironmentReview::class,
subjectId: (int) $review->getKey(),
status: ResolutionProofStatus::NotAccessible,
currentness: ResolutionProofCurrentness::Unknown,
usability: ResolutionProofUsability::NotUsable,
visibility: ResolutionProofVisibility::Hidden,
reasonCode: $reasonCode,
evaluatedAt: now(),
safeSummary: [
'message' => 'Proof is not available for the current workspace or environment.',
],
);
}
/**
* @return Collection<string, StoredReport>
*/
private function latestRequiredReports(EnvironmentReview $review): Collection
{
return StoredReport::query()
->where('workspace_id', (int) $review->workspace_id)
->where('managed_environment_id', (int) $review->managed_environment_id)
->whereIn('report_type', array_values(self::REPORT_DIMENSION_TYPES))
->orderByDesc('generated_at')
->orderByDesc('updated_at')
->orderByDesc('id')
->get()
->unique('report_type')
->mapWithKeys(function (StoredReport $report): array {
$dimension = array_search((string) $report->report_type, self::REPORT_DIMENSION_TYPES, true);
return is_string($dimension) ? [$dimension => $report] : [];
});
}
/**
* @param Collection<string, StoredReport> $latestReports
* @return list<string>
*/
private function staleSnapshotDimensions(EvidenceSnapshot $snapshot, Collection $latestReports): array
{
$stale = [];
foreach (self::REPORT_DIMENSION_TYPES as $dimension => $reportType) {
$report = $latestReports->get($dimension);
$item = $snapshot->items->firstWhere('dimension_key', $dimension);
if (! $report instanceof StoredReport || ! $item instanceof EvidenceSnapshotItem) {
continue;
}
if ((string) $item->state !== EvidenceCompletenessState::Complete->value || ! is_numeric($item->source_record_id)) {
continue;
}
if ((string) $report->status !== StoredReport::STATUS_READY) {
$stale[] = $dimension;
continue;
}
if ((int) $item->source_record_id !== (int) $report->getKey()
&& $this->reportIsNewerThanSnapshot($report, $snapshot, $item->source_record_id)) {
$stale[] = $dimension;
}
}
return $stale;
}
/**
* @param Collection<string, StoredReport> $latestReports
* @return list<string>
*/
private function unavailableRequiredReportDimensions(Collection $latestReports): array
{
$unavailable = [];
foreach (self::REPORT_DIMENSION_TYPES as $dimension => $reportType) {
$report = $latestReports->get($dimension);
if (! $report instanceof StoredReport || (string) $report->status !== StoredReport::STATUS_READY) {
$unavailable[] = $dimension;
}
}
return $unavailable;
}
/**
* @param Collection<string, StoredReport> $latestReports
* @return list<string>
*/
private function missingCurrentReportReferenceDimensions(EvidenceSnapshot $snapshot, Collection $latestReports): array
{
$missing = [];
foreach (self::REPORT_DIMENSION_TYPES as $dimension => $reportType) {
$report = $latestReports->get($dimension);
if (! $report instanceof StoredReport || (string) $report->status !== StoredReport::STATUS_READY) {
continue;
}
$item = $snapshot->items->firstWhere('dimension_key', $dimension);
if (! $item instanceof EvidenceSnapshotItem
|| (string) $item->state !== EvidenceCompletenessState::Complete->value
|| ! is_numeric($item->source_record_id)
|| (int) $item->source_record_id !== (int) $report->getKey()) {
$missing[] = $dimension;
}
}
return $missing;
}
private function reportIsNewerThanSnapshot(StoredReport $report, ?EvidenceSnapshot $snapshot, mixed $itemSourceRecordId): bool
{
if ($snapshot === null) {
return true;
}
if (is_numeric($itemSourceRecordId) && (int) $itemSourceRecordId === (int) $report->getKey()) {
return false;
}
$reportTimestamp = $this->latestTimestamp($report->generated_at, $report->updated_at, $report->created_at);
$snapshotTimestamp = $this->latestTimestamp($snapshot->generated_at, $snapshot->updated_at, $snapshot->created_at);
if (! $reportTimestamp instanceof CarbonInterface) {
return false;
}
if (! $snapshotTimestamp instanceof CarbonInterface) {
return true;
}
if ($reportTimestamp->greaterThan($snapshotTimestamp)) {
return true;
}
if ($reportTimestamp->lessThan($snapshotTimestamp)) {
return false;
}
return ! is_numeric($itemSourceRecordId)
|| (int) $report->getKey() > (int) $itemSourceRecordId;
}
private function reportCanSupersedeOperation(StoredReport $report, ?OperationRun $operationRun): bool
{
if (! $operationRun instanceof OperationRun) {
return true;
}
$reportTimestamp = $this->latestTimestamp($report->generated_at, $report->updated_at, $report->created_at);
$operationTimestamp = $this->latestTimestamp($operationRun->completed_at, $operationRun->started_at, $operationRun->updated_at, $operationRun->created_at);
return $reportTimestamp instanceof CarbonInterface
&& $operationTimestamp instanceof CarbonInterface
&& $reportTimestamp->greaterThan($operationTimestamp);
}
private function latestTimestamp(?CarbonInterface ...$timestamps): ?CarbonInterface
{
return collect($timestamps)
->filter(static fn (?CarbonInterface $timestamp): bool => $timestamp instanceof CarbonInterface)
->sortByDesc(static fn (CarbonInterface $timestamp): int => $timestamp->getTimestamp())
->first();
}
private function sameReviewScope(EnvironmentReview $review, EvidenceSnapshot|ReviewPack $artifact): bool
{
return (int) $artifact->workspace_id === (int) $review->workspace_id
&& (int) $artifact->managed_environment_id === (int) $review->managed_environment_id;
}
private function sameOperationScope(EnvironmentReview $review, OperationRun $operationRun): bool
{
return (int) $operationRun->workspace_id === (int) $review->workspace_id
&& (int) $operationRun->managed_environment_id === (int) $review->managed_environment_id;
}
private function reviewPackMatchesCurrentOutput(EnvironmentReview $review, ReviewPack $reviewPack): bool
{
if (! is_string($reviewPack->fingerprint) || $reviewPack->fingerprint === '') {
return false;
}
$options = is_array($reviewPack->options) ? $reviewPack->options : [];
$expectedFingerprint = $this->reviewPacks->computeFingerprintForReview($review, $options);
return hash_equals($expectedFingerprint, (string) $reviewPack->fingerprint);
}
private function operationMismatchReason(
ReviewPublicationResolutionStepKey $stepKey,
EnvironmentReview $review,
ReviewPublicationResolutionStep $step,
OperationRun $operationRun,
): ?string {
$expectedTypes = $this->expectedOperationTypes($stepKey);
if ($expectedTypes !== [] && ! in_array((string) $operationRun->type, $expectedTypes, true)) {
return 'proof.operation_type_mismatch';
}
$context = is_array($operationRun->context) ? $operationRun->context : [];
$contextReviewIds = [];
foreach (['environment_review_id', 'review_id'] as $reviewContextKey) {
$contextReviewId = data_get($context, $reviewContextKey);
if ($contextReviewId === null) {
continue;
}
if (! is_numeric($contextReviewId)) {
return 'proof.operation_context_mismatch';
}
$contextReviewIds[$reviewContextKey] = (int) $contextReviewId;
}
$contextCaseId = data_get($context, 'review_publication_resolution_case_id');
if ($contextCaseId !== null && ! is_numeric($contextCaseId)) {
return 'proof.operation_context_mismatch';
}
if (count(array_unique(array_values($contextReviewIds))) > 1) {
return 'proof.operation_context_mismatch';
}
foreach ($contextReviewIds as $contextReviewId) {
if ($contextReviewId !== (int) $review->getKey()) {
return 'proof.operation_context_mismatch';
}
}
if ($contextCaseId !== null && (int) $contextCaseId !== (int) $step->case_id) {
return 'proof.operation_context_mismatch';
}
if ($contextReviewIds !== [] || $contextCaseId !== null) {
return null;
}
if ($this->operationSourceArtifactMatchesStep($stepKey, $review, $step, $operationRun)) {
return null;
}
return 'proof.operation_context_missing';
}
private function operationHasCurrentResolutionContext(
ReviewPublicationResolutionStepKey $stepKey,
EnvironmentReview $review,
ReviewPublicationResolutionStep $step,
OperationRun $operationRun,
): bool {
$context = is_array($operationRun->context) ? $operationRun->context : [];
$contextReviewIds = [];
foreach (['environment_review_id', 'review_id'] as $reviewContextKey) {
$contextReviewId = data_get($context, $reviewContextKey);
if ($contextReviewId === null || ! is_numeric($contextReviewId)) {
continue;
}
$contextReviewIds[] = (int) $contextReviewId;
}
$hasCurrentReviewContext = $contextReviewIds !== []
&& count(array_unique($contextReviewIds)) === 1
&& $contextReviewIds[0] === (int) $review->getKey();
$contextCaseId = data_get($context, 'review_publication_resolution_case_id');
$hasCurrentCaseContext = is_numeric($contextCaseId)
&& (int) $contextCaseId === (int) $step->case_id;
if (! $hasCurrentReviewContext && ! $hasCurrentCaseContext) {
return false;
}
$expectedTypes = $this->expectedOperationTypes($stepKey);
return $expectedTypes === [] || in_array((string) $operationRun->type, $expectedTypes, true);
}
private function operationSourceArtifactMatchesStep(
ReviewPublicationResolutionStepKey $stepKey,
EnvironmentReview $review,
ReviewPublicationResolutionStep $step,
OperationRun $operationRun,
): bool {
if (! is_string($step->proof_type) || ! is_numeric($step->proof_id)) {
return false;
}
return match ($stepKey) {
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => $step->proof_type === 'evidence_snapshot'
&& EvidenceSnapshot::query()
->whereKey((int) $step->proof_id)
->where('workspace_id', (int) $review->workspace_id)
->where('managed_environment_id', (int) $review->managed_environment_id)
->where('operation_run_id', (int) $operationRun->getKey())
->exists(),
ReviewPublicationResolutionStepKey::RefreshReviewComposition => $step->proof_type === 'environment_review'
&& (int) $step->proof_id === (int) $review->getKey()
&& EnvironmentReview::query()
->whereKey((int) $review->getKey())
->where('workspace_id', (int) $review->workspace_id)
->where('managed_environment_id', (int) $review->managed_environment_id)
->where('operation_run_id', (int) $operationRun->getKey())
->exists(),
ReviewPublicationResolutionStepKey::GenerateReviewPack => $step->proof_type === 'review_pack'
&& ReviewPack::query()
->whereKey((int) $step->proof_id)
->where('workspace_id', (int) $review->workspace_id)
->where('managed_environment_id', (int) $review->managed_environment_id)
->where('environment_review_id', (int) $review->getKey())
->where('operation_run_id', (int) $operationRun->getKey())
->exists(),
default => false,
};
}
/**
* @return list<string>
*/
private function expectedOperationTypes(ReviewPublicationResolutionStepKey $stepKey): array
{
return match ($stepKey) {
ReviewPublicationResolutionStepKey::CompleteRequiredReports => [
self::PROVIDER_CONNECTION_CHECK_OPERATION_TYPE,
OperationRunType::EntraAdminRolesScan->value,
],
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => [
OperationRunType::EvidenceSnapshotGenerate->value,
],
ReviewPublicationResolutionStepKey::RefreshReviewComposition => [
OperationRunType::EnvironmentReviewCompose->value,
],
ReviewPublicationResolutionStepKey::GenerateReviewPack => [
OperationRunType::ReviewPackGenerate->value,
],
ReviewPublicationResolutionStepKey::ValidateReviewReadiness,
ReviewPublicationResolutionStepKey::ReturnToPublication => [],
};
}
private function operationMismatch(
EnvironmentReview $review,
ReviewPublicationResolutionStepKey $stepKey,
OperationRun $operationRun,
string $reasonCode,
): ResolutionProofEvaluation {
return new ResolutionProofEvaluation(
actionKey: $stepKey,
subjectType: EnvironmentReview::class,
subjectId: (int) $review->getKey(),
status: ResolutionProofStatus::Unknown,
currentness: ResolutionProofCurrentness::Unknown,
usability: ResolutionProofUsability::NotUsable,
visibility: ResolutionProofVisibility::OperatorVisible,
reasonCode: $reasonCode,
evaluatedAt: now(),
safeSummary: [
'label' => 'Linked operation does not match the current step',
'operation_type' => (string) $operationRun->type,
],
);
}
/**
* @param array<string, ResolutionProofStatus> $statusMap
*/
private function artifactStatus(string $status, array $statusMap): ResolutionProofStatus
{
return $statusMap[$status] ?? ResolutionProofStatus::Unknown;
}
private function supersededOperationRunId(?ReviewPublicationResolutionStep $existingStep): ?int
{
if (! $existingStep instanceof ReviewPublicationResolutionStep || ! is_numeric($existingStep->operation_run_id)) {
return null;
}
if (! in_array($existingStep->statusEnum(), [
ReviewPublicationResolutionStepStatus::Running,
ReviewPublicationResolutionStepStatus::Failed,
], true)) {
return null;
}
return (int) $existingStep->operation_run_id;
}
private function shouldRejectDirectOperationBeforeArtifactProof(?ReviewPublicationResolutionStep $existingStep, EnvironmentReview $review): bool
{
if (! $existingStep instanceof ReviewPublicationResolutionStep) {
return true;
}
$operationRun = $existingStep->operationRun;
if (! $operationRun instanceof OperationRun || ! $this->sameOperationScope($review, $operationRun)) {
return true;
}
$mismatchReason = $this->operationMismatchReason(
ReviewPublicationResolutionStepKey::CompleteRequiredReports,
$review,
$existingStep,
$operationRun,
);
if ($mismatchReason === null) {
return false;
}
if ($mismatchReason !== 'proof.operation_context_missing') {
return true;
}
return (string) $operationRun->status !== OperationRunStatus::Completed->value;
}
private function validSupersededOperationRunId(?ReviewPublicationResolutionStep $existingStep, EnvironmentReview $review): ?int
{
$operationRunId = $this->supersededOperationRunId($existingStep);
if ($operationRunId === null || ! $existingStep instanceof ReviewPublicationResolutionStep) {
return null;
}
$operationRun = $existingStep->operationRun;
if (! $operationRun instanceof OperationRun
|| ! $this->sameOperationScope($review, $operationRun)
|| $this->operationMismatchReason(ReviewPublicationResolutionStepKey::CompleteRequiredReports, $review, $existingStep, $operationRun) !== null) {
return null;
}
return $operationRunId;
}
private function previouslySupersededOperationRunId(?ReviewPublicationResolutionStep $existingStep): ?int
{
if (! $existingStep instanceof ReviewPublicationResolutionStep) {
return null;
}
$reasonCode = (string) data_get($existingStep->metadata, 'proof_reason_code', '');
$operationRunId = data_get($existingStep->metadata, 'proof_summary.superseded_operation_run_id');
if (! str_contains($reasonCode, 'supersede') || ! is_numeric($operationRunId)) {
return null;
}
return (int) $operationRunId;
}
private function validPreviouslySupersededOperationRunId(?ReviewPublicationResolutionStep $existingStep, EnvironmentReview $review): ?int
{
$operationRunId = $this->previouslySupersededOperationRunId($existingStep);
if ($operationRunId === null) {
return null;
}
$operationRun = OperationRun::query()->find($operationRunId);
if (! $operationRun instanceof OperationRun
|| ! $this->sameOperationScope($review, $operationRun)
|| ! ($existingStep instanceof ReviewPublicationResolutionStep)
|| $this->operationMismatchReason(ReviewPublicationResolutionStepKey::CompleteRequiredReports, $review, $existingStep, $operationRun) !== null) {
return null;
}
$expectedTypes = $this->expectedOperationTypes(ReviewPublicationResolutionStepKey::CompleteRequiredReports);
if ($expectedTypes !== [] && ! in_array((string) $operationRun->type, $expectedTypes, true)) {
return null;
}
return $operationRunId;
}
}