use crate::model::{
CanonicalId, CompletenessDeclaration, Component, ComponentType, Creator, CreatorType,
CvssScore, CvssVersion, DependencyEdge, DependencyScope, DependencyType, DocumentMetadata,
ExternalRefType, ExternalReference, Hash, HashAlgorithm, LicenseExpression, NormalizedSbom,
Organization, Property, Remediation, RemediationType, SbomFormat, Severity, SignatureInfo,
VexJustification, VexResponse, VexState, VexStatus, VulnerabilityRef, VulnerabilitySource,
};
use crate::parsers::traits::{ParseError, SbomParser};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use std::collections::HashMap;
#[allow(dead_code)]
pub struct CycloneDxParser {
strict: bool,
}
impl CycloneDxParser {
#[must_use]
pub const fn new() -> Self {
Self { strict: false }
}
#[must_use]
pub const fn strict() -> Self {
Self { strict: true }
}
fn parse_json(&self, content: &str) -> Result<NormalizedSbom, ParseError> {
let cdx: CycloneDxBom =
serde_json::from_str(content).map_err(|e| ParseError::JsonError(e.to_string()))?;
Ok(self.convert_to_normalized(cdx))
}
pub fn parse_json_reader<R: std::io::Read>(
&self,
reader: R,
) -> Result<NormalizedSbom, ParseError> {
let cdx: CycloneDxBom =
serde_json::from_reader(reader).map_err(|e| ParseError::JsonError(e.to_string()))?;
Ok(self.convert_to_normalized(cdx))
}
fn parse_xml(&self, content: &str) -> Result<NormalizedSbom, ParseError> {
let cdx: CycloneDxBomXml =
quick_xml::de::from_str(content).map_err(|e| ParseError::XmlError(e.to_string()))?;
let bom = CycloneDxBom {
bom_format: Some("CycloneDX".to_string()),
spec_version: cdx.version.unwrap_or_else(|| "1.4".to_string()),
serial_number: cdx.serial_number,
version: cdx.bom_version,
metadata: cdx.metadata.map(|m| CdxMetadata {
timestamp: m.timestamp,
tools: m.tools.map(|t| t.tool),
authors: None,
component: m.component,
lifecycles: None,
distribution_constraints: None,
}),
components: cdx.components.map(|c| c.component),
dependencies: cdx.dependencies.map(|d| d.dependency),
vulnerabilities: cdx.vulnerabilities.map(|v| v.vulnerability),
compositions: None,
signature: None,
citations: None,
};
Ok(self.convert_to_normalized(bom))
}
fn convert_to_normalized(&self, cdx: CycloneDxBom) -> NormalizedSbom {
let document = self.convert_metadata(&cdx);
let mut sbom = NormalizedSbom::new(document);
let mut id_map: HashMap<String, CanonicalId> = HashMap::new();
if let Some(meta) = &cdx.metadata
&& let Some(meta_comp) = &meta.component
{
let comp = self.convert_component(meta_comp);
let bom_ref = meta_comp
.bom_ref
.clone()
.unwrap_or_else(|| comp.name.clone());
let canonical_id = comp.canonical_id.clone();
id_map.insert(bom_ref, canonical_id.clone());
sbom.set_primary_component(canonical_id);
for ext_ref in &comp.external_refs {
match ext_ref.ref_type {
ExternalRefType::SecurityContact => {
sbom.document.security_contact = Some(ext_ref.url.clone());
}
ExternalRefType::Advisories | ExternalRefType::Support => {
if sbom.document.vulnerability_disclosure_url.is_none() {
sbom.document.vulnerability_disclosure_url = Some(ext_ref.url.clone());
}
}
_ => {}
}
}
if let Some(props) = &meta_comp.properties {
for prop in props {
let name_lower = prop.name.to_lowercase();
if name_lower.contains("endofsupport")
|| name_lower.contains("end-of-support")
|| name_lower.contains("eol")
|| name_lower.contains("supportend")
|| name_lower.contains("support_end")
{
if let Ok(dt) = DateTime::parse_from_rfc3339(&prop.value) {
sbom.document.support_end_date = Some(dt.with_timezone(&Utc));
} else if let Ok(dt) =
chrono::NaiveDate::parse_from_str(&prop.value, "%Y-%m-%d")
{
sbom.document.support_end_date = Some(
dt.and_hms_opt(0, 0, 0)
.expect("midnight is always valid")
.and_utc(),
);
}
}
}
}
sbom.add_component(comp);
}
let mut scope_map: HashMap<String, DependencyScope> = HashMap::new();
if let Some(components) = cdx.components {
for cdx_comp in components {
let comp = self.convert_component(&cdx_comp);
let bom_ref = cdx_comp.bom_ref.unwrap_or_else(|| comp.name.clone());
if let Some(scope_str) = &cdx_comp.scope {
let scope = match scope_str.to_lowercase().as_str() {
"optional" => DependencyScope::Optional,
"excluded" => DependencyScope::Excluded,
_ => DependencyScope::Required,
};
scope_map.insert(bom_ref.clone(), scope);
}
id_map.insert(bom_ref, comp.canonical_id.clone());
sbom.add_component(comp);
}
}
if let Some(deps) = cdx.dependencies {
for dep in deps {
if let Some(from_id) = id_map.get(&dep.ref_field) {
for depends_on in dep.depends_on.unwrap_or_default() {
if let Some(to_id) = id_map.get(&depends_on) {
let dep_type = scope_map.get(&depends_on).map_or(
DependencyType::DependsOn,
|scope| match scope {
DependencyScope::Optional => DependencyType::OptionalDependsOn,
_ => DependencyType::DependsOn,
},
);
let mut edge =
DependencyEdge::new(from_id.clone(), to_id.clone(), dep_type);
if let Some(scope) = scope_map.get(&depends_on) {
edge = edge.with_scope(scope.clone());
}
sbom.add_edge(edge);
}
}
}
}
}
if let Some(vulns) = cdx.vulnerabilities {
for vuln in vulns {
self.apply_vulnerability(&mut sbom, &vuln, &id_map);
}
}
if let Some(citations) = &cdx.citations
&& !citations.is_empty()
&& let Ok(citations_json) = serde_json::to_value(
citations
.iter()
.map(|c| {
serde_json::json!({
"timestamp": c.timestamp,
"attributedTo": c.attributed_to,
"process": c.process,
"note": c.note,
"pointers": c.pointers,
"expressions": c.expressions,
})
})
.collect::<Vec<_>>(),
)
{
sbom.extensions.cyclonedx = Some(serde_json::json!({ "citations": citations_json }));
}
sbom.calculate_content_hash();
sbom
}
fn convert_metadata(&self, cdx: &CycloneDxBom) -> DocumentMetadata {
let created = cdx
.metadata
.as_ref()
.and_then(|m| m.timestamp.as_ref())
.and_then(|t| DateTime::parse_from_rfc3339(t).ok())
.map_or_else(Utc::now, |dt| dt.with_timezone(&Utc));
let mut creators = Vec::new();
if let Some(meta) = &cdx.metadata
&& let Some(tools) = &meta.tools
{
for tool in tools {
creators.push(Creator {
creator_type: CreatorType::Tool,
name: format!(
"{} {}",
tool.name.as_deref().unwrap_or("unknown"),
tool.version.as_deref().unwrap_or("")
)
.trim()
.to_string(),
email: None,
});
}
}
let lifecycle_phase = cdx
.metadata
.as_ref()
.and_then(|m| m.lifecycles.as_ref())
.and_then(|lcs| lcs.first())
.and_then(|lc| lc.phase.clone().or_else(|| lc.name.clone()));
let completeness_declaration = cdx
.compositions
.as_ref()
.and_then(|comps| comps.first())
.and_then(|comp| comp.aggregate.as_deref())
.map_or(CompletenessDeclaration::Unknown, |agg| match agg {
"complete" => CompletenessDeclaration::Complete,
"incomplete" => CompletenessDeclaration::Incomplete,
"incomplete_first_party_only" => CompletenessDeclaration::IncompleteFirstPartyOnly,
"incomplete_third_party_only" => CompletenessDeclaration::IncompleteThirdPartyOnly,
"unknown" => CompletenessDeclaration::Unknown,
"not_specified" => CompletenessDeclaration::NotSpecified,
_ => CompletenessDeclaration::Unknown,
});
let signature = cdx.signature.as_ref().map(|sig| SignatureInfo {
algorithm: sig
.algorithm
.clone()
.unwrap_or_else(|| "unknown".to_string()),
has_value: sig.value.as_ref().is_some_and(|v| !v.is_empty()),
});
let distribution_classification = cdx
.metadata
.as_ref()
.and_then(|m| m.distribution_constraints.as_ref())
.and_then(|dc| dc.tlp.clone());
let citations_count = cdx.citations.as_ref().map_or(0, Vec::len);
DocumentMetadata {
format: SbomFormat::CycloneDx,
format_version: cdx.spec_version.clone(),
spec_version: cdx.spec_version.clone(),
serial_number: cdx.serial_number.clone(),
created,
creators,
name: cdx
.metadata
.as_ref()
.and_then(|m| m.component.as_ref())
.map(|c| c.name.clone()),
security_contact: None,
vulnerability_disclosure_url: None,
support_end_date: None,
lifecycle_phase,
completeness_declaration,
signature,
distribution_classification,
citations_count,
}
}
fn convert_component(&self, cdx: &CdxComponent) -> Component {
let format_id = cdx.bom_ref.clone().unwrap_or_else(|| cdx.name.clone());
let mut comp = Component::new(cdx.name.clone(), format_id);
if let Some(version) = &cdx.version {
comp = comp.with_version(version.clone());
}
if let Some(purl) = &cdx.purl {
comp = comp.with_purl(purl.clone());
}
comp.component_type = match cdx.component_type.as_str() {
"application" => ComponentType::Application,
"framework" => ComponentType::Framework,
"library" => ComponentType::Library,
"container" => ComponentType::Container,
"operating-system" => ComponentType::OperatingSystem,
"device" => ComponentType::Device,
"firmware" => ComponentType::Firmware,
"file" => ComponentType::File,
"machine-learning-model" => ComponentType::MachineLearningModel,
"data" => ComponentType::Data,
"platform" => ComponentType::Platform,
"device-driver" => ComponentType::DeviceDriver,
"cryptographic" | "cryptographic-asset" => ComponentType::Cryptographic,
other => ComponentType::Other(other.to_string()),
};
if let Some(cpe) = &cdx.cpe {
comp.identifiers.cpe.push(cpe.clone());
}
if let Some(licenses) = &cdx.licenses {
for lic in licenses {
if let Some(license) = &lic.license {
let expr = license
.id
.clone()
.or_else(|| license.name.clone())
.unwrap_or_else(|| "NOASSERTION".to_string());
comp.licenses.add_declared(LicenseExpression::new(expr));
}
if let Some(expr) = &lic.expression {
comp.licenses
.add_declared(LicenseExpression::new(expr.clone()));
}
}
}
if let Some(supplier) = &cdx.supplier {
comp.supplier = Some(Organization::new(supplier.name.clone()));
}
if let Some(hashes) = &cdx.hashes {
for h in hashes {
let algorithm = match h.alg.to_uppercase().as_str() {
"MD5" => HashAlgorithm::Md5,
"SHA-1" => HashAlgorithm::Sha1,
"SHA-256" => HashAlgorithm::Sha256,
"SHA-384" => HashAlgorithm::Sha384,
"SHA-512" => HashAlgorithm::Sha512,
"SHA3-256" => HashAlgorithm::Sha3_256,
"SHA3-384" => HashAlgorithm::Sha3_384,
"SHA3-512" => HashAlgorithm::Sha3_512,
"BLAKE2B-256" => HashAlgorithm::Blake2b256,
"BLAKE2B-384" => HashAlgorithm::Blake2b384,
"BLAKE2B-512" => HashAlgorithm::Blake2b512,
"BLAKE3" => HashAlgorithm::Blake3,
"STREEBOG-256" => HashAlgorithm::Streebog256,
"STREEBOG-512" => HashAlgorithm::Streebog512,
other => HashAlgorithm::Other(other.to_string()),
};
comp.hashes.push(Hash::new(algorithm, h.content.clone()));
}
}
if let Some(ext_refs) = &cdx.external_references {
for ext_ref in ext_refs {
let ref_type = match ext_ref.ref_type.as_str() {
"vcs" => ExternalRefType::Vcs,
"issue-tracker" => ExternalRefType::IssueTracker,
"website" => ExternalRefType::Website,
"advisories" => ExternalRefType::Advisories,
"bom" => ExternalRefType::Bom,
"documentation" => ExternalRefType::Documentation,
"support" => ExternalRefType::Support,
"security-contact" => ExternalRefType::SecurityContact,
"license" => ExternalRefType::License,
"build-meta" => ExternalRefType::BuildMeta,
"release-notes" => ExternalRefType::ReleaseNotes,
"citation" => ExternalRefType::Citation,
"patent" => ExternalRefType::Patent,
"patent-assertion" => ExternalRefType::PatentAssertion,
"patent-family" => ExternalRefType::PatentFamily,
other => ExternalRefType::Other(other.to_string()),
};
comp.external_refs.push(ExternalReference {
ref_type,
url: ext_ref.url.clone(),
comment: ext_ref.comment.clone(),
hashes: Vec::new(),
});
}
}
if let Some(props) = &cdx.properties {
for prop in props {
comp.extensions.properties.push(Property {
name: prop.name.clone(),
value: prop.value.clone(),
});
}
}
comp.description.clone_from(&cdx.description);
comp.group.clone_from(&cdx.group);
comp.author.clone_from(&cdx.author);
comp.copyright.clone_from(&cdx.copyright);
comp.is_external = cdx.is_external;
comp.version_range.clone_from(&cdx.version_range);
comp.calculate_content_hash();
comp
}
fn apply_vulnerability(
&self,
sbom: &mut NormalizedSbom,
vuln: &CdxVulnerability,
id_map: &HashMap<String, CanonicalId>,
) {
let source = vuln.source.as_ref().map_or(VulnerabilitySource::Cve, |s| {
match s.name.to_lowercase().as_str() {
"nvd" => VulnerabilitySource::Nvd,
"ghsa" | "github" => VulnerabilitySource::Ghsa,
"osv" => VulnerabilitySource::Osv,
"snyk" => VulnerabilitySource::Snyk,
other => VulnerabilitySource::Other(other.to_string()),
}
});
let mut vuln_ref = VulnerabilityRef::new(vuln.id.clone(), source);
vuln_ref.description.clone_from(&vuln.description);
if let Some(ratings) = &vuln.ratings {
for rating in ratings {
let version = match rating.method.as_deref() {
Some("CVSSv2") => CvssVersion::V2,
Some("CVSSv3") => CvssVersion::V3,
Some("CVSSv4") => CvssVersion::V4,
_ => CvssVersion::V31,
};
if let Some(score) = rating.score {
let mut cvss = CvssScore::new(version, score);
cvss.vector.clone_from(&rating.vector);
vuln_ref.cvss.push(cvss);
}
if vuln_ref.severity.is_none() {
vuln_ref.severity =
rating
.severity
.as_ref()
.map(|s| match s.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
"low" => Severity::Low,
"info" | "informational" => Severity::Info,
"none" => Severity::None,
_ => Severity::Unknown,
});
}
}
}
if vuln_ref.severity.is_none()
&& let Some(max_score) = vuln_ref.max_cvss_score()
{
vuln_ref.severity = Some(Severity::from_cvss(max_score));
}
if let Some(cwes) = &vuln.cwes {
vuln_ref.cwes = cwes.iter().map(|c| format!("CWE-{c}")).collect();
}
if let Some(recommendation) = &vuln.recommendation {
vuln_ref.remediation = Some(Remediation {
remediation_type: RemediationType::Upgrade,
description: Some(recommendation.clone()),
fixed_version: None,
});
}
let vex_status = vuln.analysis.as_ref().map(|analysis| {
let status = match analysis.state.as_deref() {
Some("not_affected") => VexState::NotAffected,
Some("affected") => VexState::Affected,
Some("fixed") => VexState::Fixed,
_ => VexState::UnderInvestigation,
};
let justification = analysis.justification.as_ref().map(|j| match j.as_str() {
"code_not_present" => VexJustification::VulnerableCodeNotPresent,
"code_not_reachable" => VexJustification::VulnerableCodeNotInExecutePath,
"requires_configuration" | "requires_dependency" | "requires_environment" => {
VexJustification::VulnerableCodeCannotBeControlledByAdversary
}
"protected_by_mitigating_control" => {
VexJustification::InlineMitigationsAlreadyExist
}
_ => VexJustification::ComponentNotPresent,
});
let responses: Vec<VexResponse> = analysis
.response
.as_ref()
.map(|rs| {
rs.iter()
.map(|r| match r.as_str() {
"can_not_fix" => VexResponse::CanNotFix,
"will_not_fix" => VexResponse::WillNotFix,
"rollback" => VexResponse::Rollback,
"workaround_available" => VexResponse::Workaround,
_ => VexResponse::Update,
})
.collect()
})
.unwrap_or_default();
VexStatus {
status,
justification,
action_statement: None,
impact_statement: analysis.detail.clone(),
responses,
detail: analysis.detail.clone(),
}
});
if let Some(affects) = &vuln.affects {
for affect in affects {
if let Some(canonical_id) = id_map.get(&affect.ref_field)
&& let Some(comp) = sbom.components.get_mut(canonical_id)
{
let mut v = vuln_ref.clone();
if let Some(versions) = &affect.versions {
v.affected_versions = versions
.iter()
.filter_map(|ver| ver.version.clone())
.collect();
}
if let Some(vex) = &vex_status {
v.vex_status = Some(vex.clone());
}
comp.vulnerabilities.push(v);
if let Some(vex) = &vex_status {
comp.vex_status = Some(vex.clone());
}
}
}
}
}
}
impl Default for CycloneDxParser {
fn default() -> Self {
Self::new()
}
}
impl SbomParser for CycloneDxParser {
fn parse_str(&self, content: &str) -> Result<NormalizedSbom, ParseError> {
let trimmed = content.trim();
if trimmed.starts_with('{') {
self.parse_json(content)
} else if trimmed.starts_with('<') {
self.parse_xml(content)
} else {
Err(ParseError::UnknownFormat(
"Expected JSON or XML CycloneDX format".to_string(),
))
}
}
fn supported_versions(&self) -> Vec<&str> {
vec!["1.4", "1.5", "1.6", "1.7"]
}
fn format_name(&self) -> &'static str {
"CycloneDX"
}
fn detect(&self, content: &str) -> crate::parsers::traits::FormatDetection {
use crate::parsers::traits::{FormatConfidence, FormatDetection};
let trimmed = content.trim();
if trimmed.starts_with('{') {
let has_bom_format = content.contains("\"bomFormat\"");
let has_cyclonedx = content.contains("CycloneDX") || content.contains("cyclonedx");
let has_spec_version = content.contains("\"specVersion\"");
let has_schema = content.contains("\"$schema\"") && content.contains("cyclonedx");
let version = Self::extract_json_version(content);
if has_bom_format && has_cyclonedx {
let mut detection =
FormatDetection::with_confidence(FormatConfidence::CERTAIN).variant("JSON");
if let Some(v) = version {
detection = detection.version(&v);
}
return detection;
} else if has_bom_format || has_schema {
let mut detection =
FormatDetection::with_confidence(FormatConfidence::HIGH).variant("JSON");
if let Some(v) = version {
detection = detection.version(&v);
}
return detection;
} else if has_spec_version && content.contains("\"components\"") {
return FormatDetection::with_confidence(FormatConfidence::MEDIUM)
.variant("JSON")
.warning("Missing bomFormat field - might not be CycloneDX");
}
}
if trimmed.starts_with('<') {
let has_bom_element = content.contains("<bom");
let has_cyclonedx_ns = content.contains("cyclonedx.org");
let xml_version = Self::extract_xml_version(content);
if has_bom_element && has_cyclonedx_ns {
let mut detection =
FormatDetection::with_confidence(FormatConfidence::CERTAIN).variant("XML");
if let Some(v) = xml_version {
detection = detection.version(&v);
}
return detection;
} else if has_bom_element {
let mut detection = FormatDetection::with_confidence(FormatConfidence::MEDIUM)
.variant("XML")
.warning("Missing CycloneDX namespace");
if let Some(v) = xml_version {
detection = detection.version(&v);
}
return detection;
}
}
FormatDetection::no_match()
}
}
impl CycloneDxParser {
fn extract_json_version(content: &str) -> Option<String> {
if let Some(idx) = content.find("\"specVersion\"") {
let after = &content[idx..];
if let Some(colon_idx) = after.find(':') {
let value_part = &after[colon_idx + 1..];
if let Some(quote_start) = value_part.find('"') {
let after_quote = &value_part[quote_start + 1..];
if let Some(quote_end) = after_quote.find('"') {
return Some(after_quote[..quote_end].to_string());
}
}
}
}
None
}
fn extract_xml_version(content: &str) -> Option<String> {
if let Some(bom_idx) = content.find("<bom") {
let bom_part = &content[bom_idx..];
if let Some(gt_idx) = bom_part.find('>') {
let attrs = &bom_part[..gt_idx];
if let Some(ver_idx) = attrs.find("version=") {
let after_ver = &attrs[ver_idx + 8..];
let quote_char = after_ver.chars().next()?;
if quote_char == '"' || quote_char == '\'' {
let after_quote = &after_ver[1..];
if let Some(end_idx) = after_quote.find(quote_char) {
return Some(after_quote[..end_idx].to_string());
}
}
}
}
}
None
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CycloneDxBom {
#[serde(alias = "bomFormat")]
bom_format: Option<String>,
spec_version: String,
serial_number: Option<String>,
version: Option<u32>,
metadata: Option<CdxMetadata>,
components: Option<Vec<CdxComponent>>,
dependencies: Option<Vec<CdxDependency>>,
vulnerabilities: Option<Vec<CdxVulnerability>>,
compositions: Option<Vec<CdxComposition>>,
signature: Option<CdxSignature>,
citations: Option<Vec<CdxCitation>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxComposition {
aggregate: Option<String>,
assemblies: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxCitation {
#[serde(alias = "bom-ref")]
bom_ref: Option<String>,
pointers: Option<Vec<String>>,
expressions: Option<Vec<String>>,
timestamp: Option<String>,
attributed_to: Option<String>,
process: Option<String>,
note: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxDistributionConstraints {
tlp: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxSignature {
algorithm: Option<String>,
value: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxMetadata {
timestamp: Option<String>,
#[serde(default, deserialize_with = "deserialize_tools")]
tools: Option<Vec<CdxTool>>,
authors: Option<Vec<CdxAuthor>>,
component: Option<CdxComponent>,
lifecycles: Option<Vec<CdxLifecycle>>,
distribution_constraints: Option<CdxDistributionConstraints>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxLifecycle {
phase: Option<String>,
name: Option<String>,
description: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxToolsObject {
components: Option<Vec<CdxToolComponent>>,
services: Option<Vec<CdxToolService>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxToolComponent {
name: Option<String>,
version: Option<String>,
#[serde(alias = "bom-ref")]
bom_ref: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxToolService {
name: Option<String>,
version: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxAuthor {
name: Option<String>,
email: Option<String>,
#[serde(alias = "bom-ref")]
bom_ref: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CdxTool {
name: Option<String>,
version: Option<String>,
}
fn deserialize_tools<'de, D>(deserializer: D) -> Result<Option<Vec<CdxTool>>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, MapAccess, SeqAccess, Visitor};
use std::fmt;
struct ToolsVisitor;
impl<'de> Visitor<'de> for ToolsVisitor {
type Value = Option<Vec<CdxTool>>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an array of tools or an object with components/services")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(None)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut tools = Vec::new();
while let Some(tool) = seq.next_element::<CdxTool>()? {
tools.push(tool);
}
Ok(Some(tools))
}
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let tools_obj: CdxToolsObject =
serde::Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?;
let mut tools = Vec::new();
if let Some(components) = tools_obj.components {
for comp in components {
tools.push(CdxTool {
name: comp.name,
version: comp.version,
});
}
}
if let Some(services) = tools_obj.services {
for svc in services {
tools.push(CdxTool {
name: svc.name,
version: svc.version,
});
}
}
Ok(if tools.is_empty() { None } else { Some(tools) })
}
}
deserializer.deserialize_any(ToolsVisitor)
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CdxComponent {
#[serde(rename = "type")]
component_type: String,
#[serde(alias = "bom-ref")]
bom_ref: Option<String>,
name: String,
version: Option<String>,
group: Option<String>,
scope: Option<String>,
purl: Option<String>,
cpe: Option<String>,
description: Option<String>,
author: Option<String>,
copyright: Option<String>,
licenses: Option<Vec<CdxLicenseChoice>>,
supplier: Option<CdxSupplier>,
hashes: Option<Vec<CdxHash>>,
external_references: Option<Vec<CdxExternalReference>>,
properties: Option<Vec<CdxProperty>>,
#[serde(default)]
is_external: bool,
version_range: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CdxLicenseChoice {
license: Option<CdxLicense>,
expression: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxLicense {
id: Option<String>,
name: Option<String>,
url: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxSupplier {
name: String,
url: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CdxHash {
alg: String,
content: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxExternalReference {
#[serde(rename = "type")]
ref_type: String,
url: String,
comment: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CdxProperty {
name: String,
value: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CdxDependency {
#[serde(rename = "ref")]
ref_field: String,
depends_on: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CdxVulnerability {
id: String,
source: Option<CdxVulnSource>,
description: Option<String>,
recommendation: Option<String>,
ratings: Option<Vec<CdxRating>>,
cwes: Option<Vec<u32>>,
affects: Option<Vec<CdxAffects>>,
analysis: Option<CdxAnalysis>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxVulnSource {
name: String,
url: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CdxRating {
score: Option<f32>,
severity: Option<String>,
method: Option<String>,
vector: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CdxAffects {
#[serde(rename = "ref")]
ref_field: String,
versions: Option<Vec<CdxVersionAffected>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxVersionAffected {
version: Option<String>,
range: Option<String>,
status: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CdxAnalysis {
state: Option<String>,
justification: Option<String>,
response: Option<Vec<String>>,
detail: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename = "bom")]
struct CycloneDxBomXml {
#[serde(rename = "@version")]
version: Option<String>,
#[serde(rename = "@serialNumber")]
serial_number: Option<String>,
#[serde(rename = "@bomVersion")]
bom_version: Option<u32>,
metadata: Option<CdxMetadataXml>,
components: Option<CdxComponentsXml>,
dependencies: Option<CdxDependenciesXml>,
vulnerabilities: Option<CdxVulnerabilitiesXml>,
}
#[derive(Debug, Deserialize)]
struct CdxMetadataXml {
timestamp: Option<String>,
tools: Option<CdxToolsXml>,
component: Option<CdxComponent>,
}
#[derive(Debug, Deserialize)]
struct CdxToolsXml {
#[serde(rename = "tool", default)]
tool: Vec<CdxTool>,
}
#[derive(Debug, Deserialize)]
struct CdxComponentsXml {
#[serde(rename = "component", default)]
component: Vec<CdxComponent>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxComponentXml {
#[serde(rename = "@type")]
component_type: String,
#[serde(rename = "@bom-ref")]
bom_ref: Option<String>,
name: String,
version: Option<String>,
group: Option<String>,
purl: Option<String>,
cpe: Option<String>,
description: Option<String>,
author: Option<String>,
copyright: Option<String>,
licenses: Option<CdxLicensesXml>,
supplier: Option<CdxSupplier>,
hashes: Option<CdxHashesXml>,
#[serde(rename = "externalReferences")]
external_references: Option<CdxExternalReferencesXml>,
properties: Option<CdxPropertiesXml>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CdxLicensesXml {
#[serde(rename = "$value", default)]
licenses: Vec<CdxLicenseChoiceXml>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct CdxLicenseChoiceXml {
license: Option<CdxLicense>,
expression: Option<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CdxHashesXml {
#[serde(rename = "hash", default)]
hash: Vec<CdxHashXml>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CdxHashXml {
#[serde(rename = "@alg")]
alg: String,
#[serde(rename = "$value")]
content: String,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CdxExternalReferencesXml {
#[serde(rename = "reference", default)]
reference: Vec<CdxExternalReferenceXml>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CdxExternalReferenceXml {
#[serde(rename = "@type")]
ref_type: String,
url: String,
comment: Option<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CdxPropertiesXml {
#[serde(rename = "property", default)]
property: Vec<CdxPropertyXml>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CdxPropertyXml {
#[serde(rename = "@name")]
name: String,
#[serde(rename = "$value")]
value: String,
}
#[derive(Debug, Deserialize)]
struct CdxDependenciesXml {
#[serde(rename = "dependency", default)]
dependency: Vec<CdxDependency>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CdxDependencyXml {
#[serde(rename = "@ref")]
ref_field: String,
#[serde(rename = "dependency", default)]
depends_on: Vec<CdxDependencyRefXml>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CdxDependencyRefXml {
#[serde(rename = "@ref")]
ref_field: String,
}
#[derive(Debug, Deserialize)]
struct CdxVulnerabilitiesXml {
#[serde(rename = "vulnerability", default)]
vulnerability: Vec<CdxVulnerability>,
}