rustsec_admin/
linter.rs

1//! RustSec Advisory DB Linter
2
3use crate::{
4    error::{Error, ErrorKind},
5    lock::acquire_cargo_package_lock,
6    prelude::*,
7};
8use std::{
9    fs,
10    path::{Path, PathBuf},
11};
12use tame_index::index::RemoteGitIndex;
13
14/// List of "collections" within the Advisory DB
15// TODO(tarcieri): provide some other means of iterating over the collections?
16pub const COLLECTIONS: &[rustsec::Collection] =
17    &[rustsec::Collection::Crates, rustsec::Collection::Rust];
18
19/// Advisory linter
20pub struct Linter {
21    /// Path to the advisory database
22    repo_path: PathBuf,
23
24    /// Loaded crates.io index
25    crates_index: RemoteGitIndex,
26
27    /// Loaded Advisory DB
28    advisory_db: rustsec::Database,
29
30    /// Total number of invalid advisories encountered
31    invalid_advisories: usize,
32
33    /// Skip namecheck list
34    skip_namecheck: Option<String>,
35}
36
37impl Linter {
38    /// Create a new linter for the database at the given path
39    pub fn new(
40        repo_path: impl Into<PathBuf>,
41        skip_namecheck: Option<String>,
42    ) -> Result<Self, Error> {
43        let repo_path = repo_path.into();
44        let cargo_package_lock = acquire_cargo_package_lock()?;
45        let mut crates_index = RemoteGitIndex::new(
46            tame_index::GitIndex::new(tame_index::IndexLocation::new(
47                tame_index::IndexUrl::CratesIoGit,
48            ))?,
49            &cargo_package_lock,
50        )?;
51        crates_index.fetch(&cargo_package_lock)?;
52        let advisory_db = rustsec::Database::open(&repo_path)?;
53
54        Ok(Self {
55            repo_path,
56            crates_index,
57            advisory_db,
58            invalid_advisories: 0,
59            skip_namecheck,
60        })
61    }
62
63    /// Borrow the loaded advisory database
64    pub fn advisory_db(&self) -> &rustsec::Database {
65        &self.advisory_db
66    }
67
68    /// Lint the loaded database
69    pub fn lint(mut self) -> Result<usize, Error> {
70        for collection in COLLECTIONS {
71            for crate_entry in fs::read_dir(self.repo_path.join(collection.as_str())).unwrap() {
72                let crate_dir = crate_entry.unwrap().path();
73
74                if !crate_dir.is_dir() {
75                    fail!(
76                        ErrorKind::RustSec,
77                        "unexpected file in `{}`: {}",
78                        collection,
79                        crate_dir.display()
80                    );
81                }
82
83                for advisory_entry in crate_dir.read_dir().unwrap() {
84                    let advisory_path = advisory_entry.unwrap().path();
85                    self.lint_advisory(*collection, &advisory_path)?;
86                }
87            }
88        }
89
90        Ok(self.invalid_advisories)
91    }
92
93    /// Lint an advisory at the specified path
94    // TODO(tarcieri): separate out presentation (`status_*`) from linting code?
95    fn lint_advisory(
96        &mut self,
97        collection: rustsec::Collection,
98        advisory_path: &Path,
99    ) -> Result<(), Error> {
100        if !advisory_path.is_file() {
101            fail!(
102                ErrorKind::RustSec,
103                "unexpected entry in `{}`: {}",
104                collection,
105                advisory_path.display()
106            );
107        }
108
109        let advisory = rustsec::Advisory::load_file(advisory_path)?;
110
111        if collection == rustsec::Collection::Crates {
112            self.crates_io_lints(&advisory)?;
113        }
114
115        let lint_result = rustsec::advisory::Linter::lint_file(advisory_path)?;
116
117        if lint_result.errors().is_empty() {
118            status_ok!("Linted", "ok: {}", advisory_path.display());
119        } else {
120            self.invalid_advisories += 1;
121
122            status_err!(
123                "{} contained the following lint errors:",
124                advisory_path.display()
125            );
126
127            for error in lint_result.errors() {
128                println!("  - {}", error);
129            }
130        }
131
132        Ok(())
133    }
134
135    /// Perform lints that connect to https://crates.io
136    fn crates_io_lints(&mut self, advisory: &rustsec::Advisory) -> Result<(), Error> {
137        if !self.name_is_skipped(advisory.metadata.package.as_str())
138            && !self.name_exists_on_crates_io(advisory.metadata.package.as_str())
139        {
140            self.invalid_advisories += 1;
141
142            fail!(
143                ErrorKind::CratesIo,
144                "crates.io package name does not match package name in advisory for {} in {}",
145                advisory.metadata.package.as_str(),
146                advisory.metadata.id
147            );
148        }
149
150        Ok(())
151    }
152
153    /// Checks whether the name is in the skiplist
154    fn name_is_skipped(&self, package_name: &str) -> bool {
155        match &self.skip_namecheck {
156            Some(skips) => skips.split(',').any(|a| a == package_name),
157            None => false,
158        }
159    }
160
161    /// Checks if a crate with this name is present on crates.io
162    fn name_exists_on_crates_io(&self, name: &str) -> bool {
163        if let Ok(Some(crate_)) = self.crates_index.krate(
164            name.try_into().unwrap(),
165            true,
166            &acquire_cargo_package_lock().unwrap(),
167        ) {
168            // This check verifies name normalization.
169            // A request for "serde-json" might return "serde_json",
170            // and we want to catch use a non-canonical name and report it as an error.
171            crate_.name() == name
172        } else {
173            false
174        }
175    }
176}