Skip to main content

sbom_tools/tui/
export.rs

1//! TUI export functionality.
2//!
3//! Provides export capabilities for diff and view modes using the reports module.
4
5use 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/// Export format selection for TUI
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ExportFormat {
15    Json,
16    Markdown,
17    Html,
18    Sarif,
19    Csv,
20}
21
22impl ExportFormat {
23    /// Get file extension for this format
24    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    /// Get human-readable name
35    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    /// Convert to report format (where applicable)
46    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/// Result of an export operation
58#[derive(Debug)]
59pub struct ExportResult {
60    pub path: PathBuf,
61    pub format: ExportFormat,
62    pub success: bool,
63    pub message: String,
64}
65
66/// Export diff results to a file
67pub 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
93/// Export single SBOM to a file (view mode)
94pub 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
187/// Export compliance results to a file (JSON or SARIF)
188pub 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