From d72f84a9cf75c33bc98c167280befc89f1e751d8 Mon Sep 17 00:00:00 2001 From: librelad Date: Sat, 18 Jul 2026 23:34:20 +0100 Subject: [PATCH] =?UTF-8?q?fix(updater):=20honest=20CVE=20framing=20?= =?UTF-8?q?=E2=80=94=20key=20actionability=20on=20a=20real=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Security triage implied 'fix available · update the app to clear these' even when the app was already on the newest image — where updating does nothing and the patch only lands when the maintainer rebuilds. That read as a false to-do. Reframe honestly: - Status line keyed on update_available, not Trivy's fixed_in: up to date => 'nothing to apply; clears when the maintainer ships a rebuilt image'; update available => 'updating may pull in patched packages'. - Groups relabeled to describe reality: 'Patch released upstream' (hint: lands on rebuild / may be cleared by updating) and 'No patch yet'. Counts go neutral so a big number doesn't read as either alarm or all-clear. - Row 'no fix yet' -> 'no patch'; use getAppDisplayName so it's 'Trivy' not 'trivy'. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/updater/css/updater.css | 28 +++++++++------- .../components/updater/js/updater-page.js | 33 ++++++++++++------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/containers/libreportal/frontend/components/updater/css/updater.css b/containers/libreportal/frontend/components/updater/css/updater.css index f353072..0d49ad0 100644 --- a/containers/libreportal/frontend/components/updater/css/updater.css +++ b/containers/libreportal/frontend/components/updater/css/updater.css @@ -108,29 +108,33 @@ border: 1px solid rgba(var(--text-rgb), 0.10); } -/* 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); } +/* Explainer + status above the CVE box: name what these CVEs actually are + (image dependencies, not the app version) and tell the user honestly whether + there's anything they can do — keyed on a real app update, not a package + patch that only lands when the maintainer rebuilds. */ +.updater-cve-explain { margin: 0 0 5px; font-size: 0.8rem; color: rgba(var(--text-rgb), 0.6); } +.updater-cve-status { margin: 0 0 10px; font-size: 0.8rem; 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. */ +/* Triage groups inside the box: "Patch released upstream" leads, the dimmed + "No patch yet" pile follows. The count sits at the row's right edge. */ .updater-cve-group + .updater-cve-group { margin-top: 2px; } .updater-cve-group-head { - display: flex; align-items: center; gap: 8px; + display: flex; align-items: center; gap: 6px; padding: 10px 0 6px; +} +.updater-cve-group-title { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: rgba(var(--text-rgb), 0.72); } +.updater-cve-group-hint { font-size: 0.72rem; font-weight: 500; color: rgba(var(--text-rgb), 0.45); } +.updater-cve-group-hint::before { content: '·'; margin-right: 6px; opacity: 0.55; } .updater-cve-group-n { + margin-left: auto; 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)); + background: rgba(var(--text-rgb), 0.12); color: rgba(var(--text-rgb), 0.7); } -.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-group-n { background: rgba(var(--text-rgb), 0.08); color: rgba(var(--text-rgb), 0.55); } .updater-cve-group.is-nofix .updater-cve { opacity: 0.72; } .updater-cve-group .updater-cve:first-of-type { border-top: 0; } diff --git a/containers/libreportal/frontend/components/updater/js/updater-page.js b/containers/libreportal/frontend/components/updater/js/updater-page.js index b3ad0b8..2fecc0f 100644 --- a/containers/libreportal/frontend/components/updater/js/updater-page.js +++ b/containers/libreportal/frontend/components/updater/js/updater-page.js @@ -226,7 +226,7 @@ class UpdaterPage { const origin = this.cveOrigin(c); const ver = this.cveHasFix(c) ? `${this.escape(c.installed || '?')} ${this.escape(c.fixed_in)}` - : `no fix yet`; + : `no patch`; return `
${this.escape((c.severity || '').toUpperCase())} ${this.escape(c.id || 'CVE')} @@ -236,16 +236,20 @@ class UpdaterPage {
`; } - renderCveList(cves) { + renderCveList(cves, opts = {}) { const list = [...(cves || [])].sort((x, y) => this.sevRank(x.severity) - this.sevRank(y.severity)); const fixable = list.filter((c) => this.cveHasFix(c)); const noFix = list.filter((c) => !this.cveHasFix(c)); + // The "patched upstream" pile is only user-actionable when an app update + // exists — otherwise the patch just waits for the maintainer's rebuild. + const fixHint = opts.updateAvailable ? 'may be cleared by updating' : 'lands when the image is rebuilt'; const group = (title, hint, mod, rows) => rows.length - ? `
${title}${rows.length}${ - hint ? `${hint}` : ''}
${rows.map((c) => this.cveRow(c)).join('')}
` + ? `
${title}${ + hint ? `${hint}` : ''}${rows.length}
${ + rows.map((c) => this.cveRow(c)).join('')}
` : ''; - const body = group('Fix available', 'update the app to clear these', '', fixable) - + group('No fix yet', 'waiting on an upstream patch', ' is-nofix', noFix); + const body = group('Patch released upstream', fixHint, '', fixable) + + group('No patch yet', 'waiting on upstream', ' is-nofix', noFix); return `
${body}
`; } @@ -484,7 +488,7 @@ class UpdaterPage { if (!this.cves) return this.empty('No vulnerability scan yet — one runs automatically within a couple of minutes.'); if (!withCves.length) return this.empty('No known vulnerabilities in your installed apps. 🎉'); const blocks = withCves.map(a => - `
${this.escape(a.displayName)} ${(a.cves || []).length}
${this.renderCveList(a.cves)}
` + `
${this.escape(a.displayName)} ${(a.cves || []).length}
${this.renderCveList(a.cves, { updateAvailable: a.update_available })}
` ).join(''); return `
${blocks}
`; } @@ -539,17 +543,22 @@ class UpdaterPage {
${badge} ${cur}${avail ? ` ${avail}` : ''}
`; } const cves = a.cves || []; - const fixN = cves.filter((c) => this.cveHasFix(c)).length; - const noFixN = cves.length - fixN; - const appLabel = this.escape(a.displayName || a.name || 'the app'); + const appLabel = this.escape((window.getAppDisplayName ? window.getAppDisplayName(a.name) : null) || a.displayName || a.name || 'the app'); const verLabel = a.current_version ? ` ${this.escape(a.current_version)}` : ''; + // Frame actionability by whether an app update actually EXISTS — not by + // Trivy's fixed_in. An upstream package patch does the user no good while + // they're already on the newest image; it only lands when the maintainer + // rebuilds. Telling them to "update to clear these" in that state is a lie. + const statusLine = a.update_available + ? 'An update is available — installing it (a snapshot is taken first) may pull in patched packages.' + : `You're on the latest published version, so there's nothing to apply. These clear when ${appLabel}'s maintainer ships a rebuilt image.`; const secIntro = cves.length ? `

Vulnerabilities in the packages bundled inside this app's image — not ${appLabel}${verLabel} itself.

-

${fixN} with a fix available · ${noFixN} no fix yet

` +

${statusLine}

` : ''; const security = `

Security${ cves.length ? ` ${cves.length}` : ''}

${ - cves.length ? `${secIntro}
${this.renderCveList(cves)}
` : '

No known CVEs. 🎉

'}
`; + cves.length ? `${secIntro}
${this.renderCveList(cves, { updateAvailable: a.update_available })}
` : '

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