1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! RustSec Advisory DB Linter

use crate::{
    error::{Error, ErrorKind},
    prelude::*,
};
use std::{
    fs,
    path::{Path, PathBuf},
};

/// List of "collections" within the Advisory DB
// TODO(tarcieri): provide some other means of iterating over the collections?
pub const COLLECTIONS: &[rustsec::Collection] =
    &[rustsec::Collection::Crates, rustsec::Collection::Rust];

/// Advisory linter
pub struct Linter {
    /// Path to the advisory database
    repo_path: PathBuf,

    /// Crates.io client
    cratesio_client: crates_io_api::SyncClient,

    /// Loaded Advisory DB
    advisory_db: rustsec::Database,

    /// Total number of invalid advisories encountered
    invalid_advisories: usize,
}

impl Linter {
    /// Create a new linter for the database at the given path
    pub fn new(repo_path: impl Into<PathBuf>) -> Result<Self, Error> {
        let repo_path = repo_path.into();
        let cratesio_client = crates_io_api::SyncClient::new();
        let advisory_db = rustsec::Database::open(&repo_path)?;

        Ok(Self {
            repo_path,
            cratesio_client,
            advisory_db,
            invalid_advisories: 0,
        })
    }

    /// Borrow the loaded advisory database
    pub fn advisory_db(&self) -> &rustsec::Database {
        &self.advisory_db
    }

    /// Lint the loaded database
    pub fn lint(mut self) -> Result<usize, Error> {
        for collection in COLLECTIONS {
            for crate_entry in fs::read_dir(self.repo_path.join(collection.as_str())).unwrap() {
                let crate_dir = crate_entry.unwrap().path();

                if !crate_dir.is_dir() {
                    fail!(
                        ErrorKind::RustSec,
                        "unexpected file in `{}`: {}",
                        collection,
                        crate_dir.display()
                    );
                }

                for advisory_entry in crate_dir.read_dir().unwrap() {
                    let advisory_path = advisory_entry.unwrap().path();
                    self.lint_advisory(*collection, &advisory_path)?;
                }
            }
        }

        Ok(self.invalid_advisories)
    }

    /// Lint an advisory at the specified path
    // TODO(tarcieri): separate out presentation (`status_*`) from linting code?
    fn lint_advisory(
        &mut self,
        collection: rustsec::Collection,
        advisory_path: &Path,
    ) -> Result<(), Error> {
        if !advisory_path.is_file() {
            fail!(
                ErrorKind::RustSec,
                "unexpected entry in `{}`: {}",
                collection,
                advisory_path.display()
            );
        }

        let advisory = rustsec::Advisory::load_file(advisory_path)?;

        if collection == rustsec::Collection::Crates {
            self.crates_io_lints(&advisory)?;
        }

        let lint_result = rustsec::advisory::Linter::lint_file(&advisory_path)?;

        if lint_result.errors().is_empty() {
            status_ok!("Linted", "ok: {}", advisory_path.display());
        } else {
            self.invalid_advisories += 1;

            status_err!(
                "{} contained the following lint errors:",
                advisory_path.display()
            );

            for error in lint_result.errors() {
                println!("  - {}", error);
            }
        }

        Ok(())
    }

    /// Perform lints that connect to https://crates.io
    fn crates_io_lints(&mut self, advisory: &rustsec::Advisory) -> Result<(), Error> {
        let response = self
            .cratesio_client
            .get_crate(advisory.metadata.package.as_str())?;

        if response.crate_data.name != advisory.metadata.package.as_str() {
            self.invalid_advisories += 1;

            fail!(
                ErrorKind::CratesIo,
                "crates.io package name does not match package name in advisory for {}",
                advisory.metadata.package.as_str()
            );
        }

        Ok(())
    }
}