use crate::model::NormalizedSbom;
use anyhow::Result;
use std::path::Path;
pub struct ParsedSbom {
pub sbom: NormalizedSbom,
pub raw_content: String,
#[cfg(feature = "enrichment")]
pub enrichment_stats: Option<crate::enrichment::EnrichmentStats>,
}
impl ParsedSbom {
#[must_use]
pub const fn new(sbom: NormalizedSbom, raw_content: String) -> Self {
Self {
sbom,
raw_content,
#[cfg(feature = "enrichment")]
enrichment_stats: None,
}
}
#[must_use]
pub const fn sbom(&self) -> &NormalizedSbom {
&self.sbom
}
pub const fn sbom_mut(&mut self) -> &mut NormalizedSbom {
&mut self.sbom
}
#[must_use]
pub fn raw_content(&self) -> &str {
&self.raw_content
}
#[must_use]
pub fn into_sbom(self) -> NormalizedSbom {
self.sbom
}
#[must_use]
pub fn into_parts(self) -> (NormalizedSbom, String) {
(self.sbom, self.raw_content)
}
pub fn drop_raw_content(&mut self) {
self.raw_content = String::new();
}
}
pub fn parse_sbom_with_context(path: &Path, quiet: bool) -> Result<ParsedSbom> {
if !quiet {
tracing::info!("Parsing SBOM: {:?}", path);
}
let path_display = path.display().to_string();
let raw_content =
std::fs::read_to_string(path).map_err(|e| super::PipelineError::ParseFailed {
path: path_display.clone(),
source: e.into(),
})?;
let sbom = crate::parsers::parse_sbom_str(&raw_content).map_err(|e| {
super::PipelineError::ParseFailed {
path: path_display,
source: e.into(),
}
})?;
if !quiet {
tracing::info!("Parsed {} components", sbom.component_count());
}
sbom.log_collision_summary();
Ok(ParsedSbom::new(sbom, raw_content))
}
#[cfg(feature = "enrichment")]
#[must_use]
pub fn build_enrichment_config(
config: &crate::config::EnrichmentConfig,
) -> crate::enrichment::OsvEnricherConfig {
crate::enrichment::OsvEnricherConfig {
cache_dir: config
.cache_dir
.clone()
.unwrap_or_else(super::dirs::osv_cache_dir),
cache_ttl: std::time::Duration::from_secs(config.cache_ttl_hours * 3600),
bypass_cache: config.bypass_cache,
timeout: std::time::Duration::from_secs(config.timeout_secs),
..Default::default()
}
}
#[cfg(feature = "enrichment")]
pub fn enrich_sbom(
sbom: &mut NormalizedSbom,
config: &crate::enrichment::OsvEnricherConfig,
quiet: bool,
) -> Option<crate::enrichment::EnrichmentStats> {
use crate::enrichment::{OsvEnricher, VulnerabilityEnricher};
if !quiet {
eprintln!(
"Enriching SBOM with OSV vulnerability data ({} components)...",
sbom.component_count()
);
}
match OsvEnricher::new(config.clone()) {
Ok(enricher) => {
if !enricher.is_available() {
eprintln!("Warning: OSV API unavailable, skipping vulnerability enrichment");
return None;
}
let components: Vec<_> = sbom.components.values().cloned().collect();
let mut comp_vec: Vec<_> = components;
match enricher.enrich(&mut comp_vec) {
Ok(stats) => {
if !quiet {
eprintln!(
"Enriched: {} components with vulns, {} total vulns found",
stats.components_with_vulns, stats.total_vulns_found
);
}
for comp in comp_vec {
sbom.components.insert(comp.canonical_id.clone(), comp);
}
Some(stats)
}
Err(e) => {
eprintln!("Warning: vulnerability enrichment failed: {e}");
None
}
}
}
Err(e) => {
eprintln!("Warning: failed to initialize OSV enricher: {e}");
None
}
}
}
#[cfg(feature = "enrichment")]
pub fn enrich_eol(
sbom: &mut NormalizedSbom,
config: &crate::enrichment::EolClientConfig,
quiet: bool,
) -> Option<crate::enrichment::EolEnrichmentStats> {
use crate::enrichment::EolEnricher;
if !quiet {
eprintln!("Enriching SBOM with end-of-life data from endoflife.date...");
}
match EolEnricher::new(config.clone()) {
Ok(mut enricher) => {
let components: Vec<_> = sbom.components.values().cloned().collect();
let mut comp_vec = components;
match enricher.enrich_components(&mut comp_vec) {
Ok(stats) => {
if !quiet {
eprintln!(
"EOL enrichment: {} enriched, {} EOL, {} approaching, {} supported, {} skipped",
stats.components_enriched,
stats.eol_count,
stats.approaching_eol_count,
stats.supported_count,
stats.skipped_count,
);
}
for comp in comp_vec {
sbom.components.insert(comp.canonical_id.clone(), comp);
}
Some(stats)
}
Err(e) => {
eprintln!("Warning: EOL enrichment failed: {e}");
None
}
}
}
Err(e) => {
eprintln!("Warning: failed to initialize EOL enricher: {e}");
None
}
}
}
#[cfg(feature = "enrichment")]
pub fn enrich_vex(
sbom: &mut NormalizedSbom,
vex_paths: &[std::path::PathBuf],
quiet: bool,
) -> Option<crate::enrichment::VexEnrichmentStats> {
if vex_paths.is_empty() {
return None;
}
if !quiet {
eprintln!(
"Enriching SBOM with VEX data from {} document(s)...",
vex_paths.len()
);
}
match crate::enrichment::VexEnricher::from_files(vex_paths) {
Ok(mut enricher) => {
let stats = enricher.enrich_sbom(sbom);
if !quiet {
eprintln!(
"VEX enrichment: {} documents, {} statements, {} vulns matched, {} components",
stats.documents_loaded,
stats.statements_parsed,
stats.vulns_matched,
stats.components_with_vex,
);
}
Some(stats)
}
Err(e) => {
eprintln!("Warning: failed to load VEX documents: {e}");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parsed_sbom_creation() {
let sbom = NormalizedSbom::default();
let parsed = ParsedSbom::new(sbom, String::new());
assert_eq!(parsed.sbom().component_count(), 0);
}
#[test]
fn test_parsed_sbom_into_sbom() {
let sbom = NormalizedSbom::default();
let parsed = ParsedSbom::new(sbom, String::new());
let recovered = parsed.into_sbom();
assert_eq!(recovered.component_count(), 0);
}
#[test]
fn test_parsed_sbom_raw_content() {
let sbom = NormalizedSbom::default();
let parsed = ParsedSbom::new(sbom, "raw content".to_string());
assert_eq!(parsed.raw_content(), "raw content");
}
#[test]
fn test_parsed_sbom_into_parts() {
let sbom = NormalizedSbom::default();
let parsed = ParsedSbom::new(sbom, "test".to_string());
let (recovered, raw) = parsed.into_parts();
assert_eq!(recovered.component_count(), 0);
assert_eq!(raw, "test");
}
}