Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
82 changes: 66 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 42 additions & 11 deletions app/routes/crate/security.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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}`);
}
Expand All @@ -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 => {
Expand Down
41 changes: 41 additions & 0 deletions app/templates/crate/security.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
28 changes: 24 additions & 4 deletions app/templates/crate/security.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -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})`;
}

<template>
<CrateHeader @crate={{@controller.crate}} />
{{#if @controller.advisories.length}}
Expand Down Expand Up @@ -50,7 +64,13 @@ function cvssUrl(cvss) {
{{#if advisory.cvss}}
<div class='cvss' data-test-cvss>
<strong>CVSS:</strong>
<a href={{cvssUrl advisory.cvss}}>{{advisory.cvss}}</a>
{{#if advisory.cvss.valid}}
<span class={{severityClass advisory.cvss.severity}} data-test-cvss-score>{{~formatScoreDisplay
advisory.cvss
~}}</span>
{{/if}}
<a href={{cvssUrl advisory.cvss}}>{{advisory.cvss.vector}}</a>
</div>
{{/if}}
{{htmlSafe (@controller.convertMarkdown advisory.details)}}
Expand Down
Loading
Loading