cargo_audit/
auditor.rs

1//! Core auditing functionality
2
3use 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
17// TODO: make configurable
18const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_secs(5 * 60);
19
20/// Security vulnerability auditor
21pub struct Auditor {
22    /// RustSec Advisory Database
23    database: rustsec::Database,
24
25    /// Crates.io registry index
26    registry_index: Option<registry::CachedIndex>,
27
28    /// Presenter for displaying the report
29    presenter: Presenter,
30
31    /// Audit report settings
32    report_settings: report::Settings,
33}
34
35impl Auditor {
36    /// Initialize the auditor
37    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 the directory is locked, print a message and wait for it to become unlocked.
64            // If we don't print the message, `cargo audit` would just hang with no explanation.
65            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 the directory is locked, print a message and wait for it to become unlocked.
120                // If we don't print the message, `cargo audit` would just hang with no explanation.
121                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 the directory is locked, print a message and wait for it to become unlocked.
142                // If we don't print the message, `cargo audit` would just hang with no explanation.
143                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    /// Perform an audit of a textual `Cargo.lock` file
174    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    /// Perform an audit of multiple binary files
199    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    /// Perform an audit of a binary file with dependency data embedded by `cargo auditable`
234    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    /// The part of the auditing process that is shared between auditing lockfiles and binary files
252    fn audit(
253        &mut self,
254        lockfile: &Lockfile,
255        path: Option<&Path>,
256        #[allow(unused_variables)] // May be unused when the "binary-scanning" feature is disabled
257        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        // Warn for yanked crates
268        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    /// Load the lockfile to be audited
313    fn load_lockfile(&self, lockfile_path: &Path) -> rustsec::Result<Lockfile> {
314        if lockfile_path == Path::new("-") {
315            // Read Cargo.lock from STDIN
316            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    /// Query the database for advisories about `cargo-audit` or `rustsec` itself
325    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    /// Determines whether the process should exit with failure based on configuration
345    /// such as `--deny=warnings`.
346    /// **Performance:** calls `Auditor.self_advisories()`, which is costly.
347    /// Do not call this in a hot loop.
348    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/// Summary of the report over multiple scanned files
357#[derive(Clone, Copy, Debug, Default)]
358pub struct MultiFileReportSummmary {
359    /// Whether any vulnerabilities were found
360    pub vulnerabilities_found: bool,
361    /// Whether any errors were encountered during scanning
362    pub errors_encountered: bool,
363}