diff --git a/csaf/Cargo.toml b/csaf/Cargo.toml index f1a1043..d819e8c 100644 --- a/csaf/Cargo.toml +++ b/csaf/Cargo.toml @@ -40,7 +40,6 @@ walker-common = { version = "0.6.0-alpha.7", path = "../common" } sequoia-openpgp = { version = "1", default-features = false } csaf = { version = "0.5", default-features = false, optional = true } -clap = "4.4.8" html-escape = "0.2.13" [features] diff --git a/csaf/csaf-cli/Cargo.toml b/csaf/csaf-cli/Cargo.toml index 56d64f1..9690c51 100644 --- a/csaf/csaf-cli/Cargo.toml +++ b/csaf/csaf-cli/Cargo.toml @@ -20,7 +20,6 @@ comrak = { version = "0.20.0" } csaf = { version = "0.5.0", default-features = false } env_logger = "0.10.0" flexible-time = "0.1.1" -html-escape = "0.2.13" humantime = "2" indicatif = { version = "0.17.3", features = [] } indicatif-log-bridge = "0.2.1" diff --git a/csaf/csaf-cli/src/cmd/report/mod.rs b/csaf/csaf-cli/src/cmd/report/mod.rs index 98152e6..76f81ae 100644 --- a/csaf/csaf-cli/src/cmd/report/mod.rs +++ b/csaf/csaf-cli/src/cmd/report/mod.rs @@ -5,7 +5,7 @@ use crate::{ use async_trait::async_trait; use csaf_walker::{ discover::{AsDiscovered, DiscoveredAdvisory, DiscoveredContext, DiscoveredVisitor}, - report::{render, DocumentKey, Duplicates, ReportResult}, + report::{render_to_html, DocumentKey, Duplicates, ReportRenderOption, ReportResult}, retrieve::RetrievingVisitor, validation::{ValidatedAdvisory, ValidationError, ValidationVisitor}, verification::{ @@ -159,7 +159,15 @@ impl Report { fn render(render: RenderOptions, report: ReportResult) -> anyhow::Result<()> { let mut out = std::fs::File::create(&render.output)?; - render::render_to_html(&mut out, &report, render.output, render.base_url)?; + + render_to_html( + &mut out, + &report, + ReportRenderOption { + output: render.output, + base_url: render.base_url, + }, + )?; Ok(()) } diff --git a/csaf/src/lib.rs b/csaf/src/lib.rs index 5ff3997..72a0cbf 100644 --- a/csaf/src/lib.rs +++ b/csaf/src/lib.rs @@ -55,12 +55,11 @@ pub mod discover; pub mod model; +pub mod report; pub mod retrieve; pub mod source; pub mod validation; -pub mod visitors; -pub mod walker; - -pub mod report; #[cfg(feature = "csaf")] pub mod verification; +pub mod visitors; +pub mod walker; diff --git a/csaf/src/report/mod.rs b/csaf/src/report/mod.rs index 7ce101a..67177dd 100644 --- a/csaf/src/report/mod.rs +++ b/csaf/src/report/mod.rs @@ -1,9 +1,10 @@ -pub mod render; - use crate::discover::DiscoveredAdvisory; use std::borrow::Cow; use std::collections::{BTreeMap, HashSet}; +use std::fmt::{Display, Formatter}; +use std::path::PathBuf; use url::Url; +use walker_common::report; use walker_common::utils::url::Urlify; #[derive(Clone, Debug)] @@ -36,3 +37,307 @@ impl DocumentKey { } } } + +#[derive(Clone, Debug)] +pub struct ReportRenderOption { + pub output: PathBuf, + + pub base_url: Option, +} + +pub fn render_to_html( + out: &mut W, + report: &ReportResult, + options: ReportRenderOption, +) -> anyhow::Result<()> { + report::render( + out, + "CSAF Report", + HtmlReport { + result: report, + base_url: &options.base_url, + }, + &Default::default(), + )?; + + Ok(()) +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Title { + Duplicates, + Warnings, + Errors, +} + +impl Display for Title { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Duplicates => f.write_str("Duplicates"), + Self::Warnings => f.write_str("Warnings"), + Self::Errors => f.write_str("Errors"), + } + } +} + +struct HtmlReport<'r> { + result: &'r ReportResult<'r>, + base_url: &'r Option, +} + +impl HtmlReport<'_> { + fn render_duplicates(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let count = self.result.duplicates.duplicates.len(); + let data = |f: &mut Formatter<'_>| { + for (k, v) in &self.result.duplicates.duplicates { + let (_url, label) = self.link_document(k); + writeln!( + f, + r#" + + {label} + {v} + + "#, + label = html_escape::encode_text(&label), + )?; + } + Ok(()) + }; + + if !self.result.duplicates.duplicates.is_empty() { + let total: usize = self.result.duplicates.duplicates.values().sum(); + + Self::render_table( + f, + count, + Title::Duplicates, + format!( + "{:?} duplicates URLs found, totalling {:?} redundant entries", + count, total + ) + .as_str(), + data, + )?; + } + Ok(()) + } + + fn render_errors(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let count = self.result.errors.len(); + let data = |f: &mut Formatter<'_>| { + for (k, v) in self.result.errors { + let (url, label) = self.link_document(k); + + writeln!( + f, + r#" + + {label} + {v} + + "#, + url = html_escape::encode_quoted_attribute(&url), + label = html_escape::encode_text(&label), + v = html_escape::encode_text(&v), + )?; + } + Ok(()) + }; + Self::render_table( + f, + count, + Title::Errors, + format!("{:?} error(s) detected", count).as_str(), + data, + )?; + Ok(()) + } + + fn render_table( + f: &mut Formatter<'_>, + count: usize, + title: Title, + sub_title: &str, + data: F, + ) -> std::fmt::Result + where + F: Fn(&mut Formatter<'_>) -> std::fmt::Result, + { + Self::title(f, title, count)?; + writeln!(f, "

{sub_title}

")?; + writeln!( + f, + r#" + + + + + + + + + +"# + )?; + data(f)?; + writeln!(f, "
File{title}
")?; + + Ok(()) + } + + fn render_warnings(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut count = 0; + for warnings in self.result.warnings.values() { + count += warnings.len(); + } + + let data = |f: &mut Formatter<'_>| { + for (k, v) in self.result.warnings { + let (url, label) = self.link_document(k); + + writeln!( + f, + r#" + + {label} +
    +"#, + url = html_escape::encode_quoted_attribute(&url), + label = html_escape::encode_text(&label), + )?; + + for text in v { + writeln!( + f, + r#" +
  • + {v} +
  • + "#, + v = html_escape::encode_text(&text), + )?; + } + + writeln!( + f, + r#" +
+ + +"# + )?; + } + + Ok(()) + }; + Self::render_table( + f, + count, + Title::Warnings, + format!("{:?} warning(s) detected", count).as_str(), + data, + )?; + Ok(()) + } + + fn gen_link(&self, key: &DocumentKey) -> Option<(String, String)> { + let label = key.url.clone(); + + // the full URL of the document + let url = key.distribution_url.join(&key.url).ok()?; + + let url = match &self.base_url { + Some(base_url) => base_url + .make_relative(&url) + .unwrap_or_else(|| url.to_string()), + None => url.to_string(), + }; + + Some((url, label)) + } + + /// create a link towards a document, returning url and label + fn link_document(&self, key: &DocumentKey) -> (String, String) { + self.gen_link(key) + .unwrap_or_else(|| (key.url.clone(), key.url.clone())) + } + + fn title(f: &mut Formatter<'_>, title: Title, count: usize) -> std::fmt::Result { + write!(f, "

{title}")?; + + let (class, text) = if count > 0 { + ( + match title { + Title::Warnings => "text-bg-warning", + _ => "text-bg-danger", + }, + count.to_string(), + ) + } else { + ("text-bg-light", "None".to_string()) + }; + + write!( + f, + r#" {text}"#, + )?; + + writeln!(f, "

")?; + + Ok(()) + } + + fn render_total(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + r#" +

Summary

+
+
Total
+
{total}
+
+"#, + total = self.result.total + ) + } +} + +impl<'r> Display for HtmlReport<'r> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.render_total(f)?; + self.render_duplicates(f)?; + self.render_errors(f)?; + self.render_warnings(f)?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use reqwest::Url; + + #[test] + fn test_link() { + let details = ReportResult { + total: 0, + duplicates: &Default::default(), + errors: &Default::default(), + warnings: &Default::default(), + }; + let _output = PathBuf::default(); + let base_url = Some(Url::parse("file:///foo/bar/").unwrap()); + let report = HtmlReport { + result: &details, + base_url: &base_url, + }; + + let (url, _label) = report.link_document(&DocumentKey { + distribution_url: Url::parse("file:///foo/bar/distribution/").unwrap(), + url: "2023/cve.json".to_string(), + }); + + assert_eq!(url, "distribution/2023/cve.json"); + } +} diff --git a/csaf/src/report/render.rs b/csaf/src/report/render.rs deleted file mode 100644 index ffde720..0000000 --- a/csaf/src/report/render.rs +++ /dev/null @@ -1,295 +0,0 @@ -use crate::report::DocumentKey; -use crate::report::ReportResult; -use std::fmt::{Display, Formatter}; -use std::path::PathBuf; -use url::Url; -use walker_common::report; - -pub fn render_to_html( - out: &mut W, - report: &ReportResult, - output: PathBuf, - base_url: Option, -) -> anyhow::Result<()> { - report::render( - out, - "CSAF Report", - HtmlReport(report, &output, &base_url), - &Default::default(), - )?; - - Ok(()) -} - -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum Title { - Duplicates, - Warnings, - Errors, -} - -impl Display for Title { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::Duplicates => f.write_str("Duplicates"), - Self::Warnings => f.write_str("Warnings"), - Self::Errors => f.write_str("Errors"), - } - } -} - -struct HtmlReport<'r>(&'r ReportResult<'r>, &'r PathBuf, &'r Option); - -impl HtmlReport<'_> { - fn render_duplicates(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let count = self.0.duplicates.duplicates.len(); - let data = |f: &mut Formatter<'_>| { - for (k, v) in &self.0.duplicates.duplicates { - let (_url, label) = self.link_document(k); - writeln!( - f, - r#" - - {label} - {v} - - "#, - label = html_escape::encode_text(&label), - )?; - } - Ok(()) - }; - - if !self.0.duplicates.duplicates.is_empty() { - let total: usize = self.0.duplicates.duplicates.values().sum(); - - Self::render_table( - f, - count, - Title::Duplicates, - format!( - "{:?} duplicates URLs found, totalling {:?} redundant entries", - count, total - ) - .as_str(), - data, - )?; - } - Ok(()) - } - - fn render_errors(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let count = self.0.errors.len(); - let data = |f: &mut Formatter<'_>| { - for (k, v) in self.0.errors { - let (url, label) = self.link_document(k); - - writeln!( - f, - r#" - - {label} - {v} - - "#, - url = html_escape::encode_quoted_attribute(&url), - label = html_escape::encode_text(&label), - v = html_escape::encode_text(&v), - )?; - } - Ok(()) - }; - Self::render_table( - f, - count, - Title::Errors, - format!("{:?} error(s) detected", count).as_str(), - data, - )?; - Ok(()) - } - - fn render_table( - f: &mut Formatter<'_>, - count: usize, - title: Title, - sub_title: &str, - data: F, - ) -> std::fmt::Result - where - F: Fn(&mut Formatter<'_>) -> std::fmt::Result, - { - Self::title(f, title, count)?; - writeln!(f, "

{sub_title}

")?; - writeln!( - f, - r#" - - - - - - - - - -"# - )?; - data(f)?; - writeln!(f, "
File{title}
")?; - - Ok(()) - } - - fn render_warnings(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut count = 0; - for warnings in self.0.warnings.values() { - count += warnings.len(); - } - - let data = |f: &mut Formatter<'_>| { - for (k, v) in self.0.warnings { - let (url, label) = self.link_document(k); - - writeln!( - f, - r#" - - {label} -
    -"#, - url = html_escape::encode_quoted_attribute(&url), - label = html_escape::encode_text(&label), - )?; - - for text in v { - writeln!( - f, - r#" -
  • - {v} -
  • - "#, - v = html_escape::encode_text(&text), - )?; - } - - writeln!( - f, - r#" -
- - -"# - )?; - } - - Ok(()) - }; - Self::render_table( - f, - count, - Title::Warnings, - format!("{:?} warning(s) detected", count).as_str(), - data, - )?; - Ok(()) - } - - fn gen_link(&self, key: &DocumentKey) -> Option<(String, String)> { - let label = key.url.clone(); - - // the full URL of the document - let url = key.distribution_url.join(&key.url).ok()?; - - let url = match &self.2 { - Some(base_url) => base_url - .make_relative(&url) - .unwrap_or_else(|| url.to_string()), - None => url.to_string(), - }; - - Some((url, label)) - } - - /// create a link towards a document, returning url and label - fn link_document(&self, key: &DocumentKey) -> (String, String) { - self.gen_link(key) - .unwrap_or_else(|| (key.url.clone(), key.url.clone())) - } - - fn title(f: &mut Formatter<'_>, title: Title, count: usize) -> std::fmt::Result { - write!(f, "

{title}")?; - - let (class, text) = if count > 0 { - ( - match title { - Title::Warnings => "text-bg-warning", - _ => "text-bg-danger", - }, - count.to_string(), - ) - } else { - ("text-bg-light", "None".to_string()) - }; - - write!( - f, - r#" {text}"#, - )?; - - writeln!(f, "

")?; - - Ok(()) - } - - fn render_total(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!( - f, - r#" -

Summary

-
-
Total
-
{total}
-
-"#, - total = self.0.total - ) - } -} - -impl<'r> Display for HtmlReport<'r> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.render_total(f)?; - self.render_duplicates(f)?; - self.render_errors(f)?; - self.render_warnings(f)?; - Ok(()) - } -} - -#[cfg(test)] -mod test { - use super::*; - use reqwest::Url; - - #[test] - fn test_link() { - let details = ReportResult { - total: 0, - duplicates: &Default::default(), - errors: &Default::default(), - warnings: &Default::default(), - }; - let output = Default::default(); - let base_url = Some(Url::parse("file:///foo/bar/").unwrap()); - let report = HtmlReport(&details, &output, &base_url); - - let (url, _label) = report.link_document(&DocumentKey { - distribution_url: Url::parse("file:///foo/bar/distribution/").unwrap(), - url: "2023/cve.json".to_string(), - }); - - assert_eq!(url, "distribution/2023/cve.json"); - } -}