Skip to main content

rustsec/database/
query.rs

1//! Queries against the RustSec database
2//!
3use crate::{
4    SourceId,
5    advisory::{Advisory, Severity},
6    collection::Collection,
7    package::{self, Package},
8};
9use platforms::target::{Arch, OS};
10use semver::Version;
11
12/// Queries against the RustSec database
13#[derive(Clone, Debug)]
14pub struct Query {
15    /// Collection to query against
16    pub(super) collection: Option<Collection>,
17
18    /// Package name to search for
19    pub(super) package_name: Option<package::Name>,
20
21    /// Package version to search for
22    package_version: Option<Version>,
23
24    /// Source of the package advisories should be matched against
25    package_source: Option<SourceId>,
26
27    /// Severity threshold (i.e. minimum severity)
28    severity: Option<Severity>,
29
30    /// Target architecture
31    target_arch: Vec<Arch>,
32
33    /// Target operating system
34    target_os: Vec<OS>,
35
36    /// Year associated with the advisory ID
37    year: Option<u32>,
38
39    /// Query for withdrawn advisories
40    /// (i.e. advisories which were soft-deleted from the database,
41    /// as opposed to yanked crates)
42    withdrawn: Option<bool>,
43
44    /// Query for informational advisories
45    informational: Option<bool>,
46}
47
48impl Query {
49    /// Create a new query.
50    ///
51    /// This creates a "wildcard" query with no constraints. Use the various
52    /// builder methods of this type to restrict which advisories match.
53    ///
54    /// Note that this differs from [`Query::default()`], which scopes the
55    /// query to crates (i.e. [`Query::crate_scope`]).
56    ///
57    /// When in doubt, use [`Query::default()`].
58    pub fn new() -> Self {
59        Self {
60            collection: None,
61            package_name: None,
62            package_version: None,
63            package_source: None,
64            severity: None,
65            target_arch: Default::default(),
66            target_os: Default::default(),
67            year: None,
68            withdrawn: None,
69            informational: None,
70        }
71    }
72
73    /// Create a new query which uses the default scope rules for crates:
74    ///
75    /// - Only `Collection::Crates`
76    /// - Ignore withdrawn advisories
77    /// - Ignore informational advisories
78    pub fn crate_scope() -> Self {
79        Self::new()
80            .collection(Collection::Crates)
81            .withdrawn(false)
82            .informational(false)
83    }
84
85    /// Set collection to query against
86    pub fn collection(mut self, collection: Collection) -> Self {
87        self.collection = Some(collection);
88        self
89    }
90
91    /// Provide a package and use all of its attributes as part of the query
92    #[allow(clippy::assigning_clones)]
93    pub fn package(mut self, package: &Package) -> Self {
94        self.package_name = Some(package.name.clone());
95        self.package_version = Some(package.version.clone());
96        self.package_source = package.source.clone();
97        self
98    }
99
100    /// Set package name to search for.
101    pub fn package_name(mut self, name: package::Name) -> Self {
102        self.package_name = Some(name);
103        self
104    }
105
106    /// Set package version to search for
107    pub fn package_version(mut self, version: Version) -> Self {
108        self.package_version = Some(version);
109        self
110    }
111
112    /// Set package source (e.g. registry) where this package is located
113    pub fn package_source(mut self, source: SourceId) -> Self {
114        self.package_source = Some(source);
115        self
116    }
117
118    /// Set minimum severity threshold according to the CVSS
119    /// Qualitative Severity Rating Scale.
120    ///
121    /// Vulnerabilities without associated CVSS information will always
122    /// match regardless of what this is set to.
123    pub fn severity(mut self, severity: Severity) -> Self {
124        self.severity = Some(severity);
125        self
126    }
127
128    /// Set target architectures
129    pub fn target_arch(mut self, arch: Vec<Arch>) -> Self {
130        self.target_arch = arch;
131        self
132    }
133
134    /// Set target operating systems
135    pub fn target_os(mut self, os: Vec<OS>) -> Self {
136        self.target_os = os;
137        self
138    }
139
140    /// Query for vulnerabilities occurring in a specific year.
141    pub fn year(mut self, year: u32) -> Self {
142        self.year = Some(year);
143        self
144    }
145
146    /// Query for withdrawn advisories.
147    ///
148    /// By default they will be omitted from query results.
149    pub fn withdrawn(mut self, setting: bool) -> Self {
150        self.withdrawn = Some(setting);
151        self
152    }
153
154    /// Query for informational advisories. By default they will be omitted
155    /// from query results.
156    pub fn informational(mut self, setting: bool) -> Self {
157        self.informational = Some(setting);
158        self
159    }
160
161    /// Does this query match a given advisory?
162    pub fn matches(&self, advisory: &Advisory) -> bool {
163        if let Some(collection) = self.collection {
164            if Some(collection) != advisory.metadata.collection {
165                return false;
166            }
167        }
168
169        if let Some(package_name) = &self.package_name {
170            if package_name != &advisory.metadata.package {
171                return false;
172            }
173        }
174
175        if let Some(package_version) = &self.package_version {
176            if !advisory.versions.is_vulnerable(package_version) {
177                return false;
178            }
179        }
180
181        if let Some(package_source) = &self.package_source {
182            let advisory_source = advisory
183                .metadata
184                .source
185                .as_ref()
186                .cloned()
187                .unwrap_or_default();
188
189            // TODO(tarcieri): better source comparison?
190            if advisory_source.kind() != package_source.kind()
191                || advisory_source.url() != package_source.url()
192            {
193                return false;
194            }
195        }
196
197        if let Some(severity_threshold) = self.severity {
198            if let Some(advisory_severity) = advisory.severity() {
199                if advisory_severity < severity_threshold {
200                    return false;
201                }
202            }
203        }
204
205        if let Some(affected) = &advisory.affected {
206            if !affected.arch.is_empty()
207                && !self.target_arch.is_empty()
208                && !self
209                    .target_arch
210                    .iter()
211                    .any(|target_arch| affected.arch.contains(target_arch))
212            {
213                return false;
214            }
215
216            if !affected.os.is_empty()
217                && !self.target_os.is_empty()
218                && !self
219                    .target_os
220                    .iter()
221                    .any(|target_os| affected.os.contains(target_os))
222            {
223                return false;
224            }
225        }
226
227        if let Some(query_year) = self.year {
228            if let Some(advisory_year) = advisory.metadata.id.year() {
229                if query_year != advisory_year {
230                    return false;
231                }
232            }
233        }
234
235        if let Some(withdrawn) = self.withdrawn {
236            if withdrawn != advisory.metadata.withdrawn.is_some() {
237                return false;
238            }
239        }
240
241        if let Some(informational) = self.informational {
242            if informational != advisory.metadata.informational.is_some() {
243                return false;
244            }
245        }
246
247        true
248    }
249}
250
251impl Default for Query {
252    fn default() -> Query {
253        Query::crate_scope()
254    }
255}