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 * } */ 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 */ 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 $latestReports * @return list */ 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 $latestReports * @return list */ 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 $latestReports * @return list */ 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 */ 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 $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; } }