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
This commit is contained in:
Eduardo Pinho 2021-09-05 08:51:10 +01:00 committed by GitHub
parent 50d81a7a79
commit 6cd7256ee8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 88 additions and 14 deletions

View file

@ -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

View file

@ -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());

View file

@ -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,11 +23,19 @@ pub fn badge(analysis_outcome: Option<&AnalyzeDependenciesOutcome>) -> Badge {
color: "#dfb317".into(),
}
} else if total > 0 {
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 {
subject: "dependencies".into(),

View file

@ -47,6 +47,10 @@ fn dependency_tables(crate_name: &CrateName, deps: &AnalyzedDependencies) -> Mar
fn dependency_table(title: &str, deps: &IndexMap<CrateName, AnalyzedDependency>) -> 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<CrateName, AnalyzedDependency>)
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<CrateName, AnalyzedDependency>)
}
{ "\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<CrateName, AnalyzedDependency>)
}
}
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))
}