From 74ebe212dcb6e1a649e4228fc513fa9ca8391cdd Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Mon, 9 Sep 2019 11:39:11 -0700 Subject: [PATCH] Upgrade to `rustsec` crate v0.13.0-alpha1; add linter Upgrades the `rustsec` crate to the latest alpha release and uses the new `rustsec::advisory::Linter` functionality to lint advisories currently in the database. Several of them are using invalid keys and need to be updated. --- Cargo.toml | 5 +- src/main.rs | 238 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 164 insertions(+), 79 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7f2f3b7..9544796 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ publish = false name = "rustsec-advisory-db" [dependencies] -gumdrop = "0.6" -rustsec = "0.12" crates_io_api = "0.5" +gumdrop = "0.6" +rustsec = "0.13.0-alpha1" +termcolor = "1" diff --git a/src/main.rs b/src/main.rs index 04562bc..bb920d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,120 +1,204 @@ +//! RustSec Advisory DB Linter + #![allow(clippy::never_loop)] use gumdrop::Options; -use rustsec::{AdvisoryDatabase, Repository}; -use std::{env, process::exit}; +use rustsec::package; +use std::{env, io::Write, path::PathBuf, process::exit}; +use termcolor::{ + Color::{Green, Red}, + ColorChoice, ColorSpec, StandardStream, WriteColor, +}; -const MIN_EXPECTED_ADVISORIES: usize = 5; +/// Minimum number of advisories we expect in the database +const MIN_EXPECTED_ADVISORIES: usize = 10; -/// Subcommands -#[derive(Debug, Options)] -enum Opts { - #[options(help = "show help for a command")] - Help(HelpOpts), +macro_rules! writeln_color { + ($stream:expr, $color:path, $fmt:expr, $msg:expr) => { + let mut color = ColorSpec::new(); + color.bold(); + color.set_fg(Some($color)); + $stream.set_color(&color).unwrap(); - #[options(help = "check the advisory DB is well-formed")] - Check(CheckOpts), + writeln!($stream, $fmt, $msg).unwrap(); + $stream.reset().unwrap(); + }; } -/// Options for the `help` command -#[derive(Debug, Default, Options)] -struct HelpOpts { - #[options(free)] - commands: Vec, +macro_rules! writeln_success { + ($stream:expr, $msg:expr) => { + writeln_color!($stream, Green, "✔ {}", $msg); + }; + ($stream:expr, $fmt:expr, $($arg:tt)+) => { + writeln_success!($stream, format!($fmt, $($arg)+)); + } } -/// Options for the `check` command -#[derive(Debug, Default, Options)] -struct CheckOpts {} +macro_rules! writeln_error { + ($stream:expr, $msg:expr) => { + writeln_color!($stream, Red, "✘ {}", $msg); + }; + ($stream:expr, $fmt:expr, $($arg:tt)+) => { + writeln_error!($stream, format!($fmt, $($arg)+)); + } +} fn main() { let args: Vec<_> = env::args().collect(); - let opts = Opts::parse_args_default(&args[1..]).unwrap_or_else(|e| { + let opts = Commands::parse_args_default(&args[1..]).unwrap_or_else(|e| { match e.to_string().as_ref() { // Show usage if no command name is given or if "help" is given - "missing command name" => help(&[]), + "missing command name" => help(), string => eprintln!("{}: {}", args[0], string), } - exit(2); + exit(1); }); match opts { - Opts::Help(opts) => help(&opts.commands), - Opts::Check(_) => check(), + Commands::Help(_) => help(), + Commands::Check(check) => check.run(), } +} - exit(0); +/// Subcommands +#[derive(Debug, Options)] +enum Commands { + #[options(help = "show help for a command")] + Help(HelpCommand), + + #[options(help = "check the advisory DB is well-formed")] + Check(CheckCommand), +} + +/// Options for the `help` command +#[derive(Debug, Default, Options)] +struct HelpCommand { + #[options(free)] + commands: Vec, } /// Print help message -fn help(_commands: &[String]) { +fn help() { println!("Usage: {} [COMMAND] [OPTIONS]", env::args().next().unwrap()); println!(); println!("Available commands:"); println!(); - println!("{}", Opts::command_list().unwrap()); + println!("{}", Commands::command_list().unwrap()); println!(); } -fn check() { - let repo = Repository::open(".").unwrap(); +/// Options for the `check` command +#[derive(Debug, Default, Options)] +struct CheckCommand {} - // Ensure Advisories.toml parses - let db = AdvisoryDatabase::from_repository(&repo).unwrap(); - let advisories = db.advisories(); +impl CheckCommand { + fn run(&self) { + let mut stdout = StandardStream::stdout(ColorChoice::Auto); + let repo = rustsec::Repository::open(".").unwrap(); - // Ensure we're parsing some advisories - if advisories.len() > MIN_EXPECTED_ADVISORIES { - println!( - "*** Check succeeded! Successfully parsed {} advisories.", - advisories.len() - ); - } else { - panic!( - "Missing advisories! Expected at least {}, but got {}", - MIN_EXPECTED_ADVISORIES, - advisories.len() - ); + // Ensure Advisories.toml parses + let db = rustsec::Database::load(&repo).unwrap(); + let advisories = db.iter(); + + // Ensure we're parsing some advisories + if advisories.len() > MIN_EXPECTED_ADVISORIES { + writeln_success!( + &mut stdout, + "Successfully parsed {} advisories", + advisories.len() + ); + } else { + writeln_error!( + &mut stdout, + "Missing advisories! Expected at least {}, but got {}", + MIN_EXPECTED_ADVISORIES, + advisories.len() + ); + exit(1); + } + + let cratesio_client = crates_io_api::SyncClient::new(); + + let mut invalid_advisories = 0; + + for advisory in advisories { + if !self.check_advisory(&mut stdout, &cratesio_client, advisory) { + invalid_advisories += 1; + } + } + + if invalid_advisories == 0 { + writeln_success!(&mut stdout, "All advisories are well-formed"); + } else { + writeln_error!( + &mut stdout, + "{} advisories contain errors!", + invalid_advisories + ); + exit(1); + } } - let cratesio_client = crates_io_api::SyncClient::new(); - for advisory in advisories { - check_advisory(&cratesio_client, advisory); - } - println!("*** Check succeeded! All advisories refer to valid crates."); -} - -fn check_advisory(cratesio_client: &crates_io_api::SyncClient, advisory: &rustsec::Advisory) { - let response = cratesio_client - .get_crate(advisory.package.as_str()) - .unwrap_or_else(|_| { - panic!( - "Failed to get package from crates.io: {}", - advisory.package.as_str() - ) - }); - - if response.crate_data.name != advisory.package.as_str() { - panic!( - "crates.io package name does not match package name in advisory for {}", - advisory.package.as_str() - ); - } - - // Check that each path in `affected_paths` starts with the crate name - if let Some(ref version_req_paths) = advisory.affected_paths { - for (_, paths) in version_req_paths.iter() { - for path in paths { - if path.crate_name() != response.crate_data.name { - panic!( - "{}: affected_path does not begin with crate name: {}", - response.crate_data.name, - path.crate_name() - ) + fn check_advisory( + &self, + stdout: &mut StandardStream, + cratesio_client: &crates_io_api::SyncClient, + advisory: &rustsec::Advisory, + ) -> bool { + if advisory.metadata.collection == Some(package::Collection::Crates) { + match cratesio_client.get_crate(advisory.metadata.package.as_str()) { + Ok(response) => { + if response.crate_data.name != advisory.metadata.package.as_str() { + writeln_error!( + stdout, + "crates.io package name does not match package name in advisory for {}", + advisory.metadata.package.as_str() + ); + return false; + } + } + Err(err) => { + writeln_error!( + stdout, + "Failed to get package `{}` from crates.io: {}", + advisory.metadata.package.as_str(), + err + ); + return false; } } } + + let mut advisory_path = PathBuf::from(".") + .join(advisory.metadata.collection.as_ref().unwrap().to_string()) + .join(advisory.metadata.package.as_str()) + .join(advisory.metadata.id.as_str()); + + advisory_path.set_extension("toml"); + + let lint = rustsec::advisory::Linter::lint_file(&advisory_path).unwrap(); + + if lint.errors().is_empty() { + writeln_success!( + stdout, + "{} successfully passed lint", + advisory_path.display() + ); + true + } else { + writeln_error!( + stdout, + "{} contained the following lint errors:", + advisory_path.display() + ); + + for error in lint.errors() { + writeln!(stdout, " - {}", error).unwrap(); + } + + false + } } }