onQueue(isCloud() ? maintenance_queue() : 'default'); } public function uniqueId(): string { return (string) $this->server->id; } public static function stateCacheKey(Server $server): string { return "image-scan-prepare:{$server->id}"; } public static function builtAtCacheKey(Server $server): string { return self::stateCacheKey($server).':built-at'; } public static function diskUsedCacheKey(Server $server): string { return self::stateCacheKey($server).':disk-used'; } public static function failedStepCacheKey(Server $server): string { return self::stateCacheKey($server).':failed-step'; } public static function forget(Server $server): void { Cache::forget(self::stateCacheKey($server)); Cache::forget(self::failedStepCacheKey($server)); Cache::forget(self::builtAtCacheKey($server)); Cache::forget(self::diskUsedCacheKey($server)); } public function handle(): void { $server = $this->server->fresh(['settings']); if (! $server instanceof Server || $server->isTransferredAway()) { return; } $settings = InstanceSettings::get(); if (! $settings->is_image_scanning_allowed || ! $server->settings?->is_image_scan_enabled) { return; } try { $this->prepare($server, $settings->image_scan_db_url); } catch (\Throwable $e) { report($e); $this->setState($server, self::STATE_FAILED, self::FAILED_CONNECTION); } } public function failed(\Throwable $exception): void { $this->setState($this->server, self::STATE_FAILED); } private function prepare(Server $server, ?string $dbUrl): void { $commands = new ScannerCommands($server, Str::lower(Str::random(24))); $this->setState($server, self::STATE_PULLING); $pulled = self::run($commands->prepare(), $server, self::TIMEOUT_PREPARE); if ($pulled['exit_codes'] === []) { $this->setState($server, self::STATE_FAILED, self::FAILED_CONNECTION); return; } foreach (['pull-syft', 'pull-grype', 'pull-regctl', 'pull-init'] as $step) { if (($pulled['exit_codes'][$step] ?? null) !== 0) { $this->setState($server, self::STATE_FAILED, self::FAILED_PULLING); return; } } if (($pulled['exit_codes']['db-volume'] ?? null) !== 0) { $this->setState($server, self::STATE_FAILED, self::FAILED_SETUP); return; } if (! $this->isStillEnabled($server)) { return; } $this->setState($server, self::STATE_UPDATING_DATABASE); $lock = ScanImage::serverLock($server); if (! ScanImage::acquireServerLock($lock, $server, 0)) { if ($this->attempts() >= $this->tries) { $this->setState($server, self::STATE_FAILED); return; } $this->release(ScanResourceImagesJob::lockRetryDelay($this->attempts())); return; } try { $updated = self::run($commands->dbUpdate(is_string($dbUrl) && trim($dbUrl) !== '' ? trim($dbUrl) : null), $server, self::TIMEOUT_DB_UPDATE); } finally { $lock->release(); } if (($updated['exit_codes']['db-update'] ?? null) !== 0) { $this->setState($server, self::STATE_FAILED); return; } if (! $this->isStillEnabled($server)) { return; } self::storeDetails($server, $commands); $this->setState($server, self::STATE_READY); } /** * @param list $commands * @return array{exit_codes: array, sizes: array, payload: string} */ private static function run(array $commands, Server $server, int $timeout): array { return ScannerCommands::parseOutput(instant_remote_process($commands, $server, throwError: false, timeout: $timeout)); } /** * Caches the database build date and the disk use of the scanner data that the server page shows. */ public static function storeDetails(Server $server, ScannerCommands $commands): void { Cache::forget(self::builtAtCacheKey($server)); Cache::forget(self::diskUsedCacheKey($server)); try { $status = self::run($commands->dbStatus(), $server, self::TIMEOUT_DB_STATUS); $report = ($status['exit_codes']['db-status'] ?? null) === 0 ? json_decode($status['payload'], true) : null; $built = is_array($report) && ($report['valid'] ?? false) === true ? ($report['built'] ?? null) : null; if (is_string($built)) { Cache::put(self::builtAtCacheKey($server), CarbonImmutable::parse($built)->toIso8601ZuluString(), now()->addDays(7)); } } catch (\Throwable $e) { report($e); } try { $usage = self::run($commands->diskUsage(), $server, self::TIMEOUT_DB_STATUS); if (isset($usage['sizes']['disk-db']) || isset($usage['sizes']['disk-sboms'])) { Cache::put(self::diskUsedCacheKey($server), ($usage['sizes']['disk-db'] ?? 0) + ($usage['sizes']['disk-sboms'] ?? 0), now()->addDays(7)); } } catch (\Throwable $e) { report($e); } } private function isStillEnabled(Server $server): bool { $enabled = (bool) $server->settings()->value('is_image_scan_enabled'); if (! $enabled) { self::forget($server); } return $enabled; } /** * @param string|null $failedStep one of the FAILED_* steps; without it a failure is put on the running step */ private function setState(Server $server, string $state, ?string $failedStep = null): void { $running = in_array($state, [self::STATE_PULLING, self::STATE_UPDATING_DATABASE], true); $previous = Cache::get(self::stateCacheKey($server)); $failedStep ??= in_array($previous, [self::STATE_PULLING, self::STATE_UPDATING_DATABASE], true) ? $previous : null; if ($state === self::STATE_FAILED && $failedStep !== null) { Cache::put(self::failedStepCacheKey($server), $failedStep, now()->addDays(7)); } elseif ($running) { Cache::forget(self::failedStepCacheKey($server)); } Cache::put(self::stateCacheKey($server), $state, $running ? now()->addHour() : now()->addDays(7)); ImageScanUpdated::dispatch($server->team_id, $server->uuid, self::SCAN_UUID); } }