use log::warn;
use serde::{Deserialize, Serialize};
use crate::bom::{
BillOfMaterials, BillOfMaterialsBuilder, BomParser,
sbom::{BomComponent, BomComponentType, BomTool, BomType, BomVulnerability, Container},
};
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Bom {
#[serde(rename = "$schema")]
pub(crate) schema: Option<String>,
#[serde(rename = "bomFormat")]
pub(crate) bom_format: Option<String>,
#[serde(rename = "specVersion")]
pub(crate) spec_version: String,
#[serde(rename = "serialNumber")]
pub(crate) serial_number: Option<String>,
pub(crate) version: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) metadata: Option<Metadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) components: Option<Vec<Component>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) vulnerabilities: Option<Vec<Vulnerability>>,
}
impl BomParser for Bom {
fn parse(data: &[u8]) -> Result<BillOfMaterials, crate::KonarrError> {
let spec_v1_6: Bom = serde_json::from_slice(data)?;
if spec_v1_6.spec_version != "1.6" {
return Err(crate::KonarrError::ParseSBOM(
"Invalid CycloneDX version".to_string(),
));
}
Ok(spec_v1_6.into())
}
fn parse_path(path: std::path::PathBuf) -> Result<BillOfMaterials, crate::KonarrError> {
let reader = std::fs::File::open(path)?;
let spec_v1_6: Bom = serde_json::from_reader(reader)?;
Ok(spec_v1_6.into())
}
}
impl From<Bom> for BillOfMaterials {
fn from(value: Bom) -> Self {
let mut sbom = BillOfMaterials::new(BomType::CycloneDX_1_6, value.spec_version);
if let Some(metadata) = value.metadata {
if let Some(comp) = metadata.component {
sbom.container = Container {
image: comp.name,
version: comp.version,
..Container::default()
};
}
if let Some(timestamp) = metadata.timestamp {
sbom.timestamp = timestamp;
}
if let Some(tools) = metadata.tools {
for tool in tools.components.iter() {
sbom.tools.push(BomTool {
name: tool.name.clone().unwrap_or_default(),
version: tool.version.clone().unwrap_or_default(),
});
}
}
}
if let Some(components) = value.components {
for comp in components.iter() {
let purl: String = if let Some(purl) = comp.purl.as_ref() {
purl.to_string()
} else if let Some(name) = comp.name.as_ref() {
format!("pkg:deb/{}", name)
} else {
warn!("No purl or name found for component");
continue;
};
let mut bom_comp = BomComponent::from_purl(purl);
if sbom.components.contains(&bom_comp) {
continue;
}
if let Some(typ) = comp.comp_type.as_ref() {
bom_comp.comp_type = BomComponentType::from(typ.to_string());
}
sbom.components.push(bom_comp);
}
}
if let Some(vulns) = value.vulnerabilities {
for vulnerability in vulns.iter() {
let severity = vulnerability
.ratings
.as_ref()
.map_or("Unknown".to_string(), |r| {
r.iter()
.max_by_key(|rating| rating.severity())
.map_or("Unknown".to_string(), |rating| rating.severity().clone())
});
let source = vulnerability
.source
.as_ref()
.map_or("Unknown".to_string(), |s| s.name.clone());
let mut vuln = BomVulnerability::new(vulnerability.id.clone(), source, severity);
if let Some(desc) = &vuln.description {
vuln.description = Some(desc.clone());
}
if let Some(source) = &vulnerability.source {
vuln.url = Some(source.url.clone());
}
if let Some(affects) = &vulnerability.affects {
for v in affects {
vuln.components
.push(BomComponent::from_purl(v.reference.clone()));
}
}
sbom.vulnerabilities.push(vuln);
}
}
sbom
}
}
impl BillOfMaterialsBuilder for Bom {
fn new() -> Self {
Self {
schema: Some("http://cyclonedx.org/schema/bom-1.6.schema.json".to_string()),
bom_format: Some("CycloneDX".to_string()),
spec_version: "1.6".to_string(),
..Default::default()
}
}
fn add_project(&mut self, project: &crate::models::Projects) -> Result<(), crate::KonarrError> {
let tool: Option<Tools> = if let Some(snapshot) = project.snapshots.last() {
let name = snapshot
.find_metadata("bom.tool.name")
.map(|v| v.as_string());
let version = snapshot
.find_metadata("bom.tool.version")
.map(|v| v.as_string());
Some(Tools {
components: vec![
Component {
comp_type: Some("application".to_string()),
name: Some("konarr".to_string()),
version: Some(crate::KONARR_VERSION.to_string()),
..Default::default()
},
Component {
comp_type: Some("application".to_string()),
name: name,
version: version,
..Default::default()
},
],
..Default::default()
})
} else {
None
};
if let Some(metadata) = self.metadata.as_mut() {
metadata.timestamp = Some(project.created_at);
metadata.tools = tool;
metadata.component = Some(Component {
comp_type: Some("container".to_string()),
name: Some(project.name.clone()),
version: project.version(),
..Default::default()
});
} else {
self.metadata = Some(Metadata {
timestamp: Some(chrono::Utc::now()),
tools: tool,
component: Some(Component {
comp_type: Some("container".to_string()),
name: Some(project.name.clone()),
version: project.version(),
..Default::default()
}),
});
}
Ok(())
}
fn add_dependency(
&mut self,
dep: &crate::models::Dependencies,
) -> Result<(), crate::KonarrError> {
if let Some(components) = self.components.as_mut() {
components.push(Component::from(dep));
}
Ok(())
}
fn add_component(
&mut self,
component: &crate::models::Component,
version: &crate::models::ComponentVersion,
) -> Result<(), crate::KonarrError> {
if let Some(comps) = self.components.as_mut() {
comps.push(Component {
comp_type: Some(comptype_to_string(&component.component_type)),
name: Some(component.name.to_string()),
version: Some(version.version()?.to_string()),
purl: Some(component.purl()),
..Default::default()
});
}
Ok(())
}
fn output(&self) -> Result<Vec<u8>, crate::KonarrError> {
Ok(serde_json::to_vec(&self)?)
}
}
fn comptype_to_string(typ: &crate::models::ComponentType) -> String {
match typ {
crate::models::ComponentType::Library
| crate::models::ComponentType::Framework
| crate::models::ComponentType::OperatingEnvironment => "library".to_string(),
crate::models::ComponentType::Container => "container".to_string(),
crate::models::ComponentType::CryptographyLibrary => "cryptographic-asset".to_string(),
crate::models::ComponentType::Application
| crate::models::ComponentType::ProgrammingLanguage => "platform".to_string(),
crate::models::ComponentType::Firmware => "firmware".to_string(),
_ => "library".to_string(),
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub(crate) struct Metadata {
pub(crate) timestamp: Option<chrono::DateTime<chrono::Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) tools: Option<Tools>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) component: Option<Component>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub(crate) struct Tools {
pub(crate) components: Vec<Component>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) services: Option<Vec<ToolService>>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub(crate) struct Component {
#[serde(rename = "bom-ref", skip_serializing_if = "Option::is_none")]
pub(crate) bom_ref: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub(crate) comp_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) purl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) publisher: Option<String>,
}
impl From<&crate::models::Dependencies> for Component {
fn from(value: &crate::models::Dependencies) -> Self {
let comptype = value.component_type();
Component {
comp_type: Some(comptype_to_string(&comptype)),
name: Some(value.name()),
version: value.version(),
purl: Some(value.purl()),
..Default::default()
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct ToolService {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) vendor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) version: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Vulnerability {
#[serde(rename = "bom-ref")]
pub(crate) bom_ref: String,
pub(crate) id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) source: Option<VulnerabilitySource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) references: Option<Vec<VulnerabilityRef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) ratings: Option<Vec<VulnerabilityRating>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) affects: Option<Vec<VulnerabilityCompRef>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct VulnerabilitySource {
pub(crate) name: String,
pub(crate) url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct VulnerabilityRating {
pub(crate) score: Option<f32>,
pub(crate) severity: Option<String>,
pub(crate) method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) vector: Option<String>,
}
impl VulnerabilityRating {
pub fn severity(&self) -> String {
if let Some(sev) = &self.severity {
return sev.clone();
}
"Unknown".to_string()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct VulnerabilityRef {
pub id: String,
pub source: VulnerabilitySource,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct VulnerabilityCompRef {
#[serde(rename = "ref")]
pub(crate) reference: String,
}