1use crate::diff::DiffResult;
6use crate::model::NormalizedSbom;
7use crate::reports::{create_reporter, ReportConfig, ReportFormat};
8use std::fs::File;
9use std::io::Write;
10use std::path::PathBuf;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ExportFormat {
15 Json,
16 Markdown,
17 Html,
18 Sarif,
19 Csv,
20}
21
22impl ExportFormat {
23 pub fn extension(&self) -> &'static str {
25 match self {
26 ExportFormat::Json => "json",
27 ExportFormat::Markdown => "md",
28 ExportFormat::Html => "html",
29 ExportFormat::Sarif => "sarif.json",
30 ExportFormat::Csv => "csv",
31 }
32 }
33
34 pub fn name(&self) -> &'static str {
36 match self {
37 ExportFormat::Json => "JSON",
38 ExportFormat::Markdown => "Markdown",
39 ExportFormat::Html => "HTML",
40 ExportFormat::Sarif => "SARIF",
41 ExportFormat::Csv => "CSV",
42 }
43 }
44
45 fn to_report_format(self) -> Option<ReportFormat> {
47 match self {
48 ExportFormat::Json => Some(ReportFormat::Json),
49 ExportFormat::Markdown => Some(ReportFormat::Markdown),
50 ExportFormat::Html => Some(ReportFormat::Html),
51 ExportFormat::Sarif => Some(ReportFormat::Sarif),
52 ExportFormat::Csv => Some(ReportFormat::Csv),
53 }
54 }
55}
56
57#[derive(Debug)]
59pub struct ExportResult {
60 pub path: PathBuf,
61 pub format: ExportFormat,
62 pub success: bool,
63 pub message: String,
64}
65
66pub fn export_diff(
68 format: ExportFormat,
69 result: &DiffResult,
70 old_sbom: &NormalizedSbom,
71 new_sbom: &NormalizedSbom,
72 output_dir: Option<&str>,
73) -> ExportResult {
74 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
75 let filename = format!("sbom_tools_{}.{}", timestamp, format.extension());
76 let path = match output_dir {
77 Some(dir) => PathBuf::from(dir).join(&filename),
78 None => PathBuf::from(&filename),
79 };
80
81 if let Some(report_format) = format.to_report_format() {
82 export_with_reporter(format, report_format, result, old_sbom, new_sbom, &path)
83 } else {
84 ExportResult {
85 path,
86 format,
87 success: false,
88 message: "Unsupported format".to_string(),
89 }
90 }
91}
92
93pub fn export_view(
95 format: ExportFormat,
96 sbom: &NormalizedSbom,
97 output_dir: Option<&str>,
98) -> ExportResult {
99 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
100 let filename = format!("sbom_report_{}.{}", timestamp, format.extension());
101 let path = match output_dir {
102 Some(dir) => PathBuf::from(dir).join(&filename),
103 None => PathBuf::from(&filename),
104 };
105
106 if let Some(report_format) = format.to_report_format() {
107 export_view_with_reporter(format, report_format, sbom, &path)
108 } else {
109 ExportResult {
110 path,
111 format,
112 success: false,
113 message: "Unsupported format".to_string(),
114 }
115 }
116}
117
118fn export_with_reporter(
119 format: ExportFormat,
120 report_format: ReportFormat,
121 result: &DiffResult,
122 old_sbom: &NormalizedSbom,
123 new_sbom: &NormalizedSbom,
124 path: &PathBuf,
125) -> ExportResult {
126 let reporter = create_reporter(report_format);
127 let config = ReportConfig::default();
128
129 match reporter.generate_diff_report(result, old_sbom, new_sbom, &config) {
130 Ok(content) => match write_to_file(path, &content) {
131 Ok(()) => ExportResult {
132 path: path.clone(),
133 format,
134 success: true,
135 message: format!("Exported to {}", path.display()),
136 },
137 Err(e) => ExportResult {
138 path: path.clone(),
139 format,
140 success: false,
141 message: format!("Failed to write file: {}", e),
142 },
143 },
144 Err(e) => ExportResult {
145 path: path.clone(),
146 format,
147 success: false,
148 message: format!("Failed to generate report: {}", e),
149 },
150 }
151}
152
153fn export_view_with_reporter(
154 format: ExportFormat,
155 report_format: ReportFormat,
156 sbom: &NormalizedSbom,
157 path: &PathBuf,
158) -> ExportResult {
159 let reporter = create_reporter(report_format);
160 let config = ReportConfig::default();
161
162 match reporter.generate_view_report(sbom, &config) {
163 Ok(content) => match write_to_file(path, &content) {
164 Ok(()) => ExportResult {
165 path: path.clone(),
166 format,
167 success: true,
168 message: format!("Exported to {}", path.display()),
169 },
170 Err(e) => ExportResult {
171 path: path.clone(),
172 format,
173 success: false,
174 message: format!("Failed to write file: {}", e),
175 },
176 },
177 Err(e) => ExportResult {
178 path: path.clone(),
179 format,
180 success: false,
181 message: format!("Failed to generate report: {}", e),
182 },
183 }
184}
185
186
187pub fn export_compliance(
189 format: ExportFormat,
190 results: &[crate::quality::ComplianceResult],
191 selected_standard: usize,
192 output_dir: Option<&str>,
193) -> ExportResult {
194 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
195 let result = results.get(selected_standard);
196
197 let (ext, content) = match format {
198 ExportFormat::Json => {
199 let json = compliance_to_json(results, selected_standard);
200 ("json", json)
201 }
202 ExportFormat::Sarif => {
203 let sarif = compliance_to_sarif(result);
204 ("sarif.json", sarif)
205 }
206 _ => {
207 return ExportResult {
208 path: PathBuf::new(),
209 format,
210 success: false,
211 message: "Compliance export supports JSON and SARIF only".to_string(),
212 };
213 }
214 };
215
216 let level_name = result
217 .map(|r| r.level.name().to_lowercase().replace(' ', "_"))
218 .unwrap_or_else(|| "all".to_string());
219 let filename = format!("compliance_{}_{}.{}", level_name, timestamp, ext);
220 let path = match output_dir {
221 Some(dir) => PathBuf::from(dir).join(&filename),
222 None => PathBuf::from(&filename),
223 };
224
225 match write_to_file(&path, &content) {
226 Ok(()) => ExportResult {
227 path: path.clone(),
228 format,
229 success: true,
230 message: format!("Compliance exported to {}", path.display()),
231 },
232 Err(e) => ExportResult {
233 path,
234 format,
235 success: false,
236 message: format!("Failed to write: {}", e),
237 },
238 }
239}
240
241fn compliance_to_json(
242 results: &[crate::quality::ComplianceResult],
243 selected: usize,
244) -> String {
245 use serde_json::{json, Value};
246
247 let to_value = |r: &crate::quality::ComplianceResult| -> Value {
248 let violations: Vec<Value> = r
249 .violations
250 .iter()
251 .map(|v| {
252 json!({
253 "severity": format!("{:?}", v.severity),
254 "category": v.category.name(),
255 "message": v.message,
256 "element": v.element,
257 "requirement": v.requirement,
258 "remediation": v.remediation_guidance(),
259 })
260 })
261 .collect();
262
263 json!({
264 "standard": r.level.name(),
265 "is_compliant": r.is_compliant,
266 "error_count": r.error_count,
267 "warning_count": r.warning_count,
268 "info_count": r.info_count,
269 "violations": violations,
270 })
271 };
272
273 let output = if let Some(result) = results.get(selected) {
274 to_value(result)
275 } else {
276 let all: Vec<Value> = results.iter().map(to_value).collect();
277 json!({ "standards": all })
278 };
279
280 serde_json::to_string_pretty(&output).unwrap_or_default()
281}
282
283fn compliance_to_sarif(
284 result: Option<&crate::quality::ComplianceResult>,
285) -> String {
286 use serde_json::{json, Value};
287
288 let result = match result {
289 Some(r) => r,
290 None => return json!({"error": "no compliance result"}).to_string(),
291 };
292
293 let results: Vec<Value> = result
294 .violations
295 .iter()
296 .map(|v| {
297 let level = match v.severity {
298 crate::quality::ViolationSeverity::Error => "error",
299 crate::quality::ViolationSeverity::Warning => "warning",
300 crate::quality::ViolationSeverity::Info => "note",
301 };
302 json!({
303 "ruleId": format!("COMPLIANCE-{}", v.category.name().to_uppercase().replace(' ', "-")),
304 "level": level,
305 "message": { "text": v.message },
306 "properties": {
307 "requirement": v.requirement,
308 "category": v.category.name(),
309 "remediation": v.remediation_guidance(),
310 "element": v.element,
311 }
312 })
313 })
314 .collect();
315
316 let sarif = json!({
317 "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
318 "version": "2.1.0",
319 "runs": [{
320 "tool": {
321 "driver": {
322 "name": "sbom-tools",
323 "version": env!("CARGO_PKG_VERSION"),
324 "informationUri": "https://github.com/anthropics/sbom-tools",
325 "rules": [],
326 }
327 },
328 "results": results,
329 }]
330 });
331
332 serde_json::to_string_pretty(&sarif).unwrap_or_default()
333}
334
335fn write_to_file(path: &PathBuf, content: &str) -> std::io::Result<()> {
336 let mut file = File::create(path)?;
337 file.write_all(content.as_bytes())?;
338 Ok(())
339}
340