uv_audit/types.rs
1//! Types for interacting with dependency audits.
2
3use jiff::Timestamp;
4use uv_normalize::PackageName;
5use uv_pep440::Version;
6use uv_redacted::DisplaySafeUrl;
7use uv_small_str::SmallString;
8
9/// Represents a resolved dependency, with a normalized name and PEP 440 version.
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct Dependency {
12 name: PackageName,
13 version: Version,
14}
15
16impl Dependency {
17 /// Create a new dependency with the given name and version.
18 pub fn new(name: PackageName, version: Version) -> Self {
19 Self { name, version }
20 }
21
22 /// Get the package name.
23 pub fn name(&self) -> &PackageName {
24 &self.name
25 }
26
27 /// Get the version.
28 pub fn version(&self) -> &Version {
29 &self.version
30 }
31}
32
33/// An opaque identifier for a vulnerability. These are conventionally
34/// formatted as `SRC-XXXX-YYYY`, where `SRC` is an identifier for the vulnerability source,
35/// `XXXX` is typically a year or other "bucket" identifier, and `YYYY` is a unique identifier
36/// within that bucket. For example, `CVE-2026-12345` or `PYSEC-2023-0001`.
37///
38/// No assumptions should be made about the format of these identifiers.
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub struct VulnerabilityID(SmallString);
41
42impl VulnerabilityID {
43 /// Create a new vulnerability ID from a string.
44 pub fn new(id: impl Into<SmallString>) -> Self {
45 Self(id.into())
46 }
47
48 /// Get the string representation of this vulnerability ID.
49 pub fn as_str(&self) -> &str {
50 self.0.as_ref()
51 }
52}
53
54/// Represents an "adverse" project status, i.e. a status that indicates that
55/// a downstream user of the project should review their use of the project
56/// and consider removing it.
57///
58/// These are a subset of the possible project statuses defined in [PEP 792].
59///
60/// [PEP 792]: https://peps.python.org/pep-0792/
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum AdverseStatus {
63 /// The project is archived, meaning it is read-only and no longer maintained.
64 Archived,
65 /// The project is considered generally unsafe for use, e.g. due to malware.
66 Quarantined,
67 /// The project is considered obsolete, and may have been superseded by another project.
68 Deprecated,
69}
70
71impl std::fmt::Display for AdverseStatus {
72 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 formatter.write_str(match self {
74 Self::Archived => "archived",
75 Self::Quarantined => "quarantined",
76 Self::Deprecated => "deprecated",
77 })
78 }
79}
80
81/// A vulnerability within a dependency.
82#[derive(Debug)]
83pub struct Vulnerability {
84 /// The dependency that is vulnerable.
85 pub dependency: Dependency,
86 /// The unique identifier for the vulnerability.
87 pub id: VulnerabilityID,
88 /// A short, human-readable summary of the vulnerability, if available.
89 pub summary: Option<String>,
90 /// A full-length description of the vulnerability, if available.
91 pub description: Option<String>,
92 /// A link to more information about the vulnerability, if available.
93 pub link: Option<DisplaySafeUrl>,
94 /// Zero or more versions that fix the vulnerability.
95 pub fix_versions: Vec<Version>,
96 /// Zero or more aliases for this vulnerability in other databases.
97 pub aliases: Vec<VulnerabilityID>,
98 /// The timestamp when this vulnerability was published, if available.
99 pub published: Option<Timestamp>,
100 /// The timestamp when this vulnerability was last modified, if available.
101 pub modified: Option<Timestamp>,
102}
103
104impl Vulnerability {
105 pub(crate) fn new(
106 dependency: Dependency,
107 id: VulnerabilityID,
108 summary: Option<String>,
109 description: Option<String>,
110 link: Option<DisplaySafeUrl>,
111 fix_versions: Vec<Version>,
112 aliases: Vec<VulnerabilityID>,
113 published: Option<Timestamp>,
114 modified: Option<Timestamp>,
115 ) -> Self {
116 // Vulnerability summaries often contain excess whitespace, as well as newlines.
117 // We normalize these out.
118 let summary = summary.map(|summary| summary.trim().replace('\n', ""));
119
120 Self {
121 dependency,
122 id,
123 summary,
124 description,
125 link,
126 fix_versions,
127 aliases,
128 published,
129 modified,
130 }
131 }
132
133 /// Return an iterator over all identifiers for this vulnerability, including the primary ID and all aliases.
134 fn ids(&self) -> impl Iterator<Item = &VulnerabilityID> {
135 std::iter::once(&self.id).chain(self.aliases.iter())
136 }
137
138 /// Returns `true` if any of this vulnerability's identifiers (primary ID or aliases) match the given ID.
139 pub fn matches(&self, id: &VulnerabilityID) -> bool {
140 self.ids().any(|own_id| own_id == id)
141 }
142
143 /// Pick the subjectively "best" identifier for this vulnerability.
144 /// For our purposes we prefer PYSEC IDs, then GHSA, then CVE, then whatever
145 /// primary ID the vulnerability came with.
146 pub fn best_id(&self) -> &VulnerabilityID {
147 self.ids()
148 .find(|id| {
149 id.as_str().starts_with("PYSEC-")
150 || id.as_str().starts_with("GHSA-")
151 || id.as_str().starts_with("CVE-")
152 })
153 .unwrap_or(&self.id)
154 }
155}
156
157/// An adverse project status, such as an archived or deprecated project.
158///
159/// PEP 792 status markers are project-level, so this finding carries only the
160/// project name — not a specific version.
161#[derive(Debug)]
162pub struct ProjectStatus {
163 /// The name of the project with the adverse status.
164 pub name: PackageName,
165 /// The adverse status of the project.
166 pub status: AdverseStatus,
167 /// An optional (index-supplied) reason for the adverse status.
168 pub reason: Option<String>,
169}
170
171/// Represents a finding on a dependency.
172#[derive(Debug)]
173pub enum Finding {
174 Vulnerability(Box<Vulnerability>),
175 ProjectStatus(ProjectStatus),
176}