use crate::config::IncludedDependencies;
use crate::config::Pattern;
use crate::config::Prefix;
use crate::config::SbomConfig;
use crate::format::Format;
use crate::toml::config_from_toml;
use crate::toml::ConfigError;
use crate::traits::ToXml;
use crate::Bom;
use crate::Metadata;
use cargo::core::dependency::DepKind;
use cargo::core::Package;
use cargo::core::PackageSet;
use cargo::core::Resolve;
use cargo::core::Workspace;
use cargo::ops;
use std::{
collections::BTreeSet,
fs::File,
io::LineWriter,
path::{Path, PathBuf},
};
use thiserror::Error;
use xml_writer::XmlWriter;
pub struct SbomGenerator {}
impl SbomGenerator {
pub fn create_sboms(
ws: Workspace,
config_override: &SbomConfig,
) -> Result<Vec<GeneratedSbom>, GeneratorError> {
log::trace!(
"Processing the workspace {} configuration",
ws.root_manifest().to_string_lossy()
);
let workspace_config = config_from_toml(ws.custom_metadata())?;
let members: Vec<Package> = ws.members().cloned().collect();
let (package_ids, resolve) =
ops::resolve_ws(&ws).map_err(|error| GeneratorError::CargoConfigError {
config_filepath: ws.root_manifest().to_string_lossy().to_string(),
error,
})?;
let mut result = Vec::with_capacity(members.len());
for member in members.iter() {
log::trace!(
"Processing the package {} configuration",
member.manifest_path().to_string_lossy()
);
let package_config = config_from_toml(member.manifest().custom_metadata())?;
let config = workspace_config
.merge(&package_config)
.merge(config_override);
log::trace!("Config from workspace metadata: {:?}", workspace_config);
log::trace!("Config from package metadata: {:?}", package_config);
log::trace!("Config from config override: {:?}", config_override);
log::debug!("Config from merged config: {:?}", config);
let dependencies =
if config.included_dependencies() == IncludedDependencies::AllDependencies {
all_dependencies(&members, &package_ids, &resolve)?
} else {
top_level_dependencies(&members, &package_ids)?
};
let mut bom: Bom = dependencies.iter().collect();
bom.metadata = get_metadata(member.manifest_path());
let generated = GeneratedSbom {
bom,
manifest_path: member.manifest_path().to_path_buf(),
package_name: member.name().to_string(),
sbom_config: config,
};
result.push(generated);
}
Ok(result)
}
}
#[derive(Error, Debug)]
pub enum GeneratorError {
#[error("Expected a root package in the cargo config: {config_filepath}")]
RootPackageMissingError { config_filepath: String },
#[error("Could not process the cargo config: {config_filepath}")]
CargoConfigError {
config_filepath: String,
#[source]
error: anyhow::Error,
},
#[error("Error with Cargo metadata")]
CargoMetaDataError(#[from] cargo_metadata::Error),
#[error("Error retrieving package information: {package_id}")]
PackageError {
package_id: cargo::core::package_id::PackageId,
#[source]
error: anyhow::Error,
},
#[error("Error with Cargo custom_metadata")]
CustomMetadataTomlError(#[from] ConfigError),
}
fn get_metadata(toml_file_path: &Path) -> Option<Metadata> {
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(&toml_file_path)
.features(cargo_metadata::CargoOpt::AllFeatures)
.exec();
match metadata {
Ok(metadata) => metadata
.root_package()
.map(Metadata::from)
.or_else(|| Some(Metadata::default())),
Err(e) => {
log::error!(
"Attempted to get metadata from the cargo config: {}",
toml_file_path.to_string_lossy(),
);
log::debug!("Got error: {}", e);
Some(Metadata::default())
}
}
}
fn top_level_dependencies(
members: &[Package],
package_ids: &PackageSet<'_>,
) -> Result<BTreeSet<Package>, GeneratorError> {
log::trace!("Adding top-level dependencies to SBOM");
let mut dependencies = BTreeSet::new();
let all_dependencies = members
.iter()
.flat_map(|m| m.dependencies().iter())
.filter(|d| d.kind() == DepKind::Normal);
for dependency in all_dependencies {
if let Some(package_id) = package_ids
.package_ids()
.find(|id| dependency.matches_id(*id))
{
let package = package_ids
.get_one(package_id)
.map_err(|error| GeneratorError::PackageError { package_id, error })?;
dependencies.insert(package.to_owned());
}
}
for member in members {
dependencies.remove(member);
}
Ok(dependencies)
}
fn all_dependencies(
members: &[Package],
package_ids: &PackageSet<'_>,
resolve: &Resolve,
) -> Result<BTreeSet<Package>, GeneratorError> {
log::trace!("Adding all dependencies to SBOM");
let mut dependencies = BTreeSet::new();
for package_id in resolve.iter() {
let package = package_ids
.get_one(package_id)
.map_err(|error| GeneratorError::PackageError { package_id, error })?;
if members.contains(package) {
continue;
}
dependencies.insert(package.to_owned());
}
Ok(dependencies)
}
pub struct GeneratedSbom {
pub bom: Bom,
pub manifest_path: PathBuf,
pub package_name: String,
pub sbom_config: SbomConfig,
}
impl GeneratedSbom {
pub fn write_to_file(&self) -> Result<(), SbomWriterError> {
let path = self.manifest_path.with_file_name(self.filename());
log::info!("Outputting {}", path.display());
let file = File::create(path).map_err(SbomWriterError::FileCreateError)?;
match self.sbom_config.format() {
Format::Json => {
serde_json::to_writer_pretty(file, &self.bom)?;
}
Format::Xml => {
let file = LineWriter::new(file);
let mut xml = XmlWriter::new(file);
self.bom
.to_xml(&mut xml)
.map_err(SbomWriterError::SerializeXmlError)?;
xml.close().map_err(SbomWriterError::SerializeXmlError)?;
xml.flush().map_err(SbomWriterError::SerializeXmlError)?;
let _actual = xml.into_inner();
}
}
Ok(())
}
fn filename(&self) -> String {
let output_options = self.sbom_config.output_options();
let prefix = match output_options.prefix {
Prefix::Pattern(Pattern::Bom) => "bom".to_string(),
Prefix::Pattern(Pattern::Package) => self.package_name.clone(),
Prefix::Custom(c) => c.to_string(),
};
format!(
"{}{}.{}",
prefix,
output_options.cdx_extension.extension(),
self.sbom_config.format()
)
}
}
#[derive(Error, Debug)]
pub enum SbomWriterError {
#[error("Error creating file")]
FileCreateError(#[source] std::io::Error),
#[error("Error serializing to JSON")]
SerializeJsonError(#[from] serde_json::Error),
#[error("Error serializing to XML")]
SerializeXmlError(#[source] std::io::Error),
}