Display rustsec information on page (#96)

* chore: Bump rustsec version

* feat: display RustSec CVEs at the bottom

This closes #75.

* fix: Reduce complexity and remove duplicate advisories
This commit is contained in:
Felix Suchert 2021-02-01 19:46:26 +01:00 committed by GitHub
parent 3e77c30ada
commit 7ebffe019f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 201 additions and 28 deletions

67
Cargo.lock generated
View file

@ -142,7 +142,6 @@ dependencies = [
"libc", "libc",
"num-integer", "num-integer",
"num-traits", "num-traits",
"serde",
"time", "time",
"winapi", "winapi",
] ]
@ -417,6 +416,15 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.1.15" version = "0.1.15"
@ -535,6 +543,22 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" 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]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.2" version = "0.14.2"
@ -1047,6 +1071,18 @@ dependencies = [
"unicode-xid", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.7" version = "1.0.7"
@ -1187,23 +1223,25 @@ dependencies = [
[[package]] [[package]]
name = "rustsec" name = "rustsec"
version = "0.22.2" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5982d0d4f57176e3e8d62452a5d6dab98906ee5ab4ad1cb63c0877f0a16ab0e" checksum = "7989ff58001a2be1c17945e53d32fcad5cf33c638a4b1239d6e1c9aff7a5d2f8"
dependencies = [ dependencies = [
"cargo-lock", "cargo-lock",
"chrono",
"crates-index 0.16.2", "crates-index 0.16.2",
"cvss", "cvss",
"fs-err", "fs-err",
"git2", "git2",
"home", "home",
"humantime",
"humantime-serde",
"platforms", "platforms",
"semver 0.11.0", "semver 0.11.0",
"serde", "serde",
"smol_str", "smol_str",
"thiserror", "thiserror",
"toml", "toml",
"url",
] ]
[[package]] [[package]]
@ -1377,6 +1415,7 @@ dependencies = [
"maud", "maud",
"once_cell", "once_cell",
"pin-project 1.0.2", "pin-project 1.0.2",
"pulldown-cmark",
"relative-path", "relative-path",
"reqwest", "reqwest",
"route-recognizer", "route-recognizer",
@ -1456,9 +1495,9 @@ dependencies = [
[[package]] [[package]]
name = "smol_str" name = "smol_str"
version = "0.1.16" version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f7909a1d8bc166a862124d84fdc11bda0ea4ed3157ccca662296919c2972db1" checksum = "6ca0f7ce3a29234210f0f4f0b56f8be2e722488b95cb522077943212da3b32eb"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -1706,6 +1745,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" 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]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.4" version = "0.3.4"
@ -1724,6 +1772,12 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unicode-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.1" version = "0.2.1"
@ -1740,6 +1794,7 @@ dependencies = [
"idna", "idna",
"matches", "matches",
"percent-encoding", "percent-encoding",
"serde",
] ]
[[package]] [[package]]

View file

@ -21,11 +21,12 @@ hyper = { version = "0.14", features = ["full"] }
indexmap = { version = "1", features = ["serde-1"] } indexmap = { version = "1", features = ["serde-1"] }
lru_time_cache = "0.11.1" lru_time_cache = "0.11.1"
maud = "0.22.1" maud = "0.22.1"
pulldown-cmark = "0.8"
once_cell = "1" once_cell = "1"
pin-project = "1" pin-project = "1"
relative-path = { version = "1.3", features = ["serde"] } relative-path = { version = "1.3", features = ["serde"] }
route-recognizer = "0.3" route-recognizer = "0.3"
rustsec = "0.22" rustsec = "0.23"
crates-index = "0.15" # held back by semver 0.11 crates-index = "0.15" # held back by semver 0.11
semver = { version = "0.10", features = ["serde"] } # held at 0.10 due to #63 semver = { version = "0.10", features = ["serde"] } # held at 0.10 due to #63
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }

View file

@ -42,11 +42,12 @@ impl DependencyAnalyzer {
let version: cargo_lock::Version = ver.to_string().parse().unwrap(); let version: cargo_lock::Version = ver.to_string().parse().unwrap();
let query = database::Query::new().package_version(name, version); let query = database::Query::new().package_version(name, version);
if !advisory_db if let Some(db) = advisory_db {
.map(|db| db.query(&query).is_empty()) let vulnerabilities = db.query(&query);
.unwrap_or(true) if !vulnerabilities.is_empty() {
{ dep.vulnerabilities =
dep.insecure = true; vulnerabilities.into_iter().map(|v| v.to_owned()).collect();
}
} }
} }
if !ver.is_prerelease() { if !ver.is_prerelease() {

View file

@ -3,6 +3,7 @@ use std::{borrow::Borrow, str::FromStr};
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use indexmap::IndexMap; use indexmap::IndexMap;
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use rustsec::Advisory;
use semver::{Version, VersionReq}; use semver::{Version, VersionReq};
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@ -89,7 +90,7 @@ pub struct AnalyzedDependency {
pub required: VersionReq, pub required: VersionReq,
pub latest_that_matches: Option<Version>, pub latest_that_matches: Option<Version>,
pub latest: Option<Version>, pub latest: Option<Version>,
pub insecure: bool, pub vulnerabilities: Vec<Advisory>,
} }
impl AnalyzedDependency { impl AnalyzedDependency {
@ -98,10 +99,14 @@ impl AnalyzedDependency {
required, required,
latest_that_matches: None, latest_that_matches: None,
latest: None, latest: None,
insecure: false, vulnerabilities: Vec::new(),
} }
} }
pub fn is_insecure(&self) -> bool {
!self.vulnerabilities.is_empty()
}
pub fn is_outdated(&self) -> bool { pub fn is_outdated(&self) -> bool {
self.latest > self.latest_that_matches self.latest > self.latest_that_matches
} }
@ -181,8 +186,16 @@ impl AnalyzedDependencies {
/// Returns the number of insecure main and build dependencies /// Returns the number of insecure main and build dependencies
pub fn count_insecure(&self) -> usize { pub fn count_insecure(&self) -> usize {
let main_insecure = self.main.iter().filter(|&(_, dep)| dep.insecure).count(); let main_insecure = self
let build_insecure = self.build.iter().filter(|&(_, dep)| dep.insecure).count(); .main
.iter()
.filter(|&(_, dep)| dep.is_insecure())
.count();
let build_insecure = self
.build
.iter()
.filter(|&(_, dep)| dep.is_insecure())
.count();
main_insecure + build_insecure main_insecure + build_insecure
} }
@ -203,14 +216,17 @@ impl AnalyzedDependencies {
/// Counts the number of insecure `dev-dependencies` /// Counts the number of insecure `dev-dependencies`
pub fn count_dev_insecure(&self) -> usize { 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. /// Returns `true` if any dev-dependencies are either insecure or outdated.
pub fn any_dev_issues(&self) -> bool { pub fn any_dev_issues(&self) -> bool {
self.dev self.dev
.iter() .iter()
.any(|(_, dep)| dep.is_outdated() || dep.insecure) .any(|(_, dep)| dep.is_outdated() || dep.is_insecure())
} }
} }

View file

@ -1,6 +1,8 @@
use hyper::{Body, Response}; use hyper::{Body, Response};
use indexmap::IndexMap; 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 semver::Version;
use crate::engine::AnalyzeDependenciesOutcome; use crate::engine::AnalyzeDependenciesOutcome;
@ -17,7 +19,7 @@ fn get_crates_version_url(name: impl AsRef<str>, version: &Version) -> String {
format!("https://crates.io/crates/{}/{}", name.as_ref(), version) 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! { html! {
h2 class="title is-3" { h2 class="title is-3" {
"Crate " "Crate "
@ -29,22 +31,22 @@ fn dependency_tables(crate_name: CrateName, deps: AnalyzedDependencies) -> Marku
} }
@if !deps.main.is_empty() { @if !deps.main.is_empty() {
(dependency_table("Dependencies", deps.main)) (dependency_table("Dependencies", &deps.main))
} }
@if !deps.dev.is_empty() { @if !deps.dev.is_empty() {
(dependency_table("Dev dependencies", deps.dev)) (dependency_table("Dev dependencies", &deps.dev))
} }
@if !deps.build.is_empty() { @if !deps.build.is_empty() {
(dependency_table("Build dependencies", deps.build)) (dependency_table("Build dependencies", &deps.build))
} }
} }
} }
fn dependency_table(title: &str, deps: IndexMap<CrateName, AnalyzedDependency>) -> Markup { fn dependency_table(title: &str, deps: &IndexMap<CrateName, AnalyzedDependency>) -> Markup {
let count_total = deps.len(); 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(); let count_outdated = deps.iter().filter(|&(_, dep)| dep.is_outdated()).count();
html! { html! {
@ -86,7 +88,7 @@ fn dependency_table(title: &str, deps: IndexMap<CrateName, AnalyzedDependency>)
} }
} }
td class="has-text-right" { td class="has-text-right" {
@if dep.insecure { @if dep.is_insecure() {
span class="tag is-danger" { "insecure" } span class="tag is-danger" { "insecure" }
} @else if dep.is_outdated() { } @else if dep.is_outdated() {
span class="tag is-warning" { "out of date" } 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>> -> 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 { fn render_failure(subject_path: SubjectPath) -> Markup {
html! { html! {
section class="hero is-light" { section class="hero is-light" {
@ -220,12 +308,24 @@ fn render_success(
} }
section class="section" { section class="section" {
div class="container" { 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)) (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)) (dependency_tables(crate_name, deps))
} }
@if analysis_outcome.any_insecure() {
(vulnerability_list(&analysis_outcome))
}
} }
} }
(super::render_footer(Some(analysis_outcome.duration))) (super::render_footer(Some(analysis_outcome.duration)))