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