191 lines
5.9 KiB
PHP
191 lines
5.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\ReviewPublicationResolution;
|
|
|
|
use App\Models\EnvironmentReview;
|
|
use Carbon\CarbonInterface;
|
|
|
|
final readonly class ResolutionProofEvaluation
|
|
{
|
|
/**
|
|
* @param array<string, mixed> $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<string, mixed>
|
|
* }
|
|
*/
|
|
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<string, mixed> $summary
|
|
* @return array<string, mixed>
|
|
*/
|
|
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;
|
|
}
|
|
}
|