}>, ignores: list, servers: list} $digest */ public function __construct(public array $digest) { $this->onQueue('high'); $this->teamUrl = rtrim(base_url(), '/').'/vulnerabilities'; } public function via(object $notifiable): array { return $notifiable->getEnabledChannels('image_vulnerabilities'); } public function toMail(?object $notifiable = null): MailMessage { $budget = self::MAIL_ID_LIMIT; $resources = array_map(function (array $resource) use (&$budget): array { $why = array_slice($resource['why'], 0, max(0, $budget)); $budget -= count($why); return [ 'name' => NotificationText::mailMarkdown($resource['name']), 'url' => $resource['url'], 'summary' => NotificationText::mailMarkdown($this->resourceSentence($resource)), 'why' => array_map(NotificationText::mailWhy(...), $why), ]; }, $this->digest['resources']); $ignores = array_map(function (array $ignore) use (&$budget): array { $id = $ignore['id']; if ($id !== null && $budget > 0) { $budget--; } elseif ($id !== null) { $id = 'A vulnerability'; } $subject = match (true) { $id !== null && $ignore['package'] !== null => "{$id} in {$ignore['package']}", $id !== null => $id, default => "Package {$ignore['package']}", }; return [ 'subject' => NotificationText::mailMarkdown($subject), 'resource' => $ignore['resource'] === null ? null : NotificationText::mailMarkdown($ignore['resource']), 'expires_at' => $ignore['expires_at'], ]; }, $this->digest['ignores']); return (new MailMessage) ->subject('Coolify: '.self::TITLE) ->view('emails.image-vulnerabilities', [ 'intro' => 'Changes in the security status of your resources since the last digest.', 'resources' => $resources, 'ignores' => $ignores, 'servers' => array_map(fn (array $server): array => ['name' => NotificationText::mailMarkdown($server['name']), 'url' => $server['url']], $this->digest['servers']), 'url' => $this->teamUrl, ]); } public function toDiscord(): DiscordMessage { $suffix = "\n\n[".self::BUTTON."]({$this->teamUrl})"; $lines = $this->chatLines( fn (array $resource): string => '• '.($resource['url'] === null ? NotificationText::discord($this->name($resource)) : '['.NotificationText::discord($this->name($resource)).']('.$resource['url'].')') .': '.NotificationText::discord($this->resourceSentence($resource)), NotificationText::discord(...), self::DISCORD_LIMIT - mb_strlen($suffix), ); return new DiscordMessage( title: self::TITLE, description: implode("\n", $lines).$suffix, color: DiscordMessage::warningColor(), ); } public function toSlack(): SlackMessage { $suffix = "\n\n<{$this->teamUrl}|".self::BUTTON.'>'; $lines = $this->chatLines( fn (array $resource): string => '• '.($resource['url'] === null ? NotificationText::slack($this->name($resource)) : '<'.$resource['url'].'|'.NotificationText::slack($this->name($resource)).'>') .': '.NotificationText::slack($this->resourceSentence($resource)), NotificationText::slack(...), self::SLACK_LIMIT - mb_strlen($suffix), ); return new SlackMessage( title: self::TITLE, description: implode("\n", $lines).$suffix, color: SlackMessage::warningColor(), ); } public function toTelegram(): array { $prefix = 'Coolify: '.self::TITLE."\n\n"; $lines = $this->chatLines( fn (array $resource): string => '• '.NotificationText::plain($this->name($resource)).': '.NotificationText::plain($this->resourceSentence($resource)) .($resource['url'] === null ? '' : "\n ".$resource['url']), NotificationText::plain(...), self::TELEGRAM_LIMIT - mb_strlen($prefix), ); return [ 'message' => $prefix.implode("\n", $lines), 'buttons' => [['text' => self::BUTTON, 'url' => $this->teamUrl]], ]; } public function toPushover(): PushoverMessage { $button = ['text' => self::BUTTON, 'url' => $this->teamUrl]; // PushoverMessage appends each button to the message as a link. $buttonLength = mb_strlen(" {$button['text']}"); $lines = $this->chatLines( fn (array $resource): string => '• '.($resource['url'] === null ? NotificationText::html($this->name($resource)) : "".NotificationText::html($this->name($resource)).'') .': '.NotificationText::html($this->resourceSentence($resource)), NotificationText::html(...), self::PUSHOVER_LIMIT - $buttonLength, ); return new PushoverMessage( title: self::TITLE, message: implode("\n", $lines), buttons: [$button], level: 'warning', ); } public function toWebhook(): array { return [ 'success' => false, 'message' => self::TITLE, 'event' => 'image_vulnerabilities_digest', 'url' => $this->teamUrl, 'resources' => array_map(fn (array $resource): array => [ 'name' => $resource['name'], 'uuid' => $resource['uuid'], 'url' => $resource['url'], 'status' => $resource['status'], 'action' => $resource['action'], ], $this->digest['resources']), 'ignores_ending' => $this->digest['ignores'] !== [], 'servers_failing' => array_map(fn (array $server): array => ['name' => $server['name'], 'url' => $server['url']], $this->digest['servers']), ]; } /** * The lines of a chat message that fit in $budget characters: resources, then ignores and failing servers, * without IDs or counts, then "And N more." for the rest. * * @param callable(array): string $resourceLine * @param callable(string): string $escape * @return list */ private function chatLines(callable $resourceLine, callable $escape, int $budget): array { $lines = array_map($resourceLine, $this->digest['resources']); $ignores = count($this->digest['ignores']); if ($ignores > 0) { $lines[] = $ignores === 1 ? 'An ignore ends within 7 days.' : 'Some ignores end within 7 days.'; } foreach ($this->digest['servers'] as $server) { $lines[] = $escape('Image scans failed for 2 days on '.NotificationText::limit($server['name'], self::NAME_LIMIT).'.'); } return NotificationText::fitLines($lines, $budget); } /** * @param array $resource */ private function name(array $resource): string { return NotificationText::limit($resource['name'], self::NAME_LIMIT); } /** * "Security update. Update to 1.28." / "Check needed. Check again: the last check was 3 days ago." * * @param array $resource */ private function resourceSentence(array $resource): string { return NotificationText::limit(trim($resource['badge'].'. '.($resource['action'] ?? '')), self::SENTENCE_LIMIT); } }