From ff61d91e3956a5eb341251a1ef013ae600d50082 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 30 Jan 2026 10:19:33 +0100 Subject: [PATCH] Add CVSS scores to Security tab --- .github/workflows/ci.yml | 12 +++ Cargo.lock | 82 ++++++++++++++---- app/routes/crate/security.js | 53 +++++++++--- app/templates/crate/security.css | 41 +++++++++ app/templates/crate/security.gjs | 28 ++++++- app/utils/cvss.js | 39 +++++++++ crates/crates_io_cvss_wasm/Cargo.toml | 21 +++++ crates/crates_io_cvss_wasm/lib.rs | 114 ++++++++++++++++++++++++++ e2e/acceptance/security.spec.ts | 33 +++++++- ember-cli-build.js | 5 +- eslint.config.mjs | 1 + package.json | 8 +- pnpm-lock.yaml | 13 ++- script/precompress-assets.mjs | 7 +- 14 files changed, 415 insertions(+), 42 deletions(-) create mode 100644 app/utils/cvss.js create mode 100644 crates/crates_io_cvss_wasm/Cargo.toml create mode 100644 crates/crates_io_cvss_wasm/lib.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01867330333..5b0236f2f5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -223,6 +223,10 @@ jobs: cache: pnpm node-version-file: package.json + - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 + with: + tool: wasm-pack + - run: pnpm install - run: pnpm lint:hbs @@ -260,6 +264,10 @@ jobs: cache: pnpm node-version-file: package.json + - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 + with: + tool: wasm-pack + - run: pnpm install - if: github.repository == 'rust-lang/crates.io' @@ -347,6 +355,10 @@ jobs: cache: pnpm node-version-file: package.json + - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # v2.52.4 + with: + tool: wasm-pack + - run: pnpm install - run: pnpm playwright install chromium diff --git a/Cargo.lock b/Cargo.lock index cee45bc5695..2a2a2ca208b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1312,6 +1312,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1528,6 +1538,17 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "crates_io_cvss_wasm" +version = "0.0.0" +dependencies = [ + "console_error_panic_hook", + "cvss", + "serde", + "serde-wasm-bindgen", + "wasm-bindgen", +] + [[package]] name = "crates_io_database" version = "0.0.0" @@ -2059,6 +2080,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "cvss" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fb220d3ce1b565af39cee5b89e47fd8dd1dab162900ee4363c8ee4169ee8a2" + [[package]] name = "darling" version = "0.20.11" @@ -3706,9 +3733,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -5755,6 +5782,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -7069,25 +7107,37 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", - "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -7096,9 +7146,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7106,22 +7156,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ - "bumpalo", "proc-macro2", "quote", "syn", + "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] @@ -7141,9 +7191,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/app/routes/crate/security.js b/app/routes/crate/security.js index a64e6255d47..6337594aa06 100644 --- a/app/routes/crate/security.js +++ b/app/routes/crate/security.js @@ -1,13 +1,38 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; +import { loadCvssModule, parseCvss } from 'crates-io/utils/cvss'; import { versionRanges } from 'crates-io/utils/version-ranges'; -function extractCvss(advisory) { +async function extractCvssWithScore(advisory) { // Prefer V4 over V3 let cvssEntry = advisory.severity?.find(s => s.type === 'CVSS_V4') ?? advisory.severity?.find(s => s.type === 'CVSS_V3'); - return cvssEntry?.score ?? null; + + if (!cvssEntry?.score) { + return null; + } + + // Parse the vector using WASM module to get calculated score and severity + try { + let parsed = await parseCvss(cvssEntry.score); + return { + vector: cvssEntry.score, + calculatedScore: parsed.score, + severity: parsed.severity, + version: parsed.version, + valid: parsed.valid, + }; + } catch { + // Fallback to just returning the vector string + return { + vector: cvssEntry.score, + calculatedScore: null, + severity: null, + version: null, + valid: false, + }; + } } async function fetchAdvisories(crateId) { @@ -17,17 +42,22 @@ async function fetchAdvisories(crateId) { return []; } else if (response.ok) { let advisories = await response.json(); - return advisories - .filter( - advisory => - !advisory.withdrawn && - !advisory.affected?.some(affected => affected.database_specific?.informational === 'unmaintained'), - ) - .map(advisory => ({ + + // Filter advisories + let filtered = advisories.filter( + advisory => + !advisory.withdrawn && + !advisory.affected?.some(affected => affected.database_specific?.informational === 'unmaintained'), + ); + + // Process CVSS scores in parallel + return Promise.all( + filtered.map(async advisory => ({ ...advisory, versionRanges: versionRanges(advisory), - cvss: extractCvss(advisory), - })); + cvss: await extractCvssWithScore(advisory), + })), + ); } else { throw new Error(`HTTP error! status: ${response}`); } @@ -44,6 +74,7 @@ export default class SecurityRoute extends Route { fetchAdvisories(crate.id), import('micromark'), import('micromark-extension-gfm'), + loadCvssModule(), // Pre-load WASM module ]); let convertMarkdown = markdown => { diff --git a/app/templates/crate/security.css b/app/templates/crate/security.css index f6786ff67d0..4223efab30b 100644 --- a/app/templates/crate/security.css +++ b/app/templates/crate/security.css @@ -65,3 +65,44 @@ .cvss strong { margin-right: var(--space-2xs); } + + +.cvss a { + font-family: monospace; + font-size: 0.9em; +} + +:global(.severity-none), +:global(.severity-low), +:global(.severity-medium), +:global(.severity-high), +:global(.severity-critical) { + font-weight: 600; + padding: var(--space-4xs) var(--space-2xs); + border-radius: var(--space-4xs); +} + +:global(.severity-none) { + background-color: light-dark(#e0e0e0, #424242); + color: light-dark(#616161, #bdbdbd); +} + +:global(.severity-low) { + background-color: light-dark(#c8e6c9, #1b5e20); + color: light-dark(#2e7d32, #a5d6a7); +} + +:global(.severity-medium) { + background-color: light-dark(#fff3e0, #e65100); + color: light-dark(#e65100, #ffe0b2); +} + +:global(.severity-high) { + background-color: light-dark(#ffccbc, #bf360c); + color: light-dark(#d84315, #ffab91); +} + +:global(.severity-critical) { + background-color: light-dark(#ffcdd2, #b71c1c); + color: light-dark(#c62828, #ef9a9a); +} diff --git a/app/templates/crate/security.gjs b/app/templates/crate/security.gjs index 3ca688423f8..e55d65073bb 100644 --- a/app/templates/crate/security.gjs +++ b/app/templates/crate/security.gjs @@ -12,14 +12,28 @@ function aliasUrl(alias) { } function cvssUrl(cvss) { - // Extract version from CVSS string (e.g., "CVSS:3.1/..." -> "3.1") - let match = cvss.match(/^CVSS:(\d+\.\d+)\//); + if (!cvss?.vector) return null; + let match = cvss.vector.match(/^CVSS:(\d+\.\d+)\//); if (match) { - return `https://www.first.org/cvss/calculator/${match[1]}#${cvss}`; + return `https://www.first.org/cvss/calculator/${match[1]}#${cvss.vector}`; } return null; } +function severityClass(severity) { + return `severity-${severity ?? 'none'}`; +} + +function formatScoreDisplay(cvss) { + let score = cvss?.calculatedScore; + let severity = cvss?.severity; + if (score === null || score === undefined || Number.isNaN(score)) { + return 'N/A'; + } + let capitalizedSeverity = severity ? severity.charAt(0).toUpperCase() + severity.slice(1) : ''; + return `${score.toFixed(1)} (${capitalizedSeverity})`; +} +