A user rightly noted the Security section read as a wall of unrelated dependency CVEs against an 'Up to date' app — no cue for what, if anything, to do. Make it answer 'is this my problem, and will updating fix it?': - Scanner (trivy_scan.sh): stop discarding Trivy's Class/Type/Status at the jq flatten — bind them onto each vuln so the UI can tell an OS package from the app's own bundled dependency, and a real fix from a won't-fix. - Security section (updater-page.js): explain these are vulnerabilities in the packages bundled in the image (not the app version), tally 'N with a fix · M no fix yet', then split the list into a 'Fix available' group (worst-first, each row tagged OS/dependency and showing installed -> fixed) and a dimmed 'No fix yet' group. No fabricated 'this update fixes N' claim — fixed_in vs the image tag isn't a reliable join, so we only state fix availability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
100 lines
5.0 KiB
Bash
100 lines
5.0 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 the app's LOCAL image via the docker socket mounted into the
|
|
# container (--image-src docker) so nothing is pulled from a registry — the
|
|
# image is already present, and a privacy box may have no outbound at all.
|
|
# Client mode (--server localhost:4954, the container's fixed --listen port)
|
|
# leaves the vuln DB to the running server; a standalone scan would deadlock
|
|
# on the server's cache lock. For rootless the daemon socket lives under
|
|
# /run/user/<uid> (not trivy's default path), so point DOCKER_HOST at it;
|
|
# rooted's /var/run/docker.sock is found by default.
|
|
local envs=""
|
|
if [ "${CFG_DOCKER_INSTALL_TYPE:-}" = "rootless" ] && [ -n "${docker_install_user:-}" ]; then
|
|
local uid; uid="$(id -u "$docker_install_user" 2>/dev/null)"
|
|
[ -n "$uid" ] && envs="-e DOCKER_HOST=unix:///run/user/$uid/docker.sock"
|
|
fi
|
|
|
|
# --quiet keeps the progress spinner out of stdout; we only want CRITICAL..LOW.
|
|
local raw
|
|
raw="$(dockerCommandRun "docker exec $envs trivy-service trivy image --server http://localhost:4954 --image-src docker --quiet --scanners vuln --format json --severity CRITICAL,HIGH,MEDIUM,LOW '$image'" 2>/dev/null)"
|
|
[ -n "$raw" ] || { echo '[]'; return; }
|
|
|
|
# Carry the enclosing Result's Class/Type down onto each vuln: Class
|
|
# (os-pkgs|lang-pkgs) is what separates an OS-package CVE from an app's own
|
|
# bundled dependency (Go module, etc.), and Status (fixed|will_not_fix|…)
|
|
# separates "a fix exists" from "upstream won't fix" — both drive the
|
|
# triaged Security view. Without binding them before the Vulnerabilities[]
|
|
# flatten they'd be lost.
|
|
printf '%s' "$raw" | jq -c '
|
|
[ (.Results // [])[]
|
|
| (.Class // "") as $class
|
|
| (.Type // "") as $type
|
|
| (.Vulnerabilities // [])[] | {
|
|
id: .VulnerabilityID,
|
|
severity: ((.Severity // "UNKNOWN") | ascii_downcase),
|
|
package: .PkgName,
|
|
installed: (.InstalledVersion // ""),
|
|
fixed_in: (.FixedVersion // ""),
|
|
status: (.Status // ""),
|
|
class: $class,
|
|
type: $type,
|
|
url: (.PrimaryURL // "")
|
|
} ]
|
|
| unique_by(.id + "|" + (.package // ""))
|
|
' 2>/dev/null || echo '[]'
|
|
}
|