diff --git a/Cargo.lock b/Cargo.lock index ab3d6cf..0f762f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,7 +142,6 @@ dependencies = [ "libc", "num-integer", "num-traits", - "serde", "time", "winapi", ] @@ -417,6 +416,15 @@ dependencies = [ "slab", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.1.15" @@ -535,6 +543,22 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "humantime-serde" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac34a56cfd4acddb469cc7fff187ed5ac36f498ba085caf8bbc725e3ff474058" +dependencies = [ + "humantime", + "serde", +] + [[package]] name = "hyper" version = "0.14.2" @@ -1047,6 +1071,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "unicase", +] + [[package]] name = "quote" version = "1.0.7" @@ -1187,23 +1223,25 @@ dependencies = [ [[package]] name = "rustsec" -version = "0.22.2" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5982d0d4f57176e3e8d62452a5d6dab98906ee5ab4ad1cb63c0877f0a16ab0e" +checksum = "7989ff58001a2be1c17945e53d32fcad5cf33c638a4b1239d6e1c9aff7a5d2f8" dependencies = [ "cargo-lock", - "chrono", "crates-index 0.16.2", "cvss", "fs-err", "git2", "home", + "humantime", + "humantime-serde", "platforms", "semver 0.11.0", "serde", "smol_str", "thiserror", "toml", + "url", ] [[package]] @@ -1377,6 +1415,7 @@ dependencies = [ "maud", "once_cell", "pin-project 1.0.2", + "pulldown-cmark", "relative-path", "reqwest", "route-recognizer", @@ -1456,9 +1495,9 @@ dependencies = [ [[package]] name = "smol_str" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7909a1d8bc166a862124d84fdc11bda0ea4ed3157ccca662296919c2972db1" +checksum = "6ca0f7ce3a29234210f0f4f0b56f8be2e722488b95cb522077943212da3b32eb" dependencies = [ "serde", ] @@ -1706,6 +1745,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.4" @@ -1724,6 +1772,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + [[package]] name = "unicode-xid" version = "0.2.1" @@ -1740,6 +1794,7 @@ dependencies = [ "idna", "matches", "percent-encoding", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b221eec..605b0d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,11 +21,12 @@ hyper = { version = "0.14", features = ["full"] } indexmap = { version = "1", features = ["serde-1"] } lru_time_cache = "0.11.1" maud = "0.22.1" +pulldown-cmark = "0.8" once_cell = "1" pin-project = "1" relative-path = { version = "1.3", features = ["serde"] } route-recognizer = "0.3" -rustsec = "0.22" +rustsec = "0.23" crates-index = "0.15" # held back by semver 0.11 semver = { version = "0.10", features = ["serde"] } # held at 0.10 due to #63 reqwest = { version = "0.11", features = ["json"] } diff --git a/src/engine/machines/analyzer.rs b/src/engine/machines/analyzer.rs index a2c8b5d..bb32653 100644 --- a/src/engine/machines/analyzer.rs +++ b/src/engine/machines/analyzer.rs @@ -42,11 +42,12 @@ impl DependencyAnalyzer { let version: cargo_lock::Version = ver.to_string().parse().unwrap(); let query = database::Query::new().package_version(name, version); - if !advisory_db - .map(|db| db.query(&query).is_empty()) - .unwrap_or(true) - { - dep.insecure = true; + if let Some(db) = advisory_db { + let vulnerabilities = db.query(&query); + if !vulnerabilities.is_empty() { + dep.vulnerabilities = + vulnerabilities.into_iter().map(|v| v.to_owned()).collect(); + } } } if !ver.is_prerelease() { diff --git a/src/models/crates.rs b/src/models/crates.rs index c6ff022..0d32dcc 100644 --- a/src/models/crates.rs +++ b/src/models/crates.rs @@ -3,6 +3,7 @@ use std::{borrow::Borrow, str::FromStr}; use anyhow::{anyhow, Error}; use indexmap::IndexMap; use relative_path::RelativePathBuf; +use rustsec::Advisory; use semver::{Version, VersionReq}; #[derive(Clone, Debug, Hash, PartialEq, Eq)] @@ -89,7 +90,7 @@ pub struct AnalyzedDependency { pub required: VersionReq, pub latest_that_matches: Option, pub latest: Option, - pub insecure: bool, + pub vulnerabilities: Vec, } impl AnalyzedDependency { @@ -98,10 +99,14 @@ impl AnalyzedDependency { required, latest_that_matches: None, latest: None, - insecure: false, + vulnerabilities: Vec::new(), } } + pub fn is_insecure(&self) -> bool { + !self.vulnerabilities.is_empty() + } + pub fn is_outdated(&self) -> bool { self.latest > self.latest_that_matches } @@ -181,8 +186,16 @@ impl AnalyzedDependencies { /// Returns the number of insecure main and build dependencies pub fn count_insecure(&self) -> usize { - let main_insecure = self.main.iter().filter(|&(_, dep)| dep.insecure).count(); - let build_insecure = self.build.iter().filter(|&(_, dep)| dep.insecure).count(); + let main_insecure = self + .main + .iter() + .filter(|&(_, dep)| dep.is_insecure()) + .count(); + let build_insecure = self + .build + .iter() + .filter(|&(_, dep)| dep.is_insecure()) + .count(); main_insecure + build_insecure } @@ -203,14 +216,17 @@ impl AnalyzedDependencies { /// Counts the number of insecure `dev-dependencies` pub fn count_dev_insecure(&self) -> usize { - self.dev.iter().filter(|&(_, dep)| dep.insecure).count() + self.dev + .iter() + .filter(|&(_, dep)| dep.is_insecure()) + .count() } /// Returns `true` if any dev-dependencies are either insecure or outdated. pub fn any_dev_issues(&self) -> bool { self.dev .iter() - .any(|(_, dep)| dep.is_outdated() || dep.insecure) + .any(|(_, dep)| dep.is_outdated() || dep.is_insecure()) } } diff --git a/src/server/views/html/status.rs b/src/server/views/html/status.rs index a97b741..00bef26 100644 --- a/src/server/views/html/status.rs +++ b/src/server/views/html/status.rs @@ -1,6 +1,8 @@ use hyper::{Body, Response}; use indexmap::IndexMap; -use maud::{html, Markup}; +use maud::{html, Markup, PreEscaped}; +use pulldown_cmark::{html, Parser}; +use rustsec::advisory::Advisory; use semver::Version; use crate::engine::AnalyzeDependenciesOutcome; @@ -17,7 +19,7 @@ fn get_crates_version_url(name: impl AsRef, version: &Version) -> String { format!("https://crates.io/crates/{}/{}", name.as_ref(), version) } -fn dependency_tables(crate_name: CrateName, deps: AnalyzedDependencies) -> Markup { +fn dependency_tables(crate_name: &CrateName, deps: &AnalyzedDependencies) -> Markup { html! { h2 class="title is-3" { "Crate " @@ -29,22 +31,22 @@ fn dependency_tables(crate_name: CrateName, deps: AnalyzedDependencies) -> Marku } @if !deps.main.is_empty() { - (dependency_table("Dependencies", deps.main)) + (dependency_table("Dependencies", &deps.main)) } @if !deps.dev.is_empty() { - (dependency_table("Dev dependencies", deps.dev)) + (dependency_table("Dev dependencies", &deps.dev)) } @if !deps.build.is_empty() { - (dependency_table("Build dependencies", deps.build)) + (dependency_table("Build dependencies", &deps.build)) } } } -fn dependency_table(title: &str, deps: IndexMap) -> Markup { +fn dependency_table(title: &str, deps: &IndexMap) -> Markup { let count_total = deps.len(); - let count_insecure = deps.iter().filter(|&(_, dep)| dep.insecure).count(); + let count_insecure = deps.iter().filter(|&(_, dep)| dep.is_insecure()).count(); let count_outdated = deps.iter().filter(|&(_, dep)| dep.is_outdated()).count(); html! { @@ -86,7 +88,7 @@ fn dependency_table(title: &str, deps: IndexMap) } } td class="has-text-right" { - @if dep.insecure { + @if dep.is_insecure() { span class="tag is-danger" { "insecure" } } @else if dep.is_outdated() { span class="tag is-warning" { "out of date" } @@ -147,6 +149,92 @@ fn render_dev_dependency_box(outcome: &AnalyzeDependenciesOutcome) -> Markup { } } +fn build_rustsec_link(advisory: &Advisory) -> String { + format!( + "https://rustsec.org/advisories/{}.html", + advisory.id().as_str() + ) +} + +fn render_markdown(description: &str) -> Markup { + let mut rendered = String::new(); + html::push_html(&mut rendered, Parser::new(description)); + PreEscaped(rendered) +} + +/// Renders a list of all security vulnerabilities affecting the repository +fn vulnerability_list(analysis_outcome: &AnalyzeDependenciesOutcome) -> Markup { + let mut vulnerabilities = Vec::new(); + for (_, analyzed_crate) in &analysis_outcome.crates { + vulnerabilities.extend( + &mut analyzed_crate + .main + .iter() + .filter(|&(_, dep)| dep.is_insecure()) + .map(|(_, dep)| &dep.vulnerabilities), + ); + vulnerabilities.extend( + &mut analyzed_crate + .dev + .iter() + .filter(|&(_, dep)| dep.is_insecure()) + .map(|(_, dep)| &dep.vulnerabilities), + ); + vulnerabilities.extend( + &mut analyzed_crate + .build + .iter() + .filter(|&(_, dep)| dep.is_insecure()) + .map(|(_, dep)| &dep.vulnerabilities), + ); + } + + // flatten Vec> -> Vec<&Advisory> + let mut vulnerabilities: Vec<&Advisory> = vulnerabilities.into_iter().flatten().collect(); + vulnerabilities.sort_unstable_by_key(|&v| v.id()); + vulnerabilities.dedup(); + + html! { + h3 class="title is-3" id="vulnerabilities" { "Security Vulnerabilities" } + + @for vuln in vulnerabilities { + div class="box" { + h3 class="title is-4" { code { (vuln.metadata.package.as_str()) } ": " (vuln.title()) } + p class="subtitle is-5" style="margin-top: -0.5rem;" { a href=(build_rustsec_link(vuln)) { (vuln.id()) } } + + article { (render_markdown(vuln.description())) } + + nav class="level" style="margin-top: 1rem;" { + div class="level-item has-text-centered" { + div { + p class="heading" { "Unaffected" } + @if vuln.versions.unaffected.is_empty() { + p class="is-grey" { "None"} + } @else { + @for item in &vuln.versions.unaffected { + p { code { (item) } } + } + } + } + } + div class="level-item has-text-centered" { + div { + p class="heading" { "Patched" } + @if vuln.versions.unaffected.is_empty() { + p class="has-text-grey" { "None"} + } @else { + @for item in &vuln.versions.patched { + p { code { (item) } } + } + } + } + } + } + } + } + } +} + fn render_failure(subject_path: SubjectPath) -> Markup { html! { section class="hero is-light" { @@ -220,12 +308,24 @@ fn render_success( } section class="section" { div class="container" { - @if analysis_outcome.any_dev_issues() { + @if analysis_outcome.any_insecure() { + div class="notification is-warning" { + p { "This project contains " + b { "known security vulnerabilities" } + ". Find detailed information at the " + a href="#vulnerabilities" { "bottom"} "." + } + } + } @else if analysis_outcome.any_dev_issues() { (render_dev_dependency_box(&analysis_outcome)) } - @for (crate_name, deps) in analysis_outcome.crates { + @for (crate_name, deps) in &analysis_outcome.crates { (dependency_tables(crate_name, deps)) } + + @if analysis_outcome.any_insecure() { + (vulnerability_list(&analysis_outcome)) + } } } (super::render_footer(Some(analysis_outcome.duration)))