1use crate::{
4 config::AuditConfig, error::display_err_with_source, prelude::*, presenter::Presenter,
5};
6use rustsec::{registry, report, Error, ErrorKind, Lockfile, Warning, WarningKind};
7
8use rustsec::binary_scanning::BinaryFormat;
9
10use std::{
11 io::{self, Read},
12 path::Path,
13 process::exit,
14 time::Duration,
15};
16
17const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_secs(5 * 60);
19
20pub struct Auditor {
22 database: rustsec::Database,
24
25 registry_index: Option<registry::CachedIndex>,
27
28 presenter: Presenter,
30
31 report_settings: report::Settings,
33}
34
35impl Auditor {
36 pub fn new(config: &AuditConfig) -> Self {
38 let advisory_db_url = config
39 .database
40 .url
41 .as_ref()
42 .map(AsRef::as_ref)
43 .unwrap_or(rustsec::repository::git::DEFAULT_URL);
44
45 let advisory_db_path = config
46 .database
47 .path
48 .as_ref()
49 .cloned()
50 .unwrap_or_else(rustsec::repository::git::Repository::default_path);
51
52 let database = if config.database.fetch {
53 if !config.output.is_quiet() {
54 status_ok!("Fetching", "advisory database from `{}`", advisory_db_url);
55 }
56
57 let mut result = rustsec::repository::git::Repository::fetch(
58 advisory_db_url,
59 &advisory_db_path,
60 !config.database.stale,
61 Duration::from_secs(0),
62 );
63 if let Err(e) = &result {
66 if e.kind() == ErrorKind::LockTimeout {
67 status_warn!("directory {} is locked, waiting for up to {} seconds for it to become available", advisory_db_path.display(), DEFAULT_LOCK_TIMEOUT.as_secs());
68 result = rustsec::repository::git::Repository::fetch(
69 advisory_db_url,
70 &advisory_db_path,
71 !config.database.stale,
72 DEFAULT_LOCK_TIMEOUT,
73 );
74 }
75 }
76
77 let advisory_db_repo = result.unwrap_or_else(|e| {
78 status_err!(
79 "couldn't fetch advisory database: {}",
80 display_err_with_source(&e)
81 );
82 exit(1);
83 });
84
85 rustsec::Database::load_from_repo(&advisory_db_repo).unwrap_or_else(|e| {
86 status_err!(
87 "error loading advisory database: {}",
88 display_err_with_source(&e)
89 );
90 exit(1);
91 })
92 } else {
93 rustsec::Database::open(&advisory_db_path).unwrap_or_else(|e| {
94 status_err!(
95 "error loading advisory database: {}",
96 display_err_with_source(&e)
97 );
98 exit(1);
99 })
100 };
101
102 if !config.output.is_quiet() {
103 status_ok!(
104 "Loaded",
105 "{} security advisories (from {})",
106 database.iter().count(),
107 advisory_db_path.display()
108 );
109 }
110
111 let registry_index = if config.yanked.enabled {
112 if config.yanked.update_index && config.database.fetch {
113 if !config.output.is_quiet() {
114 status_ok!("Updating", "crates.io index");
115 }
116
117 let mut result = registry::CachedIndex::fetch(None, Duration::from_secs(0));
118
119 if let Err(e) = &result {
122 if e.kind() == ErrorKind::LockTimeout {
123 status_warn!("directory {} is locked, waiting for up to {} seconds for it to become available", advisory_db_path.display(), DEFAULT_LOCK_TIMEOUT.as_secs());
124 result = registry::CachedIndex::fetch(None, DEFAULT_LOCK_TIMEOUT);
125 }
126 }
127
128 match result {
129 Ok(index) => Some(index),
130 Err(err) => {
131 if !config.output.is_quiet() {
132 status_warn!("couldn't update crates.io index: {}", err);
133 }
134
135 None
136 }
137 }
138 } else {
139 let mut result = registry::CachedIndex::open(Duration::from_secs(0));
140
141 if let Err(e) = &result {
144 if e.kind() == ErrorKind::LockTimeout {
145 status_warn!("directory {} is locked, waiting for up to {} seconds for it to become available", advisory_db_path.display(), DEFAULT_LOCK_TIMEOUT.as_secs());
146 result = registry::CachedIndex::open(DEFAULT_LOCK_TIMEOUT)
147 }
148 }
149
150 match result {
151 Ok(index) => Some(index),
152 Err(err) => {
153 if !config.output.is_quiet() {
154 status_warn!("couldn't open crates.io index: {}", err);
155 }
156
157 None
158 }
159 }
160 }
161 } else {
162 None
163 };
164
165 Self {
166 database,
167 registry_index,
168 presenter: Presenter::new(&config.output),
169 report_settings: config.report_settings(),
170 }
171 }
172
173 pub fn audit_lockfile(&mut self, lockfile_path: &Path) -> rustsec::Result<rustsec::Report> {
175 let lockfile = match self.load_lockfile(lockfile_path) {
176 Ok(l) => l,
177 Err(e) => {
178 return Err(Error::with_source(
179 ErrorKind::NotFound,
180 format!("Couldn't load {}", lockfile_path.display()),
181 e,
182 ))
183 }
184 };
185
186 self.presenter.before_report(lockfile_path, &lockfile);
187
188 let report = self.audit(&lockfile, None, None);
189
190 let self_advisories = self.self_advisories();
191
192 self.presenter.print_self_report(self_advisories.as_slice());
193
194 report
195 }
196
197 #[cfg(feature = "binary-scanning")]
198 pub fn audit_binaries<P>(&mut self, binaries: &[P]) -> MultiFileReportSummmary
200 where
201 P: AsRef<Path>,
202 {
203 let mut summary = MultiFileReportSummmary::default();
204 for path in binaries {
205 let result = self.audit_binary(path.as_ref());
206 match result {
207 Ok(report) => {
208 if self.presenter.should_exit_with_failure(&report) {
209 summary.vulnerabilities_found = true;
210 }
211 }
212 Err(e) => {
213 status_err!("{}", display_err_with_source(&e));
214 summary.errors_encountered = true;
215 }
216 }
217 }
218
219 let self_advisories = self.self_advisories();
220
221 self.presenter.print_self_report(self_advisories.as_slice());
222
223 if self
224 .presenter
225 .should_exit_with_failure_due_to_self(&self.self_advisories())
226 {
227 summary.errors_encountered = true;
228 }
229 summary
230 }
231
232 #[cfg(feature = "binary-scanning")]
233 fn audit_binary(&mut self, binary_path: &Path) -> rustsec::Result<rustsec::Report> {
235 use rustsec::binary_scanning::BinaryReport::*;
236 let file_contents = std::fs::read(binary_path)?;
237 let (binary_type, report) =
238 rustsec::binary_scanning::load_deps_from_binary(&file_contents, Option::None)?;
239 self.presenter.binary_scan_report(&report, binary_path);
240 match report {
241 Complete(lockfile) | Incomplete(lockfile) => {
242 self.audit(&lockfile, Some(binary_path), Some(binary_type))
243 }
244 None => Err(Error::new(
245 ErrorKind::Parse,
246 &"No dependency information found! Is this a Rust executable built with cargo?",
247 )),
248 }
249 }
250
251 fn audit(
253 &mut self,
254 lockfile: &Lockfile,
255 path: Option<&Path>,
256 #[allow(unused_variables)] binary_format: Option<BinaryFormat>,
258 ) -> rustsec::Result<rustsec::Report> {
259 let mut report = rustsec::Report::generate(&self.database, lockfile, &self.report_settings);
260
261 #[cfg(feature = "binary-scanning")]
262 if let Some(format) = binary_format {
263 use rustsec::binary_scanning::filter_report_by_binary_type;
264 filter_report_by_binary_type(&format, &mut report);
265 }
266
267 let mut yanked = self.check_for_yanked_crates(lockfile);
269 if !yanked.is_empty() {
270 report
271 .warnings
272 .entry(WarningKind::Yanked)
273 .or_default()
274 .append(&mut yanked);
275 }
276
277 self.presenter.print_report(&report, lockfile, path);
278
279 Ok(report)
280 }
281
282 fn check_for_yanked_crates(&mut self, lockfile: &Lockfile) -> Vec<Warning> {
283 let mut result = Vec::new();
284 if let Some(index) = &mut self.registry_index {
285 let pkgs_to_check: Vec<_> = lockfile
286 .packages
287 .iter()
288 .filter(|pkg| match &pkg.source {
289 Some(source) => source.is_default_registry(),
290 None => false,
291 })
292 .collect();
293
294 let yanked = index.find_yanked(pkgs_to_check);
295
296 for pkg in yanked {
297 match pkg {
298 Ok(pkg) => {
299 let warning = Warning::new(WarningKind::Yanked, pkg, None, None, None);
300 result.push(warning);
301 }
302 Err(e) => status_err!(
303 "couldn't check if the package is yanked: {}",
304 display_err_with_source(&e)
305 ),
306 }
307 }
308 }
309 result
310 }
311
312 fn load_lockfile(&self, lockfile_path: &Path) -> rustsec::Result<Lockfile> {
314 if lockfile_path == Path::new("-") {
315 let mut lockfile_toml = String::new();
317 io::stdin().read_to_string(&mut lockfile_toml)?;
318 Ok(lockfile_toml.parse()?)
319 } else {
320 Ok(Lockfile::load(lockfile_path)?)
321 }
322 }
323
324 fn self_advisories(&self) -> Vec<rustsec::Advisory> {
326 let mut results = vec![];
327
328 for (package_name, package_version) in [
329 ("cargo-audit", crate::VERSION),
330 ("rustsec", rustsec::VERSION),
331 ] {
332 let query = rustsec::database::Query::crate_scope()
333 .package_name(package_name.parse().unwrap())
334 .package_version(package_version.parse().unwrap());
335
336 for advisory in self.database.query(&query) {
337 results.push(advisory.clone());
338 }
339 }
340
341 results
342 }
343
344 pub fn should_exit_with_failure(&self, report: &rustsec::Report) -> bool {
349 self.presenter.should_exit_with_failure(report)
350 || self
351 .presenter
352 .should_exit_with_failure_due_to_self(&self.self_advisories())
353 }
354}
355
356#[derive(Clone, Copy, Debug, Default)]
358pub struct MultiFileReportSummmary {
359 pub vulnerabilities_found: bool,
361 pub errors_encountered: bool,
363}