Merge claude/1

This commit is contained in:
librelad 2026-07-16 16:47:07 +01:00
commit da049ca03e
5 changed files with 172 additions and 12 deletions

View File

@ -407,8 +407,17 @@ class OverviewManager {
});
}
// Security (needs scan data — without it the updates row already says so)
if (scanned) {
// Security. The CVE scanner (Trivy) may be installed but still downloading
// its vulnerability DB — surface that pending state instead of silently
// omitting the row (which would read as "no CVEs").
const scannerState = up && up.scannerState ? up.scannerState() : null;
if (scannerState === 'db_updating') {
rows.push({
hue: 'verify', icon: '🛡️', kind: 'none',
text: 'Security — building CVE database',
sub: 'Trivy is downloading its vulnerability database; results appear here shortly.',
});
} else if (scanned) {
const sev = u.cveTotals || {};
if (u.totalCves) {
const hit = up.apps.filter((a) => (a.cves || []).length).length;

View File

@ -183,6 +183,16 @@ class UpdaterPage {
return null;
}
// The CVE scanner's live state, stamped on cves.json by the updater generator:
// 'ready' — Trivy's vulnerability DB is present; results are real
// 'db_updating' — Trivy is installed but still downloading its DB (no
// results yet — do NOT paint a green all-clear)
// 'absent' — Trivy isn't installed/running (CVE scanning unavailable)
// null before the first scan file exists.
scannerState() {
return (this.cves && this.cves.scanner && this.cves.scanner.state) || null;
}
// ---- derived counts ------------------------------------------------------
counts() {
@ -288,6 +298,17 @@ class UpdaterPage {
const c = this.counts();
const sev = c.cveTotals;
const checked = c.lastChecked ? this.fmtRel(c.lastChecked) : 'never';
const st = this.scannerState();
// The CVE card's sub-line reflects the scanner state so an empty count reads
// honestly (updating vs not-installed vs genuinely clean).
const cveSub = st === 'db_updating' ? 'database updating…'
: st === 'absent' ? 'scanner not installed'
: (sev.critical || sev.high ? `${sev.critical || 0} critical · ${sev.high || 0} high` : 'no high-severity issues');
const scanHint = st === 'db_updating'
? `<div class="updater-hint">🛡️ Trivy is installed and downloading its vulnerability database — CVE results will fill in here automatically shortly.</div>`
: (st === 'absent'
? `<div class="updater-hint">🛡️ CVE scanning is off — add <strong>Trivy</strong> from the App Center to see vulnerabilities here.</div>`
: '');
const card = (hue, big, label, sub, action) => `
<div class="updater-stat" style="--page: var(--page-${hue}); --page-rgb: var(--page-${hue}-rgb);">
<div class="updater-stat-big">${big}</div>
@ -299,8 +320,7 @@ class UpdaterPage {
<div class="updater-stat-grid">
${card('updates', c.updatesAvailable, 'Updates available', c.updatesAvailable ? 'across your apps' : "you're current",
`<button class="updater-btn updater-btn-primary" data-updater-action="goto" data-tab="updates">Review</button>`)}
${card('verify', c.totalCves, 'Known CVEs',
sev.critical || sev.high ? `${sev.critical || 0} critical · ${sev.high || 0} high` : 'no high-severity issues',
${card('verify', c.totalCves, 'Known CVEs', cveSub,
`<button class="updater-btn" data-updater-action="goto" data-tab="security">View</button>`)}
${card('setup', c.improvements, 'Improvements', c.improvements ? 'signed hotfixes to apply' : 'nothing pending',
`<button class="updater-btn" data-updater-action="goto" data-tab="improvements">View</button>`)}
@ -309,7 +329,8 @@ class UpdaterPage {
${card('system', c.apps, 'Apps tracked', `last scan: ${checked}`,
`<button class="updater-btn" data-updater-action="check">Check now</button>`)}
</div>
${this.updates ? '' : `<div class="updater-hint">No scan data yet — showing your installed apps. The first automatic scan runs within a couple of minutes, or hit <strong>Check now</strong>.</div>`}`;
${this.updates ? '' : `<div class="updater-hint">No scan data yet — showing your installed apps. The first automatic scan runs within a couple of minutes, or hit <strong>Check now</strong>.</div>`}
${scanHint}`;
}
renderUpdates() {
@ -379,6 +400,18 @@ class UpdaterPage {
renderSecurity() {
const withCves = this.apps.filter(a => (a.cves || []).length);
// Scanner-state gates come first: an empty CVE list means very different
// things depending on whether the scanner has even run yet. Painting a green
// "no vulnerabilities" all-clear while Trivy's DB is still downloading would
// be a false all-clear, so that state gets its own loading UI.
const st = this.scannerState();
if (st === 'db_updating') {
const msg = 'Trivy is installed and downloading its vulnerability database. CVE results appear here automatically once it finishes — usually a minute or two.';
return (window.lpLoadingBox && window.lpLoadingBox(msg)) || this.empty(msg);
}
if (st === 'absent') {
return this.empty('CVE scanning isnt set up yet. Add Trivy (the vulnerability scanner) from the App Center to check your app images for known CVEs — it scans entirely on your box.');
}
// No inline Check button: the host auto-scan runs the vulnerability scan on
// its own within a couple of minutes (and the embedding header carries a
// manual Check), so the message alone is the right button-free empty UI.

View File

@ -0,0 +1,74 @@
#!/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 '[]'
}

View File

@ -891,6 +891,9 @@ declare -gA LP_FN_MAP=(
[traefikSetupLoginCredentials]="network/traefik/traefik_login_credentials.sh"
[traefikUpdateWhitelist]="network/traefik/traefik_whitelist.sh"
[trilium_install_post_start]="trilium/scripts/trilium_install_hooks.sh"
[trivyDbUpdatedAt]="trivy/scripts/trivy_scan.sh"
[trivyScanImageCves]="trivy/scripts/trivy_scan.sh"
[trivyScannerState]="trivy/scripts/trivy_scan.sh"
[unbound_install_post_compose]="unbound/scripts/unbound_install_hooks.sh"
[uninstallCrowdsec]="crowdsec/crowdsec.sh"
[uninstallDockerRootless]="docker/install/rootless/rootless_uninstall.sh"
@ -1873,6 +1876,9 @@ declare -gA LP_FN_ROOT=(
[traefikSetupLoginCredentials]="scripts"
[traefikUpdateWhitelist]="scripts"
[trilium_install_post_start]="containers"
[trivyDbUpdatedAt]="containers"
[trivyScanImageCves]="containers"
[trivyScannerState]="containers"
[unbound_install_post_compose]="containers"
[uninstallCrowdsec]="containers"
[uninstallDockerRootless]="scripts"
@ -2888,6 +2894,9 @@ traefikSetupLabelsMiddlewares() { unset -f traefikSetupLabelsMiddlewares; __lpAu
traefikSetupLoginCredentials() { unset -f traefikSetupLoginCredentials; __lpAutoload "${install_scripts_dir}network/traefik/traefik_login_credentials.sh"; traefikSetupLoginCredentials "$@"; }
traefikUpdateWhitelist() { unset -f traefikUpdateWhitelist; __lpAutoload "${install_scripts_dir}network/traefik/traefik_whitelist.sh"; traefikUpdateWhitelist "$@"; }
trilium_install_post_start() { unset -f trilium_install_post_start; __lpAutoload "${install_containers_dir}trilium/scripts/trilium_install_hooks.sh"; trilium_install_post_start "$@"; }
trivyDbUpdatedAt() { unset -f trivyDbUpdatedAt; __lpAutoload "${install_containers_dir}trivy/scripts/trivy_scan.sh"; trivyDbUpdatedAt "$@"; }
trivyScanImageCves() { unset -f trivyScanImageCves; __lpAutoload "${install_containers_dir}trivy/scripts/trivy_scan.sh"; trivyScanImageCves "$@"; }
trivyScannerState() { unset -f trivyScannerState; __lpAutoload "${install_containers_dir}trivy/scripts/trivy_scan.sh"; trivyScannerState "$@"; }
unbound_install_post_compose() { unset -f unbound_install_post_compose; __lpAutoload "${install_containers_dir}unbound/scripts/unbound_install_hooks.sh"; unbound_install_post_compose "$@"; }
uninstallCrowdsec() { unset -f uninstallCrowdsec; __lpAutoload "${install_containers_dir}crowdsec/crowdsec.sh"; uninstallCrowdsec "$@"; }
uninstallDockerRootless() { unset -f uninstallDockerRootless; __lpAutoload "${install_scripts_dir}docker/install/rootless/rootless_uninstall.sh"; uninstallDockerRootless "$@"; }

View File

@ -79,14 +79,49 @@ EOF
runFileWrite "$out_dir/updates.json" < "$tmp"
rm -f "$tmp"
# CVE data — pluggable. Wire trivy/grype per image here and emit per-app
# cves[]. Honest empty-but-valid default until a scanner is configured.
if [ ! -f "$out_dir/cves.json" ]; then
local ctmp; ctmp="$(mktemp)"
printf '{ "generated_at": "%s", "apps": [], "totals": { "critical": 0, "high": 0, "medium": 0, "low": 0 } }\n' "$now" > "$ctmp"
runFileWrite "$out_dir/cves.json" < "$ctmp"
rm -f "$ctmp"
# CVE data via Trivy (containers/trivy/scripts/trivy_scan.sh). Trivy runs as
# a server whose vulnerability DB downloads on first boot; until it lands we
# must NOT emit a green empty all-clear. We stamp cves.json with the scanner
# STATE the WebUI branches on (absent | db_updating | ready) and only run the
# real per-image scans once the DB is ready. Always rewritten so the state
# tracks the live scanner. Honest empty-but-valid on any gap.
local scanner_state="absent" db_updated=""
if declare -f trivyScannerState >/dev/null 2>&1; then
scanner_state="$(trivyScannerState 2>/dev/null || echo absent)"
[ "$scanner_state" = "ready" ] && db_updated="$(trivyDbUpdatedAt 2>/dev/null)"
fi
local cve_apps="" cfirst=1 tc=0 th=0 tm=0 tl=0
if [ "$scanner_state" = "ready" ] && command -v jq >/dev/null 2>&1 \
&& declare -f trivyScanImageCves >/dev/null 2>&1; then
for app in "${apps[@]}"; do
local cimg="" ccompose="$containers_dir/$app/docker-compose.yml"
[ -f "$ccompose" ] && cimg="$(grep -m1 -E '^\s*image:' "$ccompose" 2>/dev/null | sed -E 's/^\s*image:\s*//; s/["'"'"']//g')"
[ -n "$cimg" ] || continue
local cvej; cvej="$(trivyScanImageCves "$cimg")"
[ -z "$cvej" ] || [ "$cvej" = "[]" ] && continue
tc=$((tc + $(printf '%s' "$cvej" | jq '[.[]|select(.severity=="critical")]|length' 2>/dev/null || echo 0)))
th=$((th + $(printf '%s' "$cvej" | jq '[.[]|select(.severity=="high")]|length' 2>/dev/null || echo 0)))
tm=$((tm + $(printf '%s' "$cvej" | jq '[.[]|select(.severity=="medium")]|length' 2>/dev/null || echo 0)))
tl=$((tl + $(printf '%s' "$cvej" | jq '[.[]|select(.severity=="low")]|length' 2>/dev/null || echo 0)))
[ $cfirst -eq 0 ] && cve_apps+=","
cfirst=0
cve_apps+=$(printf '\n { "name": "%s", "displayName": "%s", "cves": %s }' \
"$(printf '%s' "$app" | sed 's/"/\\"/g')" \
"$(printf '%s' "$app" | sed 's/"/\\"/g')" "$cvej")
done
fi
local ctmp; ctmp="$(mktemp)"
cat > "$ctmp" <<EOF
{ "generated_at": "$now",
"scanner": { "name": "trivy", "state": "$scanner_state", "db_updated_at": "$db_updated" },
"apps": [${cve_apps}
],
"totals": { "critical": $tc, "high": $th, "medium": $tm, "low": $tl } }
EOF
runFileWrite "$out_dir/cves.json" < "$ctmp"
rm -f "$ctmp"
# Ensure a valid (possibly empty) history file exists for the WebUI.
if [ ! -f "$out_dir/history.json" ]; then
local htmp; htmp="$(mktemp)"