fix(updater): honest CVE framing — key actionability on a real update
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) <noreply@anthropic.com>
This commit is contained in:
parent
59098667a1
commit
d72f84a9cf
@ -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; }
|
||||
|
||||
|
||||
@ -226,7 +226,7 @@ class UpdaterPage {
|
||||
const origin = this.cveOrigin(c);
|
||||
const ver = this.cveHasFix(c)
|
||||
? `<span class="updater-cve-ver">${this.escape(c.installed || '?')} <span class="updater-arrow">→</span> <strong>${this.escape(c.fixed_in)}</strong></span>`
|
||||
: `<span class="updater-cve-ver updater-cve-ver-none">no fix yet</span>`;
|
||||
: `<span class="updater-cve-ver updater-cve-ver-none">no patch</span>`;
|
||||
return `<div class="updater-cve sev-${sev}">
|
||||
<span class="updater-cve-sev">${this.escape((c.severity || '').toUpperCase())}</span>
|
||||
<a class="updater-cve-id" href="${this.escape(c.url || ('https://nvd.nist.gov/vuln/detail/' + (c.id || '')))}" target="_blank" rel="noopener">${this.escape(c.id || 'CVE')}</a>
|
||||
@ -236,16 +236,20 @@ class UpdaterPage {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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
|
||||
? `<div class="updater-cve-group${mod}"><div class="updater-cve-group-head">${title}<span class="updater-cve-group-n">${rows.length}</span>${
|
||||
hint ? `<span class="updater-cve-group-hint">${hint}</span>` : ''}</div>${rows.map((c) => this.cveRow(c)).join('')}</div>`
|
||||
? `<div class="updater-cve-group${mod}"><div class="updater-cve-group-head"><span class="updater-cve-group-title">${title}</span>${
|
||||
hint ? `<span class="updater-cve-group-hint">${hint}</span>` : ''}<span class="updater-cve-group-n">${rows.length}</span></div>${
|
||||
rows.map((c) => this.cveRow(c)).join('')}</div>`
|
||||
: '';
|
||||
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 `<div class="updater-cve-scroll${list.length > 6 ? ' is-scrollable' : ''}">${body}</div>`;
|
||||
}
|
||||
|
||||
@ -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 =>
|
||||
`<div class="updater-cve-app"><div class="updater-cve-app-name">${this.escape(a.displayName)} <span class="updater-badge sev-${a.worstSeverity}">${(a.cves || []).length}</span></div>${this.renderCveList(a.cves)}</div>`
|
||||
`<div class="updater-cve-app"><div class="updater-cve-app-name">${this.escape(a.displayName)} <span class="updater-badge sev-${a.worstSeverity}">${(a.cves || []).length}</span></div>${this.renderCveList(a.cves, { updateAvailable: a.update_available })}</div>`
|
||||
).join('');
|
||||
return `<div class="updater-list">${blocks}</div>`;
|
||||
}
|
||||
@ -539,17 +543,22 @@ class UpdaterPage {
|
||||
<div class="updater-detail-row">${badge} <span class="updater-row-ver">${cur}${avail ? ` <span class="updater-arrow">→</span> <strong>${avail}</strong>` : ''}</span></div></div>`;
|
||||
}
|
||||
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
|
||||
? `<p class="updater-cve-explain">Vulnerabilities in the packages bundled inside this app's image — not ${appLabel}${verLabel} itself.</p>
|
||||
<p class="updater-cve-tally"><span class="updater-cve-tally-fix">${fixN} with a fix available</span> · <span class="updater-cve-tally-none">${noFixN} no fix yet</span></p>`
|
||||
<p class="updater-cve-status">${statusLine}</p>`
|
||||
: '';
|
||||
const security = `<div class="updater-detail-section"><h4>Security${
|
||||
cves.length ? ` <span class="updater-cve-count">${cves.length}</span>` : ''}</h4>${
|
||||
cves.length ? `${secIntro}<div class="updater-cve-box">${this.renderCveList(cves)}</div>` : '<p class="updater-detail-empty">No known CVEs. 🎉</p>'}</div>`;
|
||||
cves.length ? `${secIntro}<div class="updater-cve-box">${this.renderCveList(cves, { updateAvailable: a.update_available })}</div>` : '<p class="updater-detail-empty">No known CVEs. 🎉</p>'}</div>`;
|
||||
|
||||
// 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user