$checkContext * @param list $actions The primary action first. * @param list $why * @param array{name: string, version: string, fixed: bool}|null $eol The end-of-life OS, and whether an action leaves it. * @param array $facts */ public function __construct( public SecurityStatusType $status, public ?string $checkReason = null, public array $checkContext = [], public array $actions = [], public array $why = [], public int $moreFixed = 0, public bool $exploitedWithoutFix = false, public int $problemCount = 0, public int $exploitedCount = 0, public bool $anyExploited = false, public int $criticalCount = 0, public int $highCount = 0, public int $fixedByPrimary = 0, public int $primaryCritical = 0, public int $primaryHigh = 0, public ?array $eol = null, public ?CarbonImmutable $checkedAt = null, public array $facts = [], public bool $builderImage = false, public int $mediumCount = 0, public int $primaryMedium = 0, ) {} public function primary(): ?SecurityAction { return $this->actions[0] ?? null; } /** * @return array */ public function toArray(): array { return [ 'status' => $this->status->value, 'check_reason' => $this->checkReason, 'check_context' => $this->checkContext, 'actions' => array_map(fn (SecurityAction $action): array => $action->toArray(), $this->actions), 'why' => $this->why, 'more_fixed' => $this->moreFixed, 'exploited_without_fix' => $this->exploitedWithoutFix, 'problem_count' => $this->problemCount, 'exploited_count' => $this->exploitedCount, 'any_exploited' => $this->anyExploited, 'critical_count' => $this->criticalCount, 'high_count' => $this->highCount, 'medium_count' => $this->mediumCount, 'fixed_by_primary' => $this->fixedByPrimary, 'primary_critical' => $this->primaryCritical, 'primary_high' => $this->primaryHigh, 'primary_medium' => $this->primaryMedium, 'eol' => $this->eol, 'checked_at' => $this->checkedAt?->toIso8601String(), 'facts' => $this->facts, 'builder_image' => $this->builderImage, ]; } /** * @param array $data */ public static function fromArray(array $data): self { $eol = is_array($data['eol'] ?? null) ? $data['eol'] : null; $facts = (array) ($data['facts'] ?? []); $stats = (array) ($facts['action_stats'] ?? []); $actions = []; $actionStats = []; foreach (array_values((array) ($data['actions'] ?? [])) as $index => $actionData) { $action = is_array($actionData) ? SecurityAction::fromArray($actionData) : null; if ($action !== null) { $actions[] = $action; $actionStats[] = $stats[$index] ?? []; } } if (array_key_exists('action_stats', $facts)) { $facts['action_stats'] = $actionStats; } return new self( status: SecurityStatusType::tryFrom((string) ($data['status'] ?? '')) ?? SecurityStatusType::NotChecked, checkReason: isset($data['check_reason']) ? (string) $data['check_reason'] : null, checkContext: (array) ($data['check_context'] ?? []), actions: $actions, why: array_values(array_map(fn ($entry): array => self::whyEntry((array) $entry), (array) ($data['why'] ?? []))), moreFixed: (int) ($data['more_fixed'] ?? 0), exploitedWithoutFix: (bool) ($data['exploited_without_fix'] ?? false), problemCount: (int) ($data['problem_count'] ?? 0), exploitedCount: (int) ($data['exploited_count'] ?? 0), anyExploited: (bool) ($data['any_exploited'] ?? false), criticalCount: (int) ($data['critical_count'] ?? 0), highCount: (int) ($data['high_count'] ?? 0), fixedByPrimary: (int) ($data['fixed_by_primary'] ?? 0), primaryCritical: (int) ($data['primary_critical'] ?? 0), primaryHigh: (int) ($data['primary_high'] ?? 0), eol: $eol === null ? null : ['name' => (string) ($eol['name'] ?? ''), 'version' => (string) ($eol['version'] ?? ''), 'fixed' => (bool) ($eol['fixed'] ?? false)], checkedAt: self::time($data['checked_at'] ?? null), facts: $facts, builderImage: (bool) ($data['builder_image'] ?? false), mediumCount: (int) ($data['medium_count'] ?? 0), primaryMedium: (int) ($data['primary_medium'] ?? 0), ); } /** * @param array $entry * @return array{title: string, display_id: string, link: ?string, score: ?float, severity: string, signal: ?string, package: string, version: string} */ public static function whyEntry(array $entry): array { return [ 'title' => ScannerText::storedTitle((string) ($entry['title'] ?? ''), (string) ($entry['package'] ?? ''), (string) ($entry['version'] ?? ''), (string) ($entry['severity'] ?? 'unknown')), 'display_id' => (string) ($entry['display_id'] ?? ''), 'link' => isset($entry['link']) ? (string) $entry['link'] : null, 'score' => is_numeric($entry['score'] ?? null) ? (float) $entry['score'] : null, 'severity' => (string) ($entry['severity'] ?? 'unknown'), 'signal' => isset($entry['signal']) ? (string) $entry['signal'] : null, 'package' => (string) ($entry['package'] ?? ''), 'version' => (string) ($entry['version'] ?? ''), ]; } private static function time(mixed $value): ?CarbonImmutable { if (! is_string($value) || $value === '') { return null; } try { return CarbonImmutable::parse($value); } catch (Throwable) { return null; } } }