diff --git a/crates/uv-audit/src/service/osv.rs b/crates/uv-audit/src/service/osv.rs index 706690701..e1ba7d594 100644 --- a/crates/uv-audit/src/service/osv.rs +++ b/crates/uv-audit/src/service/osv.rs @@ -105,6 +105,34 @@ struct Affected { // database_specific: Option, } +/// 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, affected: Option>, aliases: Option>, + references: Option>, } /// 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/` + // where `` 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", ], diff --git a/crates/uv-audit/src/types.rs b/crates/uv-audit/src/types.rs index 5992deeba..1bf6c3a1a 100644 --- a/crates/uv-audit/src/types.rs +++ b/crates/uv-audit/src/types.rs @@ -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, /// A full-length description of the vulnerability, if available. pub description: Option, + /// A link to more information about the vulnerability, if available. + pub link: Option, /// Zero or more versions that fix the vulnerability. pub fix_versions: Vec, /// Zero or more aliases for this vulnerability in other databases. @@ -94,6 +97,7 @@ impl Vulnerability { id: VulnerabilityID, summary: Option, description: Option, + link: Option, fix_versions: Vec, aliases: Vec, published: Option, @@ -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), ProjectStatus(ProjectStatus), } diff --git a/crates/uv/src/commands/project/audit.rs b/crates/uv/src/commands/project/audit.rs index ecd96ac0a..2ba08d7ae 100644 --- a/crates/uv/src/commands/project/audit.rs +++ b/crates/uv/src/commands/project/audit.rs @@ -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())?;