librelad abd8e0b68b feat(updater): surface Trivy CVE scanner state (installed / DB updating / ready)
Trivy runs as a server whose vulnerability DB downloads on first boot; until it
lands no scan can produce results. Previously the updater generator wrote an
empty-but-valid cves.json the moment the file was missing, so installing Trivy
painted a green "no known vulnerabilities" all-clear that was actually a lie —
the DB hadn't even downloaded, and the Updates/Security view gave no signal.

Add an honest scanner state the WebUI branches on:
- containers/trivy/scripts/trivy_scan.sh — trivyScannerState (absent |
  db_updating | ready) via `trivy version -f json`, trivyDbUpdatedAt, and
  trivyScanImageCves (per-image scan normalized to {id,severity,package,
  installed,fixed_in,url}, deduped). All degrade safely on error.
- webui_updater_scan.sh — stamp cves.json with scanner.state; only run real
  per-image scans once the DB is ready. Always rewritten so state tracks live.
- updater-page.js — Security tab shows a loading box while the DB updates, an
  install nudge when absent, and the genuine 🎉 only when ready+empty; Overview
  CVE card sub + hint reflect the state.
- overview-manager.js — fleet Security row surfaces the "building CVE database"
  pending state instead of silently omitting.
- function_manifest.sh — regenerated for the new trivy_scan.sh functions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 16:47:07 +01:00

75 lines
3.6 KiB
Bash

#!/bin/bash
# Trivy CVE scanner integration — probed by the WebUI updater generator.
# ---------------------------------------------------------------------------
# Trivy (containers/trivy) runs as a long-lived server whose vulnerability DB
# is cached in ./trivy-cache. On first start — and periodically after — that DB
# is downloaded (tens of MB). Until it lands NO scan can produce results, so the
# WebUI must not paint a green "no known vulnerabilities" all-clear yet.
#
# These helpers expose the scanner's state so the updater generator can write an
# honest cves.json:
# absent — Trivy isn't installed/running (CVE scanning unavailable)
# db_updating — installed, but the vulnerability DB is still downloading
# ready — DB present; real per-image scans can run
# Every helper degrades to the safe answer (absent / [] ) on any error so the
# generator never breaks.
# Echoes: absent | db_updating | ready
trivyScannerState() {
# Not in `docker ps` (never installed, stopped, or still starting) -> absent.
dockerCommandRun "docker ps --format '{{.Names}}'" 2>/dev/null \
| grep -qx trivy-service || { echo absent; return; }
# Running: the DB is ready once `trivy version` reports a VulnerabilityDB
# block (null/absent while it's still being fetched on first boot).
local ver; ver="$(dockerCommandRun "docker exec trivy-service trivy version -f json" 2>/dev/null)"
[ -n "$ver" ] || { echo db_updating; return; }
if command -v jq >/dev/null 2>&1; then
[ -n "$(printf '%s' "$ver" | jq -r '.VulnerabilityDB // empty' 2>/dev/null)" ] \
&& echo ready || echo db_updating
else
# No jq: DownloadedAt only appears once the DB is present (and, unlike a
# nullable VulnerabilityDB key, avoids a false positive when it's null).
printf '%s' "$ver" | grep -q '"DownloadedAt"' \
&& echo ready || echo db_updating
fi
}
# Echoes the DB's UpdatedAt timestamp (ISO8601) if available, else nothing.
trivyDbUpdatedAt() {
command -v jq >/dev/null 2>&1 || return 0
local ver; ver="$(dockerCommandRun "docker exec trivy-service trivy version -f json" 2>/dev/null)"
[ -n "$ver" ] || return 0
printf '%s' "$ver" | jq -r '.VulnerabilityDB.UpdatedAt // empty' 2>/dev/null
}
# trivyScanImageCves <image> — scan one image against the cached DB and echo a
# JSON array of normalized CVE objects for the WebUI:
# [ { id, severity, package, installed, fixed_in, url }, ... ]
# Echoes [] on any failure so a single bad image never aborts the whole scan.
# Requires jq (the generator already guards on it).
trivyScanImageCves() {
local image="$1"
[ -n "$image" ] || { echo '[]'; return; }
command -v jq >/dev/null 2>&1 || { echo '[]'; return; }
# Scan runs inside the server container, reusing its cached DB. --quiet keeps
# the progress spinner out of stdout; we only want CRITICAL..LOW findings.
local raw
raw="$(dockerCommandRun "docker exec trivy-service trivy image --quiet --scanners vuln --format json --severity CRITICAL,HIGH,MEDIUM,LOW '$image'" 2>/dev/null)"
[ -n "$raw" ] || { echo '[]'; return; }
printf '%s' "$raw" | jq -c '
[ (.Results // [])[] | (.Vulnerabilities // [])[] | {
id: .VulnerabilityID,
severity: ((.Severity // "UNKNOWN") | ascii_downcase),
package: .PkgName,
installed: (.InstalledVersion // ""),
fixed_in: (.FixedVersion // ""),
url: (.PrimaryURL // "")
} ]
| unique_by(.id + "|" + (.package // ""))
' 2>/dev/null || echo '[]'
}