diff --git a/containers/libreportal/frontend/components/updater/css/updater.css b/containers/libreportal/frontend/components/updater/css/updater.css index c1ac38f..f353072 100644 --- a/containers/libreportal/frontend/components/updater/css/updater.css +++ b/containers/libreportal/frontend/components/updater/css/updater.css @@ -86,7 +86,16 @@ .updater-cve-id { font-family: var(--font-mono); color: rgb(var(--page-rgb, var(--accent-rgb))); text-decoration: none; } .updater-cve-id:hover { text-decoration: underline; } .updater-cve-pkg { color: rgba(var(--text-rgb), 0.6); } -.updater-cve-fix { margin-left: auto; color: rgba(var(--page-verify-rgb), 0.9); font-size: 0.76rem; } +.updater-cve-origin { + font-size: 0.64rem; font-weight: 600; letter-spacing: .02em; white-space: nowrap; + padding: 1px 7px; border-radius: 999px; + color: rgba(var(--text-rgb), 0.7); + background: rgba(var(--text-rgb), 0.07); + border: 1px solid rgba(var(--text-rgb), 0.10); +} +.updater-cve-ver { margin-left: auto; color: rgba(var(--page-verify-rgb), 0.95); font-size: 0.76rem; white-space: nowrap; } +.updater-cve-ver .updater-arrow { color: rgba(var(--text-rgb), 0.4); } +.updater-cve-ver-none { color: rgba(var(--text-rgb), 0.4); font-style: italic; } /* Security CVEs live in their own inset dark panel so the list reads as a contained block with breathing room on both sides — matching the app rows @@ -98,7 +107,32 @@ background: rgba(10, 16, 32, 0.45); border: 1px solid rgba(var(--text-rgb), 0.10); } -.updater-cve-box .updater-cve:first-child { border-top: 0; } + +/* Explainer + tally above the CVE box: name what these CVEs actually are + (image dependencies, not the app version) and triage them at a glance. */ +.updater-cve-explain { margin: 0 0 6px; font-size: 0.8rem; color: rgba(var(--text-rgb), 0.6); } +.updater-cve-tally { margin: 0 0 10px; font-size: 0.8rem; color: rgba(var(--text-rgb), 0.5); } +.updater-cve-tally-fix { color: rgba(var(--page-verify-rgb), 0.95); font-weight: 600; } +.updater-cve-tally-none { color: rgba(var(--text-rgb), 0.55); } + +/* Triage groups inside the box: "Fix available" leads, the dimmed "No fix yet" + pile follows so the actionable CVEs are what the eye lands on first. */ +.updater-cve-group + .updater-cve-group { margin-top: 2px; } +.updater-cve-group-head { + display: flex; align-items: center; gap: 8px; + padding: 10px 0 6px; + font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; + color: rgba(var(--text-rgb), 0.72); +} +.updater-cve-group-n { + font-variant-numeric: tabular-nums; font-weight: 700; font-size: 0.64rem; + padding: 1px 7px; border-radius: 999px; + background: rgba(var(--page-verify-rgb), 0.18); color: rgb(var(--page-verify-rgb)); +} +.updater-cve-group-hint { text-transform: none; letter-spacing: 0; font-weight: 500; font-size: 0.72rem; color: rgba(var(--text-rgb), 0.45); } +.updater-cve-group.is-nofix .updater-cve-group-n { background: rgba(var(--text-rgb), 0.10); color: rgba(var(--text-rgb), 0.6); } +.updater-cve-group.is-nofix .updater-cve { opacity: 0.72; } +.updater-cve-group .updater-cve:first-of-type { border-top: 0; } /* Count pill next to the "Security" detail heading (e.g. "Security 28"). */ .updater-cve-count { diff --git a/containers/libreportal/frontend/components/updater/js/updater-page.js b/containers/libreportal/frontend/components/updater/js/updater-page.js index 0f03fcc..f34c8b6 100644 --- a/containers/libreportal/frontend/components/updater/js/updater-page.js +++ b/containers/libreportal/frontend/components/updater/js/updater-page.js @@ -192,16 +192,61 @@ class UpdaterPage { // Sorted worst-first (so the most severe are visible before any scroll) and // wrapped in a height-capped scroll box once the list is long, so an app with // dozens of CVEs (e.g. 28) stays compact instead of pushing the page down. - renderCveList(cves) { - const list = [...(cves || [])].sort((x, y) => this.sevRank(x.severity) - this.sevRank(y.severity)); - const items = list.map(c => ` -
Vulnerabilities in the packages bundled inside this app's image — not ${appLabel}${verLabel} itself.
+${fixN} with a fix available · ${noFixN} no fix yet
` + : ''; const security = `No known CVEs. 🎉
'}No known CVEs. 🎉
'}`; // A rollback target exists if a snapshot field is present (future-proofing) // OR — the data the generator actually emits today — this app has a prior diff --git a/containers/trivy/scripts/trivy_scan.sh b/containers/trivy/scripts/trivy_scan.sh index ad1a205..a09c242 100644 --- a/containers/trivy/scripts/trivy_scan.sh +++ b/containers/trivy/scripts/trivy_scan.sh @@ -73,15 +73,27 @@ trivyScanImageCves() { 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 // [])[] | (.Vulnerabilities // [])[] | { - id: .VulnerabilityID, - severity: ((.Severity // "UNKNOWN") | ascii_downcase), - package: .PkgName, - installed: (.InstalledVersion // ""), - fixed_in: (.FixedVersion // ""), - url: (.PrimaryURL // "") - } ] + [ (.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 '[]' }