From af9271690f9e780e9e3607fe572910674659e42a Mon Sep 17 00:00:00 2001 From: Metbcy Date: Sat, 9 May 2026 22:19:12 +0000 Subject: [PATCH 1/2] feat(render): add self-contained HTML report output --- src/cli.rs | 1 + src/render/html.rs | 278 +++++++++++++++++++++++++++++++++++++++++++++ src/render/mod.rs | 1 + 3 files changed, 280 insertions(+) create mode 100644 src/render/html.rs diff --git a/src/cli.rs b/src/cli.rs index e85dcfd..b3c63c4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -524,6 +524,7 @@ pub enum OutputFormat { Markdown, Json, Sarif, + Html, } #[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq, Deserialize)] diff --git a/src/render/html.rs b/src/render/html.rs new file mode 100644 index 0000000..0b4fbcc --- /dev/null +++ b/src/render/html.rs @@ -0,0 +1,278 @@ +use std::fmt::Write; + +use crate::diff::ChangeSet; +use crate::enrich::Enrichment; +use crate::model::Component; + +const CSS: &str = r#" +:root { + --bg: #1a1b26; + --bg-surface: #24283b; + --bg-hover: #292e42; + --text: #c0caf5; + --text-muted: #565f89; + --accent: #7aa2f7; + --green: #9ece6a; + --red: #f7768e; + --yellow: #e0af68; + --orange: #ff9e64; + --purple: #bb9af7; + --cyan: #7dcfff; + --border: #3b4261; + --radius: 8px; +} +* { margin: 0; padding: 0; box-sizing: border-box; } +body { + background: var(--bg); + color: var(--text); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace; + padding: 2rem; + line-height: 1.6; +} +h1 { color: var(--accent); margin-bottom: 1.5rem; font-size: 1.8rem; } +h2 { color: var(--accent); font-size: 1.2rem; } +table { + width: 100%; border-collapse: collapse; margin: 0.5rem 0; + background: var(--bg-surface); border-radius: var(--radius); + overflow: hidden; +} +th { background: var(--bg-hover); color: var(--accent); text-align: left; padding: 0.6rem 1rem; font-size: 0.85rem; } +td { padding: 0.5rem 1rem; border-top: 1px solid var(--border); font-size: 0.85rem; } +tr:hover td { background: var(--bg-hover); } +details { + background: var(--bg-surface); border: 1px solid var(--border); + border-radius: var(--radius); margin-bottom: 1rem; overflow: hidden; +} +summary { + padding: 0.8rem 1rem; cursor: pointer; font-weight: 600; + user-select: none; display: flex; align-items: center; gap: 0.5rem; +} +summary:hover { background: var(--bg-hover); } +summary::marker { color: var(--accent); } +.badge { + display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px; + font-size: 0.75rem; font-weight: 700; text-transform: uppercase; +} +.badge-critical { background: var(--red); color: var(--bg); } +.badge-high { background: var(--orange); color: var(--bg); } +.badge-medium { background: var(--yellow); color: var(--bg); } +.badge-low { background: var(--cyan); color: var(--bg); } +.badge-green { background: var(--green); color: var(--bg); } +.badge-red { background: var(--red); color: var(--bg); } +.badge-purple { background: var(--purple); color: var(--bg); } +.badge-count { + background: var(--border); color: var(--text); padding: 0.1rem 0.45rem; + border-radius: 10px; font-size: 0.75rem; margin-left: auto; +} +.summary-grid { + display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; margin-bottom: 1.5rem; +} +.stat-card { + background: var(--bg-surface); border: 1px solid var(--border); + border-radius: var(--radius); padding: 1rem; text-align: center; +} +.stat-card .value { font-size: 2rem; font-weight: 700; color: var(--accent); } +.stat-card .label { font-size: 0.8rem; color: var(--text-muted); } +.content { padding: 0 1rem 1rem; } +"#; + +fn esc(s: &str) -> String { + s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """) +} + +fn severity_badge(sev: &impl std::fmt::Display) -> String { + let s = sev.to_string(); + let cls = match s.to_lowercase().as_str() { + "critical" => "badge-critical", + "high" => "badge-high", + "medium" => "badge-medium", + "low" => "badge-low", + _ => "badge-purple", + }; + format!(r#"{}"#, cls, esc(&s)) +} + +fn comp_row(c: &Component) -> String { + format!( + "{}{}{}", + esc(&c.name), esc(&c.version), esc(&c.ecosystem.to_string()) + ) +} + +fn section(out: &mut String, title: &str, count: usize, badge_cls: &str, body: F) { + if count == 0 { return; } + let _ = write!(out, + r#"

{}

{} item{}
"#, + esc(title), badge_cls, count, if count == 1 { "" } else { "s" } + ); + body(out); + out.push_str("
"); +} + +pub fn render(cs: &ChangeSet, enrichment: &Enrichment) -> String { + let mut out = String::with_capacity(16384); + + let _ = write!(out, r#"BOMdrift Report"#); + out.push_str("

📦 BOMdrift — SBOM Diff Report

"); + + // Summary cards + let total_vulns: usize = enrichment.vulns.values().map(|v| v.len()).sum(); + out.push_str(r#"
"#); + for (label, value, color) in [ + ("Added", cs.added.len(), "var(--green)"), + ("Removed", cs.removed.len(), "var(--red)"), + ("Version Changed", cs.version_changed.len(), "var(--yellow)"), + ("License Changed", cs.license_changed.len(), "var(--orange)"), + ("Vulnerabilities", total_vulns, "var(--red)"), + ("Typosquats", enrichment.typosquats.len(), "var(--purple)"), + ("VEX Suppressed", enrichment.vex_suppressed_count, "var(--cyan)"), + ] { + if value > 0 { + let _ = write!(out, + r#"
{value}
{label}
"# + ); + } + } + out.push_str("
"); + + // Added + section(&mut out, "Added Components", cs.added.len(), "badge-green", |o| { + o.push_str(""); + for c in &cs.added { o.push_str(&comp_row(c)); } + o.push_str("
NameVersionEcosystem
"); + }); + + // Removed + section(&mut out, "Removed Components", cs.removed.len(), "badge-red", |o| { + o.push_str(""); + for c in &cs.removed { o.push_str(&comp_row(c)); } + o.push_str("
NameVersionEcosystem
"); + }); + + // Version Changed + section(&mut out, "Version Changed", cs.version_changed.len(), "badge-purple", |o| { + o.push_str(""); + for (old, new) in &cs.version_changed { + let _ = write!(o, "", + esc(&old.name), esc(&old.version), esc(&new.version), esc(&old.ecosystem.to_string())); + } + o.push_str("
NameOld VersionNew VersionEcosystem
{}{}{}{}
"); + }); + + // License Changed + section(&mut out, "License Changed", cs.license_changed.len(), "badge-purple", |o| { + o.push_str(""); + for (old, new) in &cs.license_changed { + let _ = write!(o, "", + esc(&old.name), esc(&old.licenses.join(", ")), esc(&new.licenses.join(", "))); + } + o.push_str("
NameOld LicenseNew License
{}{}{}
"); + }); + + // Vulnerabilities + section(&mut out, "Vulnerabilities", total_vulns, "badge-critical", |o| { + o.push_str(""); + for (pkg, advisories) in &enrichment.vulns { + for adv in advisories { + let _ = write!(o, "", + esc(pkg), esc(&adv.id), severity_badge(&adv.severity), + esc(&adv.aliases.join(", "))); + } + } + o.push_str("
ComponentIDSeverityAliases
{}{}{}{}
"); + }); + + // Typosquats + section(&mut out, "Typosquat Candidates", enrichment.typosquats.len(), "badge-critical", |o| { + o.push_str(""); + for t in &enrichment.typosquats { + let _ = write!(o, "", + esc(&t.component.name), esc(&t.closest), t.score); + } + o.push_str("
ComponentSimilar ToScore
{}{}{:.2}
"); + }); + + // Version Jumps + section(&mut out, "Version Jumps", enrichment.version_jumps.len(), "badge-high", |o| { + o.push_str(""); + for v in &enrichment.version_jumps { + let _ = write!(o, "", + esc(&v.before.name), esc(&v.before.version), esc(&v.after.version), v.before_major, v.after_major); + } + o.push_str("
NameOldNewOld MajorNew Major
{}{}{}{}{}
"); + }); + + // Maintainer Age + section(&mut out, "Maintainer Age Warnings", enrichment.maintainer_age.len(), "badge-high", |o| { + o.push_str(""); + for m in &enrichment.maintainer_age { + let _ = write!(o, "", + esc(&m.component.name), esc(&m.top_contributor), m.days_old, esc(&m.first_commit_at)); + } + o.push_str("
ComponentTop ContributorDays OldFirst Commit
{}{}{}{}
"); + }); + + // License Violations + section(&mut out, "License Violations", enrichment.license_violations.len(), "badge-red", |o| { + o.push_str(""); + for lv in &enrichment.license_violations { + let _ = write!(o, "", + esc(&lv.component.name), esc(&lv.license), esc(&lv.matched_rule), lv.kind); + } + o.push_str("
ComponentLicenseMatched RuleKind
{}{}{}{:?}
"); + }); + + // Recently Published + section(&mut out, "Recently Published", enrichment.recently_published.len(), "badge-high", |o| { + o.push_str(""); + for r in &enrichment.recently_published { + let _ = write!(o, "", + esc(&r.component.name), esc(&r.component.version), esc(&r.published_at), r.days_old); + } + o.push_str("
ComponentVersionPublishedDays Old
{}{}{}{}
"); + }); + + // Deprecated + section(&mut out, "Deprecated Packages", enrichment.deprecated.len(), "badge-red", |o| { + o.push_str(""); + for r in &enrichment.deprecated { + let _ = write!(o, "", + esc(&r.component.name), esc(&r.component.version), + esc(r.message.as_deref().unwrap_or(""))); + } + o.push_str("
ComponentVersionMessage
{}{}{}
"); + }); + + // Maintainer Set Changed + section(&mut out, "Maintainer Set Changed", enrichment.maintainer_set_changed.len(), "badge-high", |o| { + o.push_str(""); + for r in &enrichment.maintainer_set_changed { + let _ = write!(o, "", + esc(&r.before.name), esc(&r.before.version), esc(&r.after.version), + esc(&r.added.join(", ")), esc(&r.removed.join(", "))); + } + o.push_str("
PackageOld VersionNew VersionAddedRemoved
{}{}{}{}{}
"); + }); + + // Plugin Findings + section(&mut out, "Plugin Findings", enrichment.plugin_findings.len(), "badge-purple", |o| { + o.push_str(""); + for p in &enrichment.plugin_findings { + let _ = write!(o, "", + esc(&p.plugin_name), esc(&p.component_purl), severity_badge(&format!("{:?}", p.severity)), esc(&p.message)); + } + o.push_str("
PluginComponent PURLSeverityMessage
{}{}{}{}
"); + }); + + // VEX Suppressed + if enrichment.vex_suppressed_count > 0 { + let _ = write!(out, + r#"

VEX Suppressed

{} suppressed

{} vulnerabilities were suppressed by VEX annotations.

"#, + enrichment.vex_suppressed_count, enrichment.vex_suppressed_count + ); + } + + out.push_str(""); + out +} diff --git a/src/render/mod.rs b/src/render/mod.rs index e2af19b..36fe1b5 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -6,4 +6,5 @@ pub mod json; pub mod markdown; pub mod sarif; +pub mod html; pub mod term; From 58a599e0dcb70597341fd6e0c2aad28901c4440e Mon Sep 17 00:00:00 2001 From: Metbcy Date: Sat, 9 May 2026 22:26:48 +0000 Subject: [PATCH 2/2] fix: add Html match arm in run.rs and apply cargo fmt --- src/render/html.rs | 396 ++++++++++++++++++++++++++++++++------------- src/render/mod.rs | 2 +- src/run.rs | 1 + 3 files changed, 286 insertions(+), 113 deletions(-) diff --git a/src/render/html.rs b/src/render/html.rs index 0b4fbcc..9315339 100644 --- a/src/render/html.rs +++ b/src/render/html.rs @@ -78,7 +78,10 @@ summary::marker { color: var(--accent); } "#; fn esc(s: &str) -> String { - s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """) + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) } fn severity_badge(sev: &impl std::fmt::Display) -> String { @@ -96,15 +99,29 @@ fn severity_badge(sev: &impl std::fmt::Display) -> String { fn comp_row(c: &Component) -> String { format!( "{}{}{}", - esc(&c.name), esc(&c.version), esc(&c.ecosystem.to_string()) + esc(&c.name), + esc(&c.version), + esc(&c.ecosystem.to_string()) ) } -fn section(out: &mut String, title: &str, count: usize, badge_cls: &str, body: F) { - if count == 0 { return; } - let _ = write!(out, +fn section( + out: &mut String, + title: &str, + count: usize, + badge_cls: &str, + body: F, +) { + if count == 0 { + return; + } + let _ = write!( + out, r#"

{}

{} item{}
"#, - esc(title), badge_cls, count, if count == 1 { "" } else { "s" } + esc(title), + badge_cls, + count, + if count == 1 { "" } else { "s" } ); body(out); out.push_str("
"); @@ -113,7 +130,10 @@ fn section(out: &mut String, title: &str, count: usize, pub fn render(cs: &ChangeSet, enrichment: &Enrichment) -> String { let mut out = String::with_capacity(16384); - let _ = write!(out, r#"BOMdrift Report"#); + let _ = write!( + out, + r#"BOMdrift Report"# + ); out.push_str("

📦 BOMdrift — SBOM Diff Report

"); // Summary cards @@ -126,10 +146,15 @@ pub fn render(cs: &ChangeSet, enrichment: &Enrichment) -> String { ("License Changed", cs.license_changed.len(), "var(--orange)"), ("Vulnerabilities", total_vulns, "var(--red)"), ("Typosquats", enrichment.typosquats.len(), "var(--purple)"), - ("VEX Suppressed", enrichment.vex_suppressed_count, "var(--cyan)"), + ( + "VEX Suppressed", + enrichment.vex_suppressed_count, + "var(--cyan)", + ), ] { if value > 0 { - let _ = write!(out, + let _ = write!( + out, r#"
{value}
{label}
"# ); } @@ -137,137 +162,284 @@ pub fn render(cs: &ChangeSet, enrichment: &Enrichment) -> String { out.push_str(""); // Added - section(&mut out, "Added Components", cs.added.len(), "badge-green", |o| { - o.push_str(""); - for c in &cs.added { o.push_str(&comp_row(c)); } - o.push_str("
NameVersionEcosystem
"); - }); + section( + &mut out, + "Added Components", + cs.added.len(), + "badge-green", + |o| { + o.push_str(""); + for c in &cs.added { + o.push_str(&comp_row(c)); + } + o.push_str("
NameVersionEcosystem
"); + }, + ); // Removed - section(&mut out, "Removed Components", cs.removed.len(), "badge-red", |o| { - o.push_str(""); - for c in &cs.removed { o.push_str(&comp_row(c)); } - o.push_str("
NameVersionEcosystem
"); - }); + section( + &mut out, + "Removed Components", + cs.removed.len(), + "badge-red", + |o| { + o.push_str(""); + for c in &cs.removed { + o.push_str(&comp_row(c)); + } + o.push_str("
NameVersionEcosystem
"); + }, + ); // Version Changed - section(&mut out, "Version Changed", cs.version_changed.len(), "badge-purple", |o| { - o.push_str(""); - for (old, new) in &cs.version_changed { - let _ = write!(o, "", - esc(&old.name), esc(&old.version), esc(&new.version), esc(&old.ecosystem.to_string())); - } - o.push_str("
NameOld VersionNew VersionEcosystem
{}{}{}{}
"); - }); + section( + &mut out, + "Version Changed", + cs.version_changed.len(), + "badge-purple", + |o| { + o.push_str(""); + for (old, new) in &cs.version_changed { + let _ = write!( + o, + "", + esc(&old.name), + esc(&old.version), + esc(&new.version), + esc(&old.ecosystem.to_string()) + ); + } + o.push_str("
NameOld VersionNew VersionEcosystem
{}{}{}{}
"); + }, + ); // License Changed - section(&mut out, "License Changed", cs.license_changed.len(), "badge-purple", |o| { - o.push_str(""); - for (old, new) in &cs.license_changed { - let _ = write!(o, "", - esc(&old.name), esc(&old.licenses.join(", ")), esc(&new.licenses.join(", "))); - } - o.push_str("
NameOld LicenseNew License
{}{}{}
"); - }); + section( + &mut out, + "License Changed", + cs.license_changed.len(), + "badge-purple", + |o| { + o.push_str(""); + for (old, new) in &cs.license_changed { + let _ = write!( + o, + "", + esc(&old.name), + esc(&old.licenses.join(", ")), + esc(&new.licenses.join(", ")) + ); + } + o.push_str("
NameOld LicenseNew License
{}{}{}
"); + }, + ); // Vulnerabilities - section(&mut out, "Vulnerabilities", total_vulns, "badge-critical", |o| { - o.push_str(""); - for (pkg, advisories) in &enrichment.vulns { - for adv in advisories { - let _ = write!(o, "", - esc(pkg), esc(&adv.id), severity_badge(&adv.severity), - esc(&adv.aliases.join(", "))); + section( + &mut out, + "Vulnerabilities", + total_vulns, + "badge-critical", + |o| { + o.push_str( + "
ComponentIDSeverityAliases
{}{}{}{}
", + ); + for (pkg, advisories) in &enrichment.vulns { + for adv in advisories { + let _ = write!( + o, + "", + esc(pkg), + esc(&adv.id), + severity_badge(&adv.severity), + esc(&adv.aliases.join(", ")) + ); + } } - } - o.push_str("
ComponentIDSeverityAliases
{}{}{}{}
"); - }); + o.push_str(""); + }, + ); // Typosquats - section(&mut out, "Typosquat Candidates", enrichment.typosquats.len(), "badge-critical", |o| { - o.push_str(""); - for t in &enrichment.typosquats { - let _ = write!(o, "", - esc(&t.component.name), esc(&t.closest), t.score); - } - o.push_str("
ComponentSimilar ToScore
{}{}{:.2}
"); - }); + section( + &mut out, + "Typosquat Candidates", + enrichment.typosquats.len(), + "badge-critical", + |o| { + o.push_str(""); + for t in &enrichment.typosquats { + let _ = write!( + o, + "", + esc(&t.component.name), + esc(&t.closest), + t.score + ); + } + o.push_str("
ComponentSimilar ToScore
{}{}{:.2}
"); + }, + ); // Version Jumps - section(&mut out, "Version Jumps", enrichment.version_jumps.len(), "badge-high", |o| { - o.push_str(""); - for v in &enrichment.version_jumps { - let _ = write!(o, "", - esc(&v.before.name), esc(&v.before.version), esc(&v.after.version), v.before_major, v.after_major); - } - o.push_str("
NameOldNewOld MajorNew Major
{}{}{}{}{}
"); - }); + section( + &mut out, + "Version Jumps", + enrichment.version_jumps.len(), + "badge-high", + |o| { + o.push_str(""); + for v in &enrichment.version_jumps { + let _ = write!( + o, + "", + esc(&v.before.name), + esc(&v.before.version), + esc(&v.after.version), + v.before_major, + v.after_major + ); + } + o.push_str("
NameOldNewOld MajorNew Major
{}{}{}{}{}
"); + }, + ); // Maintainer Age - section(&mut out, "Maintainer Age Warnings", enrichment.maintainer_age.len(), "badge-high", |o| { - o.push_str(""); - for m in &enrichment.maintainer_age { - let _ = write!(o, "", - esc(&m.component.name), esc(&m.top_contributor), m.days_old, esc(&m.first_commit_at)); - } - o.push_str("
ComponentTop ContributorDays OldFirst Commit
{}{}{}{}
"); - }); + section( + &mut out, + "Maintainer Age Warnings", + enrichment.maintainer_age.len(), + "badge-high", + |o| { + o.push_str(""); + for m in &enrichment.maintainer_age { + let _ = write!( + o, + "", + esc(&m.component.name), + esc(&m.top_contributor), + m.days_old, + esc(&m.first_commit_at) + ); + } + o.push_str("
ComponentTop ContributorDays OldFirst Commit
{}{}{}{}
"); + }, + ); // License Violations - section(&mut out, "License Violations", enrichment.license_violations.len(), "badge-red", |o| { - o.push_str(""); - for lv in &enrichment.license_violations { - let _ = write!(o, "", - esc(&lv.component.name), esc(&lv.license), esc(&lv.matched_rule), lv.kind); - } - o.push_str("
ComponentLicenseMatched RuleKind
{}{}{}{:?}
"); - }); + section( + &mut out, + "License Violations", + enrichment.license_violations.len(), + "badge-red", + |o| { + o.push_str(""); + for lv in &enrichment.license_violations { + let _ = write!( + o, + "", + esc(&lv.component.name), + esc(&lv.license), + esc(&lv.matched_rule), + lv.kind + ); + } + o.push_str("
ComponentLicenseMatched RuleKind
{}{}{}{:?}
"); + }, + ); // Recently Published - section(&mut out, "Recently Published", enrichment.recently_published.len(), "badge-high", |o| { - o.push_str(""); - for r in &enrichment.recently_published { - let _ = write!(o, "", - esc(&r.component.name), esc(&r.component.version), esc(&r.published_at), r.days_old); - } - o.push_str("
ComponentVersionPublishedDays Old
{}{}{}{}
"); - }); + section( + &mut out, + "Recently Published", + enrichment.recently_published.len(), + "badge-high", + |o| { + o.push_str(""); + for r in &enrichment.recently_published { + let _ = write!( + o, + "", + esc(&r.component.name), + esc(&r.component.version), + esc(&r.published_at), + r.days_old + ); + } + o.push_str("
ComponentVersionPublishedDays Old
{}{}{}{}
"); + }, + ); // Deprecated - section(&mut out, "Deprecated Packages", enrichment.deprecated.len(), "badge-red", |o| { - o.push_str(""); - for r in &enrichment.deprecated { - let _ = write!(o, "", - esc(&r.component.name), esc(&r.component.version), - esc(r.message.as_deref().unwrap_or(""))); - } - o.push_str("
ComponentVersionMessage
{}{}{}
"); - }); + section( + &mut out, + "Deprecated Packages", + enrichment.deprecated.len(), + "badge-red", + |o| { + o.push_str(""); + for r in &enrichment.deprecated { + let _ = write!( + o, + "", + esc(&r.component.name), + esc(&r.component.version), + esc(r.message.as_deref().unwrap_or("")) + ); + } + o.push_str("
ComponentVersionMessage
{}{}{}
"); + }, + ); // Maintainer Set Changed - section(&mut out, "Maintainer Set Changed", enrichment.maintainer_set_changed.len(), "badge-high", |o| { - o.push_str(""); - for r in &enrichment.maintainer_set_changed { - let _ = write!(o, "", - esc(&r.before.name), esc(&r.before.version), esc(&r.after.version), - esc(&r.added.join(", ")), esc(&r.removed.join(", "))); - } - o.push_str("
PackageOld VersionNew VersionAddedRemoved
{}{}{}{}{}
"); - }); + section( + &mut out, + "Maintainer Set Changed", + enrichment.maintainer_set_changed.len(), + "badge-high", + |o| { + o.push_str(""); + for r in &enrichment.maintainer_set_changed { + let _ = write!( + o, + "", + esc(&r.before.name), + esc(&r.before.version), + esc(&r.after.version), + esc(&r.added.join(", ")), + esc(&r.removed.join(", ")) + ); + } + o.push_str("
PackageOld VersionNew VersionAddedRemoved
{}{}{}{}{}
"); + }, + ); // Plugin Findings - section(&mut out, "Plugin Findings", enrichment.plugin_findings.len(), "badge-purple", |o| { - o.push_str(""); - for p in &enrichment.plugin_findings { - let _ = write!(o, "", - esc(&p.plugin_name), esc(&p.component_purl), severity_badge(&format!("{:?}", p.severity)), esc(&p.message)); - } - o.push_str("
PluginComponent PURLSeverityMessage
{}{}{}{}
"); - }); + section( + &mut out, + "Plugin Findings", + enrichment.plugin_findings.len(), + "badge-purple", + |o| { + o.push_str(""); + for p in &enrichment.plugin_findings { + let _ = write!( + o, + "", + esc(&p.plugin_name), + esc(&p.component_purl), + severity_badge(&format!("{:?}", p.severity)), + esc(&p.message) + ); + } + o.push_str("
PluginComponent PURLSeverityMessage
{}{}{}{}
"); + }, + ); // VEX Suppressed if enrichment.vex_suppressed_count > 0 { - let _ = write!(out, + let _ = write!( + out, r#"

VEX Suppressed

{} suppressed

{} vulnerabilities were suppressed by VEX annotations.

"#, enrichment.vex_suppressed_count, enrichment.vex_suppressed_count ); diff --git a/src/render/mod.rs b/src/render/mod.rs index 36fe1b5..a9e9a39 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -3,8 +3,8 @@ //! output buffer) so the same `ChangeSet` always renders to byte-identical output — //! a hard requirement for `peter-evans/create-or-update-comment` upsert behavior. +pub mod html; pub mod json; pub mod markdown; pub mod sarif; -pub mod html; pub mod term; diff --git a/src/run.rs b/src/run.rs index 0a89e76..c5c6eb3 100644 --- a/src/run.rs +++ b/src/run.rs @@ -401,6 +401,7 @@ fn run_diff(mut args: DiffArgs) -> Result<()> { } OutputFormat::Json => render::json::render(&cs, &enrichment), OutputFormat::Sarif => render::sarif::render(&cs, &enrichment), + OutputFormat::Html => render::html::render(&cs, &enrichment), }; if let Some(path) = &args.output_file {