Merge claude/1
This commit is contained in:
commit
da049ca03e
@ -407,8 +407,17 @@ class OverviewManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Security (needs scan data — without it the updates row already says so)
|
// Security. The CVE scanner (Trivy) may be installed but still downloading
|
||||||
if (scanned) {
|
// 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 || {};
|
const sev = u.cveTotals || {};
|
||||||
if (u.totalCves) {
|
if (u.totalCves) {
|
||||||
const hit = up.apps.filter((a) => (a.cves || []).length).length;
|
const hit = up.apps.filter((a) => (a.cves || []).length).length;
|
||||||
|
|||||||
@ -183,6 +183,16 @@ class UpdaterPage {
|
|||||||
return null;
|
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 ------------------------------------------------------
|
// ---- derived counts ------------------------------------------------------
|
||||||
|
|
||||||
counts() {
|
counts() {
|
||||||
@ -288,6 +298,17 @@ class UpdaterPage {
|
|||||||
const c = this.counts();
|
const c = this.counts();
|
||||||
const sev = c.cveTotals;
|
const sev = c.cveTotals;
|
||||||
const checked = c.lastChecked ? this.fmtRel(c.lastChecked) : 'never';
|
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) => `
|
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" style="--page: var(--page-${hue}); --page-rgb: var(--page-${hue}-rgb);">
|
||||||
<div class="updater-stat-big">${big}</div>
|
<div class="updater-stat-big">${big}</div>
|
||||||
@ -299,8 +320,7 @@ class UpdaterPage {
|
|||||||
<div class="updater-stat-grid">
|
<div class="updater-stat-grid">
|
||||||
${card('updates', c.updatesAvailable, 'Updates available', c.updatesAvailable ? 'across your apps' : "you're current",
|
${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>`)}
|
`<button class="updater-btn updater-btn-primary" data-updater-action="goto" data-tab="updates">Review</button>`)}
|
||||||
${card('verify', c.totalCves, 'Known CVEs',
|
${card('verify', c.totalCves, 'Known CVEs', cveSub,
|
||||||
sev.critical || sev.high ? `${sev.critical || 0} critical · ${sev.high || 0} high` : 'no high-severity issues',
|
|
||||||
`<button class="updater-btn" data-updater-action="goto" data-tab="security">View</button>`)}
|
`<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',
|
${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>`)}
|
`<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}`,
|
${card('system', c.apps, 'Apps tracked', `last scan: ${checked}`,
|
||||||
`<button class="updater-btn" data-updater-action="check">Check now</button>`)}
|
`<button class="updater-btn" data-updater-action="check">Check now</button>`)}
|
||||||
</div>
|
</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() {
|
renderUpdates() {
|
||||||
@ -379,6 +400,18 @@ class UpdaterPage {
|
|||||||
|
|
||||||
renderSecurity() {
|
renderSecurity() {
|
||||||
const withCves = this.apps.filter(a => (a.cves || []).length);
|
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 isn’t 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
|
// 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
|
// 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.
|
// manual Check), so the message alone is the right button-free empty UI.
|
||||||
|
|||||||
74
containers/trivy/scripts/trivy_scan.sh
Normal file
74
containers/trivy/scripts/trivy_scan.sh
Normal 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 '[]'
|
||||||
|
}
|
||||||
@ -891,6 +891,9 @@ declare -gA LP_FN_MAP=(
|
|||||||
[traefikSetupLoginCredentials]="network/traefik/traefik_login_credentials.sh"
|
[traefikSetupLoginCredentials]="network/traefik/traefik_login_credentials.sh"
|
||||||
[traefikUpdateWhitelist]="network/traefik/traefik_whitelist.sh"
|
[traefikUpdateWhitelist]="network/traefik/traefik_whitelist.sh"
|
||||||
[trilium_install_post_start]="trilium/scripts/trilium_install_hooks.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"
|
[unbound_install_post_compose]="unbound/scripts/unbound_install_hooks.sh"
|
||||||
[uninstallCrowdsec]="crowdsec/crowdsec.sh"
|
[uninstallCrowdsec]="crowdsec/crowdsec.sh"
|
||||||
[uninstallDockerRootless]="docker/install/rootless/rootless_uninstall.sh"
|
[uninstallDockerRootless]="docker/install/rootless/rootless_uninstall.sh"
|
||||||
@ -1873,6 +1876,9 @@ declare -gA LP_FN_ROOT=(
|
|||||||
[traefikSetupLoginCredentials]="scripts"
|
[traefikSetupLoginCredentials]="scripts"
|
||||||
[traefikUpdateWhitelist]="scripts"
|
[traefikUpdateWhitelist]="scripts"
|
||||||
[trilium_install_post_start]="containers"
|
[trilium_install_post_start]="containers"
|
||||||
|
[trivyDbUpdatedAt]="containers"
|
||||||
|
[trivyScanImageCves]="containers"
|
||||||
|
[trivyScannerState]="containers"
|
||||||
[unbound_install_post_compose]="containers"
|
[unbound_install_post_compose]="containers"
|
||||||
[uninstallCrowdsec]="containers"
|
[uninstallCrowdsec]="containers"
|
||||||
[uninstallDockerRootless]="scripts"
|
[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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
uninstallDockerRootless() { unset -f uninstallDockerRootless; __lpAutoload "${install_scripts_dir}docker/install/rootless/rootless_uninstall.sh"; uninstallDockerRootless "$@"; }
|
||||||
|
|||||||
@ -79,14 +79,49 @@ EOF
|
|||||||
runFileWrite "$out_dir/updates.json" < "$tmp"
|
runFileWrite "$out_dir/updates.json" < "$tmp"
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
|
|
||||||
# CVE data — pluggable. Wire trivy/grype per image here and emit per-app
|
# CVE data via Trivy (containers/trivy/scripts/trivy_scan.sh). Trivy runs as
|
||||||
# cves[]. Honest empty-but-valid default until a scanner is configured.
|
# a server whose vulnerability DB downloads on first boot; until it lands we
|
||||||
if [ ! -f "$out_dir/cves.json" ]; then
|
# must NOT emit a green empty all-clear. We stamp cves.json with the scanner
|
||||||
local ctmp; ctmp="$(mktemp)"
|
# STATE the WebUI branches on (absent | db_updating | ready) and only run the
|
||||||
printf '{ "generated_at": "%s", "apps": [], "totals": { "critical": 0, "high": 0, "medium": 0, "low": 0 } }\n' "$now" > "$ctmp"
|
# real per-image scans once the DB is ready. Always rewritten so the state
|
||||||
runFileWrite "$out_dir/cves.json" < "$ctmp"
|
# tracks the live scanner. Honest empty-but-valid on any gap.
|
||||||
rm -f "$ctmp"
|
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
|
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.
|
# Ensure a valid (possibly empty) history file exists for the WebUI.
|
||||||
if [ ! -f "$out_dir/history.json" ]; then
|
if [ ! -f "$out_dir/history.json" ]; then
|
||||||
local htmp; htmp="$(mktemp)"
|
local htmp; htmp="$(mktemp)"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user