Skip to content
Merged
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
127 changes: 117 additions & 10 deletions crates/uv-audit/src/service/osv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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")
});
Comment on lines +259 to +276
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging: IME this is a reasonable hierarchy/order of preference -- ADVISORY is semantically the "best" thing, whereas WEB usually refers to a detailed blog post or similar. If neither is present we fall back to the OSV URL itself for the vulnerability, which will include a render of all links of all types for the user to peruse.

(That said, we could drop the WEB fallback here to make things a bit shorter/terser.)


// Extract fix versions from affected ranges
let fix_versions = vuln
.affected
Expand Down Expand Up @@ -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(),
)
}
}

Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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",
],
Expand Down
7 changes: 6 additions & 1 deletion crates/uv-audit/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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>,
Expand All @@ -108,6 +112,7 @@ impl Vulnerability {
id,
summary,
description,
link,
fix_versions,
aliases,
published,
Expand Down Expand Up @@ -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 changes: 8 additions & 0 deletions crates/uv/src/commands/project/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())?;
Expand Down
Loading