$safeSummary */ public function __construct( public ReviewPublicationResolutionStepKey $actionKey, public string $subjectType, public int $subjectId, public ResolutionProofStatus $status, public ResolutionProofCurrentness $currentness, public ResolutionProofUsability $usability, public ResolutionProofVisibility $visibility, public string $reasonCode, public ?ResolutionProofReference $reference = null, public ?int $operationRunId = null, public ?CarbonInterface $evaluatedAt = null, public array $safeSummary = [], ) {} public static function missing( ReviewPublicationResolutionStepKey $actionKey, EnvironmentReview $review, string $reasonCode = 'proof.missing', ?CarbonInterface $evaluatedAt = null, ): self { return new self( actionKey: $actionKey, subjectType: EnvironmentReview::class, subjectId: (int) $review->getKey(), status: ResolutionProofStatus::Missing, currentness: ResolutionProofCurrentness::Unknown, usability: ResolutionProofUsability::NotUsable, visibility: ResolutionProofVisibility::OperatorVisible, reasonCode: $reasonCode, evaluatedAt: $evaluatedAt ?? now(), safeSummary: [ 'message' => 'Proof is missing for the current resolution step.', ], ); } public function canCompleteStep(): bool { if ($this->status !== ResolutionProofStatus::Available) { return false; } if (! in_array($this->visibility, [ ResolutionProofVisibility::OperatorVisible, ], true)) { return false; } if (! in_array($this->currentness, [ ResolutionProofCurrentness::Current, ResolutionProofCurrentness::NotApplicable, ], true)) { return false; } return in_array($this->usability, [ ResolutionProofUsability::Usable, ResolutionProofUsability::UsableWithWarning, ], true); } /** * @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 toStepPayload(): array { return [ 'proof_type' => $this->reference?->proofType, 'proof_id' => $this->reference?->proofId, 'proof_status' => $this->reference?->sourceStatus ?? $this->status->value, 'operation_run_id' => $this->operationRunId, 'proof_currentness' => $this->currentness->value, 'proof_usability' => $this->usability->value, 'proof_visibility' => $this->visibility->value, 'proof_reason_code' => $this->reasonCode, 'proof_evaluated_at' => ($this->evaluatedAt ?? now())->toIso8601String(), 'proof_timestamp' => $this->reference?->proofTimestamp?->toIso8601String(), 'proof_summary' => self::sanitizeSummary($this->safeSummary), ]; } /** * @param array $summary * @return array */ public static function sanitizeSummary(array $summary): array { $safe = []; foreach ($summary as $key => $value) { $key = (string) $key; if (self::isUnsafeKey($key)) { continue; } $sanitized = self::sanitizeValue($value); if ($sanitized !== null) { $safe[$key] = $sanitized; } } return $safe; } private static function isUnsafeKey(string $key): bool { return preg_match('/(payload|graph|token|secret|password|credential|exception|raw|content|response|authorization|headers?|body|stack|trace|access[\\s_-]?token|client[\\s_-]?secret|full[\\s_-]?(report|evidence))/i', $key) === 1; } private static function isUnsafeString(string $value): bool { return preg_match('/(access[\\s_-]?token|refresh[\\s_-]?token|id[\\s_-]?token|client[\\s_-]?secret|secret[-_ ]?token|authorization\\s*:|bearer\\s+[a-z0-9._-]+|raw\\s*(graph|provider|http|response|payload)|graph\\s*(response|payload)|provider\\s*(response|payload)|http\\s*(response|body)|clientexception|serverexception|requestexception|exception\\b|stack\\s*trace|trace\\s*:)/i', $value) === 1; } private static function sanitizeValue(mixed $value): mixed { if ($value === null || is_bool($value) || is_int($value) || is_float($value)) { return $value; } if (is_string($value)) { $value = trim($value); if ($value === '' || self::isUnsafeString($value)) { return null; } return mb_substr($value, 0, 240); } if (! is_array($value)) { return null; } $nested = []; $count = 0; foreach ($value as $nestedKey => $nestedValue) { if ($count >= 12) { break; } $nestedKey = (string) $nestedKey; if (self::isUnsafeKey($nestedKey)) { continue; } $sanitized = self::sanitizeValue($nestedValue); if ($sanitized === null) { continue; } $nested[$nestedKey] = $sanitized; $count++; } return $nested; } }