Skip to main content

sbom_tools/reports/
mod.rs

1//! Report generation for diff results.
2//!
3//! This module provides multiple output formats for SBOM diff results:
4//! - JSON: Structured data for programmatic integration
5//! - SARIF: CI/CD security dashboard integration
6//! - Markdown: Human-readable documentation
7//! - HTML: Interactive stakeholder reports
8//! - Side-by-side: Terminal diff output like difftastic
9//! - Summary: Compact shell-friendly output
10//! - Table: Aligned tabular terminal output
11//!
12//! # Security
13//!
14//! The `escape` module provides utilities for safe output generation.
15//! All user-controllable data (component names, versions, etc.) should
16//! be escaped before embedding in HTML or Markdown reports.
17
18pub mod analyst;
19pub mod csaf;
20mod csv;
21pub mod escape;
22mod html;
23mod json;
24mod markdown;
25mod sarif;
26mod sidebyside;
27pub mod streaming;
28mod summary;
29mod types;
30
31pub use csaf::{CsafEmitOptions, emit_csaf};
32pub use csv::CsvReporter;
33pub use html::HtmlReporter;
34pub use json::JsonReporter;
35pub use markdown::MarkdownReporter;
36pub use sarif::SarifReporter;
37pub use sarif::{
38    generate_ai_readiness_sarif, generate_compliance_sarif, generate_multi_compliance_sarif,
39};
40pub use sidebyside::SideBySideReporter;
41pub use streaming::{
42    NdjsonReportGenerator, NdjsonReporter, NdjsonWriter, StreamingJsonReporter, StreamingJsonWriter,
43};
44pub use summary::{SummaryReporter, TableReporter};
45pub use types::{MinSeverity, ReportConfig, ReportFormat, ReportMetadata, ReportType};
46
47// Re-export traits
48// Note: StreamingReporter is implemented as a blanket impl for ReportGenerator
49
50use crate::diff::DiffResult;
51use crate::model::NormalizedSbom;
52use std::io::Write;
53use thiserror::Error;
54
55/// Errors that can occur during report generation
56#[derive(Error, Debug)]
57pub enum ReportError {
58    #[error("IO error: {0}")]
59    IoError(#[from] std::io::Error),
60
61    #[error("Serialization error: {0}")]
62    SerializationError(String),
63
64    #[error("Template error: {0}")]
65    TemplateError(String),
66
67    #[error("Invalid configuration: {0}")]
68    ConfigError(String),
69
70    #[error("Format error: {0}")]
71    FormatError(#[from] std::fmt::Error),
72}
73
74/// Trait for report generators
75pub trait ReportGenerator {
76    /// Generate a report from diff results
77    fn generate_diff_report(
78        &self,
79        result: &DiffResult,
80        old_sbom: &NormalizedSbom,
81        new_sbom: &NormalizedSbom,
82        config: &ReportConfig,
83    ) -> Result<String, ReportError>;
84
85    /// Generate a report for a single SBOM (view mode)
86    fn generate_view_report(
87        &self,
88        sbom: &NormalizedSbom,
89        config: &ReportConfig,
90    ) -> Result<String, ReportError>;
91
92    /// Write report to a writer
93    fn write_diff_report(
94        &self,
95        result: &DiffResult,
96        old_sbom: &NormalizedSbom,
97        new_sbom: &NormalizedSbom,
98        config: &ReportConfig,
99        writer: &mut dyn Write,
100    ) -> Result<(), ReportError> {
101        let report = self.generate_diff_report(result, old_sbom, new_sbom, config)?;
102        writer.write_all(report.as_bytes())?;
103        Ok(())
104    }
105
106    /// Get the format this generator produces
107    fn format(&self) -> ReportFormat;
108}
109
110/// Trait for writing reports directly to a [`Write`] sink.
111///
112/// Every `ReportGenerator` automatically implements this trait via a blanket
113/// impl that generates the full report string and writes it. Reporters that
114/// can write **incrementally** (e.g., [`StreamingJsonReporter`],
115/// [`NdjsonReporter`]) override this with truly streaming implementations
116/// that avoid buffering the entire output in memory.
117///
118/// # Example
119///
120/// ```ignore
121/// use sbom_tools::reports::{WriterReporter, JsonReporter, ReportConfig};
122/// use std::io::BufWriter;
123/// use std::fs::File;
124///
125/// let reporter = JsonReporter::new();
126/// let file = File::create("report.json")?;
127/// let mut writer = BufWriter::new(file);
128///
129/// reporter.write_diff_to(&result, &old, &new, &config, &mut writer)?;
130/// ```
131pub trait WriterReporter {
132    /// Write a diff report to a writer.
133    ///
134    /// Implementations may buffer the full report or write incrementally
135    /// depending on the reporter type.
136    fn write_diff_to<W: Write>(
137        &self,
138        result: &DiffResult,
139        old_sbom: &NormalizedSbom,
140        new_sbom: &NormalizedSbom,
141        config: &ReportConfig,
142        writer: &mut W,
143    ) -> Result<(), ReportError>;
144
145    /// Write a view report to a writer.
146    fn write_view_to<W: Write>(
147        &self,
148        sbom: &NormalizedSbom,
149        config: &ReportConfig,
150        writer: &mut W,
151    ) -> Result<(), ReportError>;
152
153    /// Get the format this reporter produces
154    fn format(&self) -> ReportFormat;
155}
156
157/// Backwards-compatible alias for `WriterReporter`.
158#[deprecated(since = "0.2.0", note = "Renamed to WriterReporter for clarity")]
159pub trait StreamingReporter: WriterReporter {}
160
161/// Blanket implementation of `WriterReporter` for any `ReportGenerator`.
162///
163/// Generates the full report in memory, then writes it. This is **not**
164/// streaming — it buffers the entire output. Reporters that need true
165/// incremental output (e.g., for very large SBOMs) should implement
166/// `WriterReporter` directly.
167impl<T: ReportGenerator> WriterReporter for T {
168    fn write_diff_to<W: Write>(
169        &self,
170        result: &DiffResult,
171        old_sbom: &NormalizedSbom,
172        new_sbom: &NormalizedSbom,
173        config: &ReportConfig,
174        writer: &mut W,
175    ) -> Result<(), ReportError> {
176        let report = self.generate_diff_report(result, old_sbom, new_sbom, config)?;
177        writer.write_all(report.as_bytes())?;
178        Ok(())
179    }
180
181    fn write_view_to<W: Write>(
182        &self,
183        sbom: &NormalizedSbom,
184        config: &ReportConfig,
185        writer: &mut W,
186    ) -> Result<(), ReportError> {
187        let report = self.generate_view_report(sbom, config)?;
188        writer.write_all(report.as_bytes())?;
189        Ok(())
190    }
191
192    fn format(&self) -> ReportFormat {
193        ReportGenerator::format(self)
194    }
195}
196
197#[allow(deprecated)]
198impl<T: WriterReporter> StreamingReporter for T {}
199
200/// Create a report generator for the given format
201#[must_use]
202pub fn create_reporter(format: ReportFormat) -> Box<dyn ReportGenerator> {
203    create_reporter_with_options(format, true)
204}
205
206/// Create a report generator with color control
207#[must_use]
208pub fn create_reporter_with_options(
209    format: ReportFormat,
210    use_color: bool,
211) -> Box<dyn ReportGenerator> {
212    match format {
213        ReportFormat::Auto | ReportFormat::Summary => {
214            if use_color {
215                Box::new(SummaryReporter::new())
216            } else {
217                Box::new(SummaryReporter::new().no_color())
218            }
219        }
220        ReportFormat::Json | ReportFormat::Tui => Box::new(JsonReporter::new()), // TUI uses JSON internally
221        ReportFormat::Sarif => Box::new(SarifReporter::new()),
222        ReportFormat::Markdown => Box::new(MarkdownReporter::new()),
223        ReportFormat::Html => Box::new(HtmlReporter::new()),
224        ReportFormat::SideBySide => Box::new(SideBySideReporter::new()),
225        ReportFormat::Table => {
226            if use_color {
227                Box::new(TableReporter::new())
228            } else {
229                Box::new(TableReporter::new().no_color())
230            }
231        }
232        ReportFormat::Csv => Box::new(CsvReporter::new()),
233        ReportFormat::Ndjson => Box::new(NdjsonReportGenerator::new()),
234    }
235}