Add links to uv audit output (#18392)

## Summary

This adds links to `uv audit`'s outputs. This required adding links to
the backing (common) Vulnerability type and pulling them from the OSV
service. OSV will always produce a link for a Vulnerability (since the
ID itself can be turned into a link), but `Vulnerability::link` itself
is optional since other services may not necessarily guarantee this.

Follows #18193.

## Test Plan

None yet.

---------

Signed-off-by: William Woodruff <william@astral.sh>
This commit is contained in:
William Woodruff
2026-03-10 17:58:55 +08:00
committed by GitHub
parent f54ce6768d
commit d31348109f
3 changed files with 131 additions and 11 deletions
+117 -10
View File
@@ -105,6 +105,34 @@ struct Affected {
// database_specific: Option<serde_json::Value>,
}
/// The type of a reference in an OSV vulnerability record.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
enum ReferenceType {
Advisory,
Article,
Detection,
Discussion,
Report,
Fix,
Introduced,
Package,
Evidence,
Web,
/// Some other reference type. We don't expect these in OSV v1 records,
/// but we include it for forward compatibility.
#[serde(other)]
Other,
}
/// A reference for more information about a vulnerability.
#[derive(Debug, Clone, Deserialize)]
struct Reference {
#[serde(rename = "type")]
reference_type: ReferenceType,
url: DisplaySafeUrl,
}
/// A full vulnerability record from OSV.
#[derive(Debug, Clone, Deserialize)]
struct Vulnerability {
@@ -122,6 +150,7 @@ struct Vulnerability {
published: Option<Timestamp>,
affected: Option<Vec<Affected>>,
aliases: Option<Vec<String>>,
references: Option<Vec<Reference>>,
}
/// Response from a single query.
@@ -223,6 +252,29 @@ impl Osv {
dependency: &types::Dependency,
vuln: Vulnerability,
) -> types::Finding {
// Extract a link for the advisory. We prefer the first
// `ADVISORY` reference, then the first `WEB` reference, and then
// finally we synthesize a URL of `https://osv.dev/vulnerability/<id>`
// where `<id>` is the vulnerability's ID.
let link = vuln
.references
.as_ref()
.and_then(|references| {
references
.iter()
.find(|reference| matches!(reference.reference_type, ReferenceType::Advisory))
.or_else(|| {
references.iter().find(|reference| {
matches!(reference.reference_type, ReferenceType::Web)
})
})
.map(|reference| reference.url.clone())
})
.unwrap_or_else(|| {
DisplaySafeUrl::parse(&format!("https://osv.dev/vulnerability/{}", vuln.id))
.expect("impossible: synthesized URL is invalid")
});
// Extract fix versions from affected ranges
let fix_versions = vuln
.affected
@@ -258,16 +310,20 @@ impl Osv {
.map(types::VulnerabilityID::new)
.collect();
types::Finding::Vulnerability(types::Vulnerability::new(
dependency.clone(),
types::VulnerabilityID::new(vuln.id),
vuln.summary,
vuln.details,
fix_versions,
aliases,
vuln.published,
Some(vuln.modified),
))
types::Finding::Vulnerability(
types::Vulnerability::new(
dependency.clone(),
types::VulnerabilityID::new(vuln.id),
vuln.summary,
vuln.details,
Some(link),
fix_versions,
aliases,
vuln.published,
Some(vuln.modified),
)
.into(),
)
}
}
@@ -419,6 +475,23 @@ mod tests {
),
summary: None,
description: None,
link: Some(
DisplaySafeUrl {
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"osv.dev",
),
),
port: None,
path: "/vulnerability/VULN-1",
query: None,
fragment: None,
},
),
fix_versions: [],
aliases: [],
published: Some(
@@ -442,6 +515,23 @@ mod tests {
),
summary: None,
description: None,
link: Some(
DisplaySafeUrl {
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"osv.dev",
),
),
port: None,
path: "/vulnerability/VULN-2",
query: None,
fragment: None,
},
),
fix_versions: [],
aliases: [],
published: Some(
@@ -505,6 +595,23 @@ mod tests {
description: Some(
"## Vulnerability Summary\n\nThe `public_key_from_numbers` (or `EllipticCurvePublicNumbers.public_key()`), `EllipticCurvePublicNumbers.public_key()`, `load_der_public_key()` and `load_pem_public_key()` functions do not verify that the point belongs to the expected prime-order subgroup of the curve.\n\nThis missing validation allows an attacker to provide a public key point `P` from a small-order subgroup. This can lead to security issues in various situations, such as the most commonly used signature verification (ECDSA) and shared key negotiation (ECDH). When the victim computes the shared secret as `S = [victim_private_key]P` via ECDH, this leaks information about `victim_private_key mod (small_subgroup_order)`. For curves with cofactor > 1, this reveals the least significant bits of the private key. When these weak public keys are used in ECDSA , it's easy to forge signatures on the small subgroup.\n\nOnly SECT curves are impacted by this.\n\n## Credit\n\nThis vulnerability was discovered by:\n- XlabAI Team of Tencent Xuanwu Lab\n- Atuin Automated Vulnerability Discovery Engine",
),
link: Some(
DisplaySafeUrl {
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"nvd.nist.gov",
),
),
port: None,
path: "/vuln/detail/CVE-2026-26007",
query: None,
fragment: None,
},
),
fix_versions: [
"46.0.5",
],
+6 -1
View File
@@ -3,6 +3,7 @@
use jiff::Timestamp;
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_redacted::DisplaySafeUrl;
use uv_small_str::SmallString;
/// Represents a resolved dependency, with a normalized name and PEP 440 version.
@@ -78,6 +79,8 @@ pub struct Vulnerability {
pub summary: Option<String>,
/// A full-length description of the vulnerability, if available.
pub description: Option<String>,
/// A link to more information about the vulnerability, if available.
pub link: Option<DisplaySafeUrl>,
/// Zero or more versions that fix the vulnerability.
pub fix_versions: Vec<Version>,
/// Zero or more aliases for this vulnerability in other databases.
@@ -94,6 +97,7 @@ impl Vulnerability {
id: VulnerabilityID,
summary: Option<String>,
description: Option<String>,
link: Option<DisplaySafeUrl>,
fix_versions: Vec<Version>,
aliases: Vec<VulnerabilityID>,
published: Option<Timestamp>,
@@ -108,6 +112,7 @@ impl Vulnerability {
id,
summary,
description,
link,
fix_versions,
aliases,
published,
@@ -144,6 +149,6 @@ pub struct ProjectStatus {
/// Represents a finding on a dependency.
#[derive(Debug)]
pub enum Finding {
Vulnerability(Vulnerability),
Vulnerability(Box<Vulnerability>),
ProjectStatus(ProjectStatus),
}
+8
View File
@@ -324,6 +324,14 @@ impl AuditResults {
.blue()
)?;
}
if let Some(link) = &vuln.link {
writeln!(
self.printer.stdout_important(),
" Advisory information: {link}\n",
link = link.as_str().blue()
)?;
}
}
writeln!(self.printer.stdout_important())?;