From 6cd7256ee8f186b3269f8cce54575302001eafcb Mon Sep 17 00:00:00 2001 From: Eduardo Pinho Date: Sun, 5 Sep 2021 08:51:10 +0100 Subject: [PATCH] Only query advisory database on latest matching version (#98) * Add methods to check always insecure dependencies Unlike checks for `_insecure`, `always_insecure_ only accounts for vulnerabilities not patched in the latest version in the range * Update status renders to show "maybe insecure" - show always insecure dependencies as insecure, and remaining ones as "possibly insecure" - show warning sign on all dependencies with possible vulnerability - tweak security banner in case all insecure dependencies are "possibly insecure" * Update badge renderer to show "maybe insecure" - only show the red "inscure" if >=1 dependency is always insecure - show "possibly insecure" if all are up to date but might be vulnerable * Update status renderer - more complete counts per project * Format code * Extend banner to explain what "maybe insecure" means --- src/engine/mod.rs | 7 ++++++ src/models/crates.rs | 34 ++++++++++++++++++++++++++ src/server/views/badge.rs | 18 ++++++++++---- src/server/views/html/status.rs | 43 ++++++++++++++++++++++++++------- 4 files changed, 88 insertions(+), 14 deletions(-) diff --git a/src/engine/mod.rs b/src/engine/mod.rs index c2aaa0a..771f579 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -108,6 +108,13 @@ impl AnalyzeDependenciesOutcome { .any(|&(_, ref deps)| deps.count_insecure() > 0) } + /// Checks if any always insecure main or build dependencies exist in the scanned crates + pub fn any_always_insecure(&self) -> bool { + self.crates + .iter() + .any(|&(_, ref deps)| deps.count_always_insecure() > 0) + } + /// Checks if any dev-dependencies in the scanned crates are either outdated or insecure pub fn any_dev_issues(&self) -> bool { self.crates diff --git a/src/models/crates.rs b/src/models/crates.rs index 96a647f..3af5cdf 100644 --- a/src/models/crates.rs +++ b/src/models/crates.rs @@ -103,10 +103,27 @@ impl AnalyzedDependency { } } + /// Check whether this dependency has at least one known vulnerability + /// in any version in the required version range. + /// + /// Note that the vulnerability may (or not) already be patched + /// in the latest version(s) in the range. pub fn is_insecure(&self) -> bool { !self.vulnerabilities.is_empty() } + /// Check whether this dependency has at laest one known vulnerability + /// even when the latest version in the required range is used. + pub fn is_always_insecure(&self) -> bool { + if let Some(latest) = &self.latest { + self.vulnerabilities + .iter() + .any(|a| a.versions.is_vulnerable(latest)) + } else { + self.is_insecure() + } + } + pub fn is_outdated(&self) -> bool { self.latest > self.latest_that_matches } @@ -199,6 +216,23 @@ impl AnalyzedDependencies { main_insecure + build_insecure } + /// Returns the number of main and build dependencies + /// which are vulnerable to security issues, + /// even they are updated to the latest version in the required range. + pub fn count_always_insecure(&self) -> usize { + let main_insecure = self + .main + .iter() + .filter(|&(_, dep)| dep.is_always_insecure()) + .count(); + let build_insecure = self + .build + .iter() + .filter(|&(_, dep)| dep.is_always_insecure()) + .count(); + main_insecure + build_insecure + } + /// Checks if any outdated main or build dependencies exist pub fn any_outdated(&self) -> bool { let main_any_outdated = self.main.iter().any(|(_, dep)| dep.is_outdated()); diff --git a/src/server/views/badge.rs b/src/server/views/badge.rs index 510c944..4282f1b 100644 --- a/src/server/views/badge.rs +++ b/src/server/views/badge.rs @@ -7,7 +7,7 @@ use crate::engine::AnalyzeDependenciesOutcome; pub fn badge(analysis_outcome: Option<&AnalyzeDependenciesOutcome>) -> Badge { let opts = match analysis_outcome { Some(outcome) => { - if outcome.any_insecure() { + if outcome.any_always_insecure() { BadgeOptions { subject: "dependencies".into(), status: "insecure".into(), @@ -23,10 +23,18 @@ pub fn badge(analysis_outcome: Option<&AnalyzeDependenciesOutcome>) -> Badge { color: "#dfb317".into(), } } else if total > 0 { - BadgeOptions { - subject: "dependencies".into(), - status: "up to date".into(), - color: "#4c1".into(), + if outcome.any_insecure() { + BadgeOptions { + subject: "dependencies".into(), + status: "maybe insecure".into(), + color: "#8b1".into(), + } + } else { + BadgeOptions { + subject: "dependencies".into(), + status: "up to date".into(), + color: "#4c1".into(), + } } } else { BadgeOptions { diff --git a/src/server/views/html/status.rs b/src/server/views/html/status.rs index 950aa03..7e21506 100644 --- a/src/server/views/html/status.rs +++ b/src/server/views/html/status.rs @@ -47,6 +47,10 @@ fn dependency_tables(crate_name: &CrateName, deps: &AnalyzedDependencies) -> Mar fn dependency_table(title: &str, deps: &IndexMap) -> Markup { let count_total = deps.len(); + let count_always_insecure = deps + .iter() + .filter(|&(_, dep)| dep.is_always_insecure()) + .count(); let count_insecure = deps.iter().filter(|&(_, dep)| dep.is_insecure()).count(); let count_outdated = deps.iter().filter(|&(_, dep)| dep.is_outdated()).count(); @@ -55,11 +59,15 @@ fn dependency_table(title: &str, deps: &IndexMap) html! { h3 class="title is-4" { (title) } p class="subtitle is-5" { - (match (count_outdated, count_insecure) { - (0, 0) => format!("({} total, all up-to-date)", count_total), - (0, _) => format!("({} total, {} insecure)", count_total, count_insecure), - (_, 0) => format!("({} total, {} outdated)", count_total, count_outdated), - (_, _) => format!("({} total, {} outdated, {} insecure)", count_total, count_outdated, count_insecure), + (match (count_outdated, count_always_insecure, count_insecure - count_always_insecure) { + (0, 0, 0) => format!("({} total, all up-to-date)", count_total), + (0, 0, c) => format!("({} total, {} possibly insecure)", count_total, c), + (_, 0, 0) => format!("({} total, {} outdated)", count_total, count_outdated), + (0, _, 0) => format!("({} total, {} insecure)", count_total, count_always_insecure), + (0, _, c) => format!("({} total, {} insecure, {} possibly insecure)", count_total, count_always_insecure, c), + (_, 0, c) => format!("({} total, {} outdated, {} possibly insecure)", count_total, count_outdated, c), + (_, _, 0) => format!("({} total, {} outdated, {} insecure)", count_total, count_outdated, count_always_insecure), + (_, _, c) => format!("({} total, {} outdated, {} insecure, {} possibly insecure)", count_total, count_outdated, count_always_insecure, c), }) } @@ -81,6 +89,11 @@ fn dependency_table(title: &str, deps: &IndexMap) } { "\u{00A0}" } // non-breaking space a href=(dep.deps_rs_path(name.as_ref())) { (name.as_ref()) } + + @if dep.is_insecure() { + { "\u{00A0}" } // non-breaking space + a href="#vulnerabilities" title="has known vulnerabilities" { "⚠️" } + } } td class="has-text-right" { code { (dep.required.to_string()) } } td class="has-text-right" { @@ -91,10 +104,12 @@ fn dependency_table(title: &str, deps: &IndexMap) } } td class="has-text-right" { - @if dep.is_insecure() { + @if dep.is_always_insecure() { span class="tag is-danger" { "insecure" } } @else if dep.is_outdated() { span class="tag is-warning" { "out of date" } + } @else if dep.is_insecure() { + span class="tag is-warning" { "maybe insecure" } } @else { span class="tag is-success" { "up to date" } } @@ -288,9 +303,9 @@ fn render_success( let status_data_uri = badge::badge(Some(&analysis_outcome)).to_svg_data_uri(); - let hero_class = if analysis_outcome.any_insecure() { + let hero_class = if analysis_outcome.any_always_insecure() { "is-danger" - } else if analysis_outcome.any_outdated() { + } else if analysis_outcome.any_insecure() || analysis_outcome.any_outdated() { "is-warning" } else { "is-success" @@ -318,7 +333,7 @@ fn render_success( } section class="section" { div class="container" { - @if analysis_outcome.any_insecure() { + @if analysis_outcome.any_always_insecure() { div class="notification is-warning" { p { "This project contains " b { "known security vulnerabilities" } @@ -326,6 +341,16 @@ fn render_success( a href="#vulnerabilities" { "bottom"} "." } } + } @else if analysis_outcome.any_insecure() { + div class="notification is-warning" { + p { "This project might be open to " + b { "known security vulnerabilities" } + ", which can be prevented by tightening " + "the version range of affected dependencies. " + "Find detailed information at the " + a href="#vulnerabilities" { "bottom"} "." + } + } } @else if analysis_outcome.any_dev_issues() { (render_dev_dependency_box(&analysis_outcome)) }