ImageScanStatus::class, 'error_code' => ImageScanErrorCode::class, 'error_context' => 'array', 'summary' => 'array', 'findings' => 'array', 'candidates' => 'array', 'builds_image' => 'boolean', 'is_baseline' => 'boolean', 'grype_db_built_at' => 'datetime', 'scanned_at' => 'datetime', ]; } /** * Entries of `candidates`. Written as `{status, checked_at, entries}`; older rows hold a bare list of entries. * * @return list> */ public function candidateEntries(): array { $candidates = $this->candidates; if (! is_array($candidates)) { return []; } $entries = array_is_list($candidates) ? $candidates : ($candidates['entries'] ?? []); return is_array($entries) ? array_values(array_filter($entries, 'is_array')) : []; } /** * Result of the last candidate check: ok, no_better, cannot_compare, auth_required, or failed. Null before the first check. */ public function candidateStatus(): ?string { $status = is_array($this->candidates) && ! array_is_list($this->candidates) ? ($this->candidates['status'] ?? null) : null; return is_string($status) ? $status : null; } /** * The better candidates Coolify can switch to: newer tags of the same line first, then a newer build of the same * tag. A major version is never one of them. * * @return list> */ public function updateCandidates(): array { try { $current = ImageRef::parse((string) $this->image_ref); } catch (InvalidArgumentException) { return []; } if ($current->digest !== null) { return []; } $currentTag = $current->tag ?? 'latest'; $line = []; $sameTag = []; foreach ($this->candidateEntries() as $entry) { if (($entry['better'] ?? false) !== true || ! is_string($entry['ref'] ?? null)) { continue; } try { $ref = ImageRef::parse($entry['ref']); } catch (InvalidArgumentException) { continue; } if ($ref->qualifiedName() !== $current->qualifiedName() || $ref->digest !== null || $ref->tag === null) { continue; } $kind = $entry['kind'] ?? null; if ($kind === 'line' && $ref->tag !== $currentTag) { $line[] = $entry; } elseif ($kind === 'same_tag' && $ref->tag === $currentTag) { $sameTag[] = $entry; } } return [...$line, ...$sameTag]; } public function resource(): MorphTo { return $this->morphTo('resource'); } public function server(): BelongsTo { return $this->belongsTo(Server::class); } public function sbom(): BelongsTo { return $this->belongsTo(ImageSbom::class, 'image_sbom_id'); } public function deploymentQueue(): BelongsTo { return $this->belongsTo(ApplicationDeploymentQueue::class, 'deployment_queue_id'); } }