Compare commits
2 Commits
b875ee0525
...
6b93554040
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b93554040 | ||
|
|
c7c1601f59 |
@ -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 { 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-id:hover { text-decoration: underline; }
|
||||||
.updater-cve-pkg { color: rgba(var(--text-rgb), 0.6); }
|
.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
|
/* 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
|
contained block with breathing room on both sides — matching the app rows
|
||||||
@ -98,7 +107,32 @@
|
|||||||
background: rgba(10, 16, 32, 0.45);
|
background: rgba(10, 16, 32, 0.45);
|
||||||
border: 1px solid rgba(var(--text-rgb), 0.10);
|
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"). */
|
/* Count pill next to the "Security" detail heading (e.g. "Security 28"). */
|
||||||
.updater-cve-count {
|
.updater-cve-count {
|
||||||
|
|||||||
@ -192,16 +192,61 @@ class UpdaterPage {
|
|||||||
// Sorted worst-first (so the most severe are visible before any scroll) and
|
// 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
|
// 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.
|
// dozens of CVEs (e.g. 28) stays compact instead of pushing the page down.
|
||||||
renderCveList(cves) {
|
// A CVE is actionable when upstream ships a fixed version AND hasn't flagged
|
||||||
const list = [...(cves || [])].sort((x, y) => this.sevRank(x.severity) - this.sevRank(y.severity));
|
// it won't-fix / EOL — those are the ones an image rebuild (i.e. updating the
|
||||||
const items = list.map(c => `
|
// app) can actually clear. The rest are noise the user can only wait on.
|
||||||
<div class="updater-cve sev-${(c.severity || 'low').toLowerCase()}">
|
cveHasFix(c) {
|
||||||
|
const st = ((c && c.status) || '').toLowerCase();
|
||||||
|
return !!(c && c.fixed_in) && st !== 'will_not_fix' && st !== 'end_of_life';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Human label for where a CVE lives: an OS package baked into the base image
|
||||||
|
// vs one of the app's own bundled dependencies (Go module, npm, …). Sourced
|
||||||
|
// from Trivy's Class/Type; empty when the scan predates capturing them (so we
|
||||||
|
// show no tag rather than a wrong one).
|
||||||
|
cveOrigin(c) {
|
||||||
|
const cls = ((c && c.class) || '').toLowerCase();
|
||||||
|
const ty = ((c && c.type) || '').toLowerCase();
|
||||||
|
if (cls === 'os-pkgs') return 'OS package';
|
||||||
|
if (cls === 'lang-pkgs') {
|
||||||
|
if (/go/.test(ty)) return 'Go dependency';
|
||||||
|
if (/(node|npm|yarn|pnpm)/.test(ty)) return 'npm dependency';
|
||||||
|
if (/(python|pip|poetry|conda)/.test(ty)) return 'Python dependency';
|
||||||
|
if (/(gem|bundler|ruby)/.test(ty)) return 'Ruby dependency';
|
||||||
|
if (/(cargo|rust)/.test(ty)) return 'Rust dependency';
|
||||||
|
if (/(jar|pom|gradle|java)/.test(ty)) return 'Java dependency';
|
||||||
|
if (/(composer|php)/.test(ty)) return 'PHP dependency';
|
||||||
|
return 'App dependency';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
cveRow(c) {
|
||||||
|
const sev = (c.severity || 'low').toLowerCase();
|
||||||
|
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>`;
|
||||||
|
return `<div class="updater-cve sev-${sev}">
|
||||||
<span class="updater-cve-sev">${this.escape((c.severity || '').toUpperCase())}</span>
|
<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>
|
<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>
|
||||||
<span class="updater-cve-pkg">${this.escape(c.package || '')}</span>
|
<span class="updater-cve-pkg">${this.escape(c.package || '')}</span>
|
||||||
${c.fixed_in ? `<span class="updater-cve-fix">fixed in ${this.escape(c.fixed_in)}</span>` : ''}
|
${origin ? `<span class="updater-cve-origin">${this.escape(origin)}</span>` : ''}
|
||||||
</div>`).join('');
|
${ver}
|
||||||
return `<div class="updater-cve-scroll${list.length > 6 ? ' is-scrollable' : ''}">${items}</div>`;
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCveList(cves) {
|
||||||
|
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));
|
||||||
|
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>`
|
||||||
|
: '';
|
||||||
|
const body = group('Fix available', 'update the app to clear these', '', fixable)
|
||||||
|
+ group('No fix yet', 'waiting on an upstream patch', ' is-nofix', noFix);
|
||||||
|
return `<div class="updater-cve-scroll${list.length > 6 ? ' is-scrollable' : ''}">${body}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The CVE scanner's live state, stamped on cves.json by the updater generator:
|
// The CVE scanner's live state, stamped on cves.json by the updater generator:
|
||||||
@ -494,9 +539,17 @@ 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>`;
|
<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 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 verLabel = a.current_version ? ` ${this.escape(a.current_version)}` : '';
|
||||||
|
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>`
|
||||||
|
: '';
|
||||||
const security = `<div class="updater-detail-section"><h4>Security${
|
const security = `<div class="updater-detail-section"><h4>Security${
|
||||||
cves.length ? ` <span class="updater-cve-count">${cves.length}</span>` : ''}</h4>${
|
cves.length ? ` <span class="updater-cve-count">${cves.length}</span>` : ''}</h4>${
|
||||||
cves.length ? `<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)}</div>` : '<p class="updater-detail-empty">No known CVEs. 🎉</p>'}</div>`;
|
||||||
|
|
||||||
// A rollback target exists if a snapshot field is present (future-proofing)
|
// 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
|
// OR — the data the generator actually emits today — this app has a prior
|
||||||
|
|||||||
@ -73,13 +73,25 @@ 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)"
|
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; }
|
[ -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 '
|
printf '%s' "$raw" | jq -c '
|
||||||
[ (.Results // [])[] | (.Vulnerabilities // [])[] | {
|
[ (.Results // [])[]
|
||||||
|
| (.Class // "") as $class
|
||||||
|
| (.Type // "") as $type
|
||||||
|
| (.Vulnerabilities // [])[] | {
|
||||||
id: .VulnerabilityID,
|
id: .VulnerabilityID,
|
||||||
severity: ((.Severity // "UNKNOWN") | ascii_downcase),
|
severity: ((.Severity // "UNKNOWN") | ascii_downcase),
|
||||||
package: .PkgName,
|
package: .PkgName,
|
||||||
installed: (.InstalledVersion // ""),
|
installed: (.InstalledVersion // ""),
|
||||||
fixed_in: (.FixedVersion // ""),
|
fixed_in: (.FixedVersion // ""),
|
||||||
|
status: (.Status // ""),
|
||||||
|
class: $class,
|
||||||
|
type: $type,
|
||||||
url: (.PrimaryURL // "")
|
url: (.PrimaryURL // "")
|
||||||
} ]
|
} ]
|
||||||
| unique_by(.id + "|" + (.package // ""))
|
| unique_by(.id + "|" + (.package // ""))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user