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
1 change: 1 addition & 0 deletions Cargo.lock

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

3 changes: 0 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,3 @@ codegen-units = 1
# The profile that 'cargo dist' will build with.
[profile.dist]
inherits = "release"

[workspace.metadata.cargo-shear]
ignored = ["uv-audit"]
1 change: 1 addition & 0 deletions _typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ CPY_VERSION_RE = "CPY_VERSION_RE" # CPython version regex
[default.extend-words]
certifi = "certifi" # Python package name
Iz = "Iz" # appears in base64-encoded hashes
vulnerabilit = "vulnerabilit" # appears in pluralization renderings of "vulnerability"/"vulnerabilities"
172 changes: 95 additions & 77 deletions crates/uv-audit/src/service/osv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
use uv_pep440::Version;
use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};

use crate::types::{Dependency, Finding, VulnerabilityID};
use crate::types;

const API_BASE: &str = "https://api.osv.dev/";

Expand Down Expand Up @@ -161,7 +161,10 @@ impl Osv {
}

/// Query OSV for vulnerabilities affecting the given dependency.
pub async fn query(&self, dependency: &Dependency) -> Result<Vec<Finding>, Error> {
pub async fn query(
&self,
dependency: &types::Dependency,
) -> Result<Vec<types::Finding>, Error> {
let mut all_vulnerabilities = Vec::new();
let mut page_token: Option<String> = None;

Expand Down Expand Up @@ -216,7 +219,10 @@ impl Osv {
}

/// Convert an OSV Vulnerability record to a Finding.
fn vulnerability_to_finding(dependency: &Dependency, vuln: Vulnerability) -> Finding {
fn vulnerability_to_finding(
dependency: &types::Dependency,
vuln: Vulnerability,
) -> types::Finding {
// Extract fix versions from affected ranges
let fix_versions = vuln
.affected
Expand Down Expand Up @@ -249,20 +255,19 @@ impl Osv {
.aliases
.unwrap_or_default()
.into_iter()
.map(VulnerabilityID::new)
.map(types::VulnerabilityID::new)
.collect();

let description = vuln.summary.or(vuln.details).unwrap_or(vuln.id.clone());

Finding::Vulnerability {
dependency: dependency.clone(),
id: VulnerabilityID::new(vuln.id),
description,
types::Finding::Vulnerability(types::Vulnerability::new(
dependency.clone(),
types::VulnerabilityID::new(vuln.id),
vuln.summary,
vuln.details,
fix_versions,
aliases,
published: vuln.published,
modified: Some(vuln.modified),
}
vuln.published,
Some(vuln.modified),
))
}
}

Expand Down Expand Up @@ -401,46 +406,52 @@ mod tests {

insta::assert_debug_snapshot!(findings, @r#"
[
Vulnerability {
dependency: Dependency {
name: PackageName(
"foobar",
Vulnerability(
Vulnerability {
dependency: Dependency {
name: PackageName(
"foobar",
),
version: "1.2.3",
},
id: VulnerabilityID(
"VULN-1",
),
summary: None,
description: None,
fix_versions: [],
aliases: [],
published: Some(
2026-01-01T00:00:00Z,
),
modified: Some(
2026-01-01T00:00:00Z,
),
version: "1.2.3",
},
id: VulnerabilityID(
"VULN-1",
),
description: "VULN-1",
fix_versions: [],
aliases: [],
published: Some(
2026-01-01T00:00:00Z,
),
modified: Some(
2026-01-01T00:00:00Z,
),
},
Vulnerability {
dependency: Dependency {
name: PackageName(
"foobar",
),
Vulnerability(
Vulnerability {
dependency: Dependency {
name: PackageName(
"foobar",
),
version: "1.2.3",
},
id: VulnerabilityID(
"VULN-2",
),
summary: None,
description: None,
fix_versions: [],
aliases: [],
published: Some(
2026-01-02T00:00:00Z,
),
modified: Some(
2026-01-02T00:00:00Z,
),
version: "1.2.3",
},
id: VulnerabilityID(
"VULN-2",
),
description: "VULN-2",
fix_versions: [],
aliases: [],
published: Some(
2026-01-02T00:00:00Z,
),
modified: Some(
2026-01-02T00:00:00Z,
),
},
),
]
"#);

Expand Down Expand Up @@ -471,38 +482,45 @@ mod tests {
let finding = findings
.iter()
.find(|finding| match finding {
Finding::Vulnerability { id, .. } => id.as_str() == "GHSA-r6ph-v2qm-q3c2",
Finding::ProjectStatus { .. } => false,
Finding::Vulnerability(vuln) => vuln.id.as_str() == "GHSA-r6ph-v2qm-q3c2",
Finding::ProjectStatus(_) => false,
})
.expect("Expected to find GHSA-r6ph-v2qm-q3c2 vulnerability");

insta::assert_debug_snapshot!(finding, @r#"
Vulnerability {
dependency: Dependency {
name: PackageName(
"cryptography",
insta::assert_debug_snapshot!(finding, @r###"
Vulnerability(
Vulnerability {
dependency: Dependency {
name: PackageName(
"cryptography",
),
version: "46.0.4",
},
id: VulnerabilityID(
"GHSA-r6ph-v2qm-q3c2",
),
version: "46.0.4",
},
id: VulnerabilityID(
"GHSA-r6ph-v2qm-q3c2",
),
description: "cryptography Vulnerable to a Subgroup Attack Due to Missing Subgroup Validation for SECT Curves",
fix_versions: [
"46.0.5",
],
aliases: [
VulnerabilityID(
"CVE-2026-26007",
summary: Some(
"cryptography Vulnerable to a Subgroup Attack Due to Missing Subgroup Validation for SECT Curves",
),
],
published: Some(
2026-02-10T21:27:06Z,
),
modified: Some(
2026-02-11T15:58:46.005582Z,
),
}
"#);
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",
),
fix_versions: [
"46.0.5",
],
aliases: [
VulnerabilityID(
"CVE-2026-26007",
),
],
published: Some(
2026-02-10T21:27:06Z,
),
modified: Some(
2026-02-11T15:58:46.005582Z,
),
},
)
"###);
}
}
92 changes: 72 additions & 20 deletions crates/uv-audit/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,31 +67,83 @@ pub enum AdverseStatus {
Deprecated,
}

/// Represents a finding on a dependency.
/// A vulnerability within a dependency.
#[derive(Debug)]
pub enum Finding {
/// A vulnerability within a dependency.
Vulnerability {
/// The dependency that is vulnerable.
pub struct Vulnerability {
/// The dependency that is vulnerable.
pub dependency: Dependency,
/// The unique identifier for the vulnerability.
pub id: VulnerabilityID,
/// A short, human-readable summary of the vulnerability, if available.
pub summary: Option<String>,
/// A full-length description of the vulnerability, if available.
pub description: Option<String>,
/// Zero or more versions that fix the vulnerability.
pub fix_versions: Vec<Version>,
/// Zero or more aliases for this vulnerability in other databases.
pub aliases: Vec<VulnerabilityID>,
/// The timestamp when this vulnerability was published, if available.
pub published: Option<Timestamp>,
/// The timestamp when this vulnerability was last modified, if available.
pub modified: Option<Timestamp>,
}

impl Vulnerability {
pub fn new(
dependency: Dependency,
/// The unique identifier for the vulnerability.
id: VulnerabilityID,
/// A short, human-readable description of the vulnerability.
description: String,
/// Zero or more versions that fix the vulnerability.
summary: Option<String>,
description: Option<String>,
fix_versions: Vec<Version>,
/// Zero or more aliases for this vulnerability in other databases.
aliases: Vec<VulnerabilityID>,
/// The timestamp when this vulnerability was published, if available.
published: Option<Timestamp>,
/// The timestamp when this vulnerability was last modified, if available.
modified: Option<Timestamp>,
},
/// An adverse project status, such as an archived or deprecated project.
ProjectStatus {
/// The dependency with the adverse status.
dependency: Dependency,
/// The adverse status of the project.
status: AdverseStatus,
},
) -> Self {
// Vulnerability summaries often contain excess whitespace, as well as newlines.
// We normalize these out.
let summary = summary.map(|summary| summary.trim().replace('\n', ""));

Self {
dependency,
id,
summary,
description,
fix_versions,
aliases,
published,
modified,
}
}

/// Pick the subjectively "best" identifier for this vulnerability.
/// For our purposes we prefer PYSEC IDs, then GHSA, then CVE, then whatever
/// primary ID the vulnerability came with.
pub fn best_id(&self) -> &VulnerabilityID {
std::iter::once(&self.id)
.chain(self.aliases.iter())
.find(|id| {
id.as_str().starts_with("PYSEC-")
|| id.as_str().starts_with("GHSA-")
|| id.as_str().starts_with("CVE-")
})
.unwrap_or(&self.id)
}
}

/// An adverse project status, such as an archived or deprecated project.
#[derive(Debug)]
pub struct ProjectStatus {
/// The dependency with the adverse status.
pub dependency: Dependency,
/// The adverse status of the project.
pub status: AdverseStatus,
/// An optional (index-supplied) reason for the adverse status.
pub reason: Option<String>,
}

/// Represents a finding on a dependency.
#[derive(Debug)]
pub enum Finding {
Vulnerability(Vulnerability),
ProjectStatus(ProjectStatus),
}
1 change: 1 addition & 0 deletions crates/uv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ default-run = "uv"
workspace = true

[dependencies]
uv-audit = { workspace = true }
uv-auth = { workspace = true }
uv-bin-install = { workspace = true }
uv-build-backend = { workspace = true }
Expand Down
Loading