1use super::{ReportConfig, ReportError, ReportFormat, ReportGenerator};
7use crate::diff::{DiffResult, SlaStatus, VulnerabilityDetail};
8use crate::model::NormalizedSbom;
9use std::fmt::Write;
10
11pub struct CsvReporter;
13
14impl CsvReporter {
15 pub fn new() -> Self {
16 Self
17 }
18}
19
20impl Default for CsvReporter {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl ReportGenerator for CsvReporter {
27 fn generate_diff_report(
28 &self,
29 result: &DiffResult,
30 _old_sbom: &NormalizedSbom,
31 _new_sbom: &NormalizedSbom,
32 _config: &ReportConfig,
33 ) -> Result<String, ReportError> {
34 let estimated_lines = result.components.total()
36 + result.vulnerabilities.introduced.len()
37 + result.vulnerabilities.resolved.len()
38 + result.vulnerabilities.persistent.len()
39 + 10; let mut content = String::with_capacity(estimated_lines * 100);
41
42 content.push_str("# Components\n");
44 content.push_str("Change,Name,Old Version,New Version,Ecosystem\n");
45
46 for comp in &result.components.added {
47 write_component_line(&mut content, "Added", comp);
48 }
49
50 for comp in &result.components.removed {
51 write_component_line(&mut content, "Removed", comp);
52 }
53
54 for comp in &result.components.modified {
55 write_component_line(&mut content, "Modified", comp);
56 }
57
58 content.push_str("\n# Vulnerabilities\n");
60 content.push_str("Status,ID,Severity,Type,SLA,Component,Description\n");
61
62 for vuln in &result.vulnerabilities.introduced {
63 write_vuln_line(&mut content, "Introduced", vuln);
64 }
65
66 for vuln in &result.vulnerabilities.resolved {
67 write_vuln_line(&mut content, "Resolved", vuln);
68 }
69
70 for vuln in &result.vulnerabilities.persistent {
71 write_vuln_line(&mut content, "Persistent", vuln);
72 }
73
74 Ok(content)
75 }
76
77 fn generate_view_report(
78 &self,
79 sbom: &NormalizedSbom,
80 _config: &ReportConfig,
81 ) -> Result<String, ReportError> {
82 let mut content = String::with_capacity(sbom.components.len() * 150 + 100);
84
85 content.push_str("Name,Version,Ecosystem,Type,PURL,Licenses,Vulnerabilities\n");
86
87 for (_, comp) in &sbom.components {
88 let licenses = comp
89 .licenses
90 .declared
91 .iter()
92 .map(|l| l.expression.as_str())
93 .collect::<Vec<_>>()
94 .join("; ");
95 let vuln_count = comp.vulnerabilities.len();
96 let ecosystem = comp
97 .ecosystem
98 .as_ref()
99 .map(|e| format!("{:?}", e))
100 .unwrap_or_else(|| "-".to_string());
101
102 let _ = writeln!(
103 content,
104 "\"{}\",\"{}\",\"{}\",\"{:?}\",\"{}\",\"{}\",{}",
105 escape_csv(&comp.name),
106 comp.version.as_deref().unwrap_or("-"),
107 ecosystem,
108 comp.component_type,
109 comp.identifiers.purl.as_deref().unwrap_or("-"),
110 escape_csv(&licenses),
111 vuln_count
112 );
113 }
114
115 Ok(content)
116 }
117
118 fn format(&self) -> ReportFormat {
119 ReportFormat::Csv
120 }
121}
122
123fn write_component_line(
125 content: &mut String,
126 change_type: &str,
127 comp: &crate::diff::ComponentChange,
128) {
129 let _ = writeln!(
130 content,
131 "{},\"{}\",\"{}\",\"{}\",\"{}\"",
132 change_type,
133 escape_csv(&comp.name),
134 comp.old_version.as_deref().unwrap_or("-"),
135 comp.new_version.as_deref().unwrap_or("-"),
136 comp.ecosystem.as_deref().unwrap_or("-")
137 );
138}
139
140fn write_vuln_line(content: &mut String, status: &str, vuln: &VulnerabilityDetail) {
141 let depth_label = match vuln.component_depth {
142 Some(1) => "Direct",
143 Some(_) => "Transitive",
144 None => "-",
145 };
146 let sla_display = format_sla_csv(vuln);
147 let desc = vuln
148 .description
149 .as_deref()
150 .map(escape_csv)
151 .unwrap_or_default();
152
153 let _ = writeln!(
154 content,
155 "{},\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"",
156 status,
157 escape_csv(&vuln.id),
158 escape_csv(&vuln.severity),
159 depth_label,
160 sla_display,
161 escape_csv(&vuln.component_name),
162 desc
163 );
164}
165
166fn escape_csv(s: &str) -> String {
169 s.replace('"', "\"\"").replace('\n', " ")
170}
171
172fn format_sla_csv(vuln: &VulnerabilityDetail) -> String {
173 match vuln.sla_status() {
174 SlaStatus::Overdue(days) => format!("{}d late", days),
175 SlaStatus::DueSoon(days) => format!("{}d left", days),
176 SlaStatus::OnTrack(days) => format!("{}d left", days),
177 SlaStatus::NoDueDate => vuln
178 .days_since_published
179 .map(|d| format!("{}d old", d))
180 .unwrap_or_else(|| "-".to_string()),
181 }
182}