#![allow(dead_code)]
use sbom_tools::model::NormalizedSbom;
use sbom_tools::parsers::parse_sbom_str;
use sbom_tools::quality::{
ComplianceChecker, ComplianceLevel, ComplianceResult, ViolationSeverity, rule_meta,
};
use std::collections::BTreeSet;
pub const SHA256_FILLER: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
pub const MD5_FILLER: &str = "d41d8cd98f00b204e9800998ecf8427e";
#[derive(Debug, Clone)]
pub struct ComponentFixture {
pub bom_ref: String,
pub name: String,
pub version: Option<String>,
pub purl: Option<String>,
pub supplier: Option<String>,
pub hashes: Vec<(String, String)>,
pub external_refs: Vec<(String, String)>,
pub licenses: Vec<String>,
pub properties: Vec<(String, String)>,
}
impl ComponentFixture {
pub fn conforming(name: &str) -> Self {
Self {
bom_ref: format!("ref-{name}"),
name: name.to_string(),
version: Some("1.2.3".to_string()),
purl: Some(format!("pkg:npm/{name}@1.2.3")),
supplier: Some("Acme Components Ltd".to_string()),
hashes: vec![("SHA-256".to_string(), SHA256_FILLER.to_string())],
external_refs: Vec::new(),
licenses: Vec::new(),
properties: Vec::new(),
}
}
fn to_json(&self) -> serde_json::Value {
let mut obj = serde_json::Map::new();
obj.insert("type".into(), serde_json::json!("library"));
obj.insert("bom-ref".into(), serde_json::json!(self.bom_ref));
obj.insert("name".into(), serde_json::json!(self.name));
if let Some(version) = &self.version {
obj.insert("version".into(), serde_json::json!(version));
}
if let Some(purl) = &self.purl {
obj.insert("purl".into(), serde_json::json!(purl));
}
if let Some(supplier) = &self.supplier {
obj.insert("supplier".into(), serde_json::json!({ "name": supplier }));
}
if !self.hashes.is_empty() {
let hashes: Vec<_> = self
.hashes
.iter()
.map(|(alg, content)| serde_json::json!({ "alg": alg, "content": content }))
.collect();
obj.insert("hashes".into(), serde_json::json!(hashes));
}
if !self.external_refs.is_empty() {
let refs: Vec<_> = self
.external_refs
.iter()
.map(|(ref_type, url)| serde_json::json!({ "type": ref_type, "url": url }))
.collect();
obj.insert("externalReferences".into(), serde_json::json!(refs));
}
if !self.licenses.is_empty() {
let licenses: Vec<_> = self
.licenses
.iter()
.map(|id| serde_json::json!({ "license": { "id": id } }))
.collect();
obj.insert("licenses".into(), serde_json::json!(licenses));
}
if !self.properties.is_empty() {
let props: Vec<_> = self
.properties
.iter()
.map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
.collect();
obj.insert("properties".into(), serde_json::json!(props));
}
serde_json::Value::Object(obj)
}
}
#[derive(Debug, Clone)]
pub struct SbomFixture {
pub spec_version: String,
pub timestamp: Option<String>,
pub serial_number: Option<String>,
pub tools: Vec<(String, String)>,
pub authors: Vec<(String, Option<String>)>,
pub manufacturer: Option<(String, Option<String>)>,
pub primary: Option<ComponentFixture>,
pub components: Vec<ComponentFixture>,
pub dependencies: Vec<(String, Vec<String>)>,
pub vulnerabilities: Vec<(String, String, String)>,
}
impl SbomFixture {
pub fn component_mut(&mut self, name: &str) -> &mut ComponentFixture {
self.components
.iter_mut()
.find(|c| c.name == name)
.unwrap_or_else(|| panic!("fixture has no component named {name:?}"))
}
pub fn to_json_string(&self) -> String {
let mut doc = serde_json::Map::new();
doc.insert("bomFormat".into(), serde_json::json!("CycloneDX"));
doc.insert("specVersion".into(), serde_json::json!(self.spec_version));
doc.insert("version".into(), serde_json::json!(1));
if let Some(serial) = &self.serial_number {
doc.insert("serialNumber".into(), serde_json::json!(serial));
}
let mut metadata = serde_json::Map::new();
if let Some(ts) = &self.timestamp {
metadata.insert("timestamp".into(), serde_json::json!(ts));
}
if !self.tools.is_empty() {
let tools: Vec<_> = self
.tools
.iter()
.map(|(name, version)| serde_json::json!({ "name": name, "version": version }))
.collect();
metadata.insert("tools".into(), serde_json::json!(tools));
}
if !self.authors.is_empty() {
let authors: Vec<_> = self
.authors
.iter()
.map(|(name, email)| {
let mut a = serde_json::Map::new();
a.insert("name".into(), serde_json::json!(name));
if let Some(email) = email {
a.insert("email".into(), serde_json::json!(email));
}
serde_json::Value::Object(a)
})
.collect();
metadata.insert("authors".into(), serde_json::json!(authors));
}
if let Some((name, email)) = &self.manufacturer {
let mut m = serde_json::Map::new();
m.insert("name".into(), serde_json::json!(name));
if let Some(email) = email {
m.insert("contact".into(), serde_json::json!([{ "email": email }]));
}
metadata.insert("manufacturer".into(), serde_json::Value::Object(m));
}
if let Some(primary) = &self.primary {
metadata.insert("component".into(), primary.to_json());
}
if !metadata.is_empty() {
doc.insert("metadata".into(), serde_json::Value::Object(metadata));
}
let components: Vec<_> = self
.components
.iter()
.map(ComponentFixture::to_json)
.collect();
doc.insert("components".into(), serde_json::json!(components));
if !self.dependencies.is_empty() {
let deps: Vec<_> = self
.dependencies
.iter()
.map(|(from, depends_on)| {
serde_json::json!({ "ref": from, "dependsOn": depends_on })
})
.collect();
doc.insert("dependencies".into(), serde_json::json!(deps));
}
if !self.vulnerabilities.is_empty() {
let vulns: Vec<_> = self
.vulnerabilities
.iter()
.map(|(id, severity, affected_ref)| {
serde_json::json!({
"id": id,
"ratings": [{ "severity": severity }],
"affects": [{ "ref": affected_ref }],
})
})
.collect();
doc.insert("vulnerabilities".into(), serde_json::json!(vulns));
}
serde_json::to_string_pretty(&serde_json::Value::Object(doc))
.expect("fixture JSON must serialize")
}
pub fn parse(&self) -> NormalizedSbom {
let json = self.to_json_string();
parse_sbom_str(&json)
.unwrap_or_else(|e| panic!("fixture must parse as CycloneDX: {e}\n{json}"))
}
}
pub fn check(level: ComplianceLevel, fixture: &SbomFixture) -> ComplianceResult {
ComplianceChecker::new(level).check(&fixture.parse())
}
pub fn render_violations(result: &ComplianceResult) -> String {
if result.violations.is_empty() {
return " (no violations)".to_string();
}
result
.violations
.iter()
.map(|v| {
format!(
" [{sev}] {rule} (sarif {sarif}): {msg}",
sev = v.severity.name(),
rule = v.rule_id,
sarif = v.sarif_rule_id(),
msg = v.message
)
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn assert_no_violations(level: ComplianceLevel, fixture: &SbomFixture) {
let result = check(level, fixture);
assert!(
result.violations.is_empty() && result.is_compliant,
"[{level:?}] conforming fixture must pass with zero violations; got \
is_compliant={compliant}:\n{violations}",
compliant = result.is_compliant,
violations = render_violations(&result)
);
}
pub struct FiringCase {
pub label: &'static str,
pub severity: ViolationSeverity,
pub departs_from_registry_default: bool,
pub fixture: SbomFixture,
}
pub enum RuleCoverage {
Tested {
firing: Vec<FiringCase>,
silent: Box<SbomFixture>,
},
Skipped { reason: &'static str },
}
pub struct RuleEntry {
pub sarif_id: &'static str,
pub coverage: RuleCoverage,
}
impl RuleEntry {
pub fn tested(
sarif_id: &'static str,
severity: ViolationSeverity,
firing: SbomFixture,
silent: SbomFixture,
) -> Self {
Self {
sarif_id,
coverage: RuleCoverage::Tested {
firing: vec![FiringCase {
label: "firing",
severity,
departs_from_registry_default: false,
fixture: firing,
}],
silent: Box::new(silent),
},
}
}
pub fn tested_departing(
sarif_id: &'static str,
severity: ViolationSeverity,
firing: SbomFixture,
silent: SbomFixture,
) -> Self {
Self {
sarif_id,
coverage: RuleCoverage::Tested {
firing: vec![FiringCase {
label: "firing",
severity,
departs_from_registry_default: true,
fixture: firing,
}],
silent: Box::new(silent),
},
}
}
#[must_use]
pub fn with_firing(
mut self,
label: &'static str,
severity: ViolationSeverity,
fixture: SbomFixture,
) -> Self {
self.push_firing(FiringCase {
label,
severity,
departs_from_registry_default: false,
fixture,
});
self
}
#[must_use]
pub fn with_departing_firing(
mut self,
label: &'static str,
severity: ViolationSeverity,
fixture: SbomFixture,
) -> Self {
self.push_firing(FiringCase {
label,
severity,
departs_from_registry_default: true,
fixture,
});
self
}
fn push_firing(&mut self, case: FiringCase) {
match &mut self.coverage {
RuleCoverage::Tested { firing, .. } => firing.push(case),
RuleCoverage::Skipped { .. } => {
panic!("cannot add a firing case to skipped rule {}", self.sarif_id)
}
}
}
pub fn skipped(sarif_id: &'static str, reason: &'static str) -> Self {
Self {
sarif_id,
coverage: RuleCoverage::Skipped { reason },
}
}
}
pub fn run_matrix(level: ComplianceLevel, universe: &[&str], entries: &[RuleEntry]) {
let universe_set: BTreeSet<&str> = universe.iter().copied().collect();
assert_eq!(
universe.len(),
universe_set.len(),
"[{level:?}] rule universe contains duplicates"
);
let mut covered: BTreeSet<&str> = BTreeSet::new();
for entry in entries {
assert!(
covered.insert(entry.sarif_id),
"[{level:?}] duplicate matrix entry for {}",
entry.sarif_id
);
}
assert_eq!(
covered, universe_set,
"[{level:?}] the matrix must cover the standard's rule universe \
exactly: every rule id is tested or explicitly skipped with a reason \
(left = matrix entries, right = universe)"
);
for entry in entries {
let RuleCoverage::Tested { firing, silent } = &entry.coverage else {
continue; };
let id = entry.sarif_id;
let meta = rule_meta(id)
.unwrap_or_else(|| panic!("[{level:?}] {id} does not resolve in the rule registry"));
assert!(!firing.is_empty(), "[{level:?}] {id} has no firing case");
for case in firing {
assert_eq!(
case.severity == meta.default_severity,
!case.departs_from_registry_default,
"[{level:?}] {id} ({label}): expected severity {expected:?} vs registry \
default {default:?} — update the departs_from_registry_default flag \
(or the expectation) so the deviation is explicit",
label = case.label,
expected = case.severity,
default = meta.default_severity,
);
let result = check(level, &case.fixture);
let hits: Vec<_> = result
.violations
.iter()
.filter(|v| v.sarif_rule_id() == id)
.collect();
assert!(
!hits.is_empty(),
"[{level:?}] {id} ({label}): the violating fixture must fire this rule; \
emitted:\n{violations}",
label = case.label,
violations = render_violations(&result)
);
assert!(
hits.iter().any(|v| v.severity == case.severity),
"[{level:?}] {id} ({label}): rule fired but not at {expected:?}; \
emitted:\n{violations}",
label = case.label,
expected = case.severity,
violations = render_violations(&result)
);
}
let result = check(level, silent);
assert!(
result.violations.iter().all(|v| v.sarif_rule_id() != id),
"[{level:?}] {id} (silent): the satisfying fixture must not fire this rule; \
emitted:\n{violations}",
violations = render_violations(&result)
);
}
for entry in entries {
let RuleCoverage::Tested { firing, silent } = &entry.coverage else {
continue;
};
let fixtures = firing
.iter()
.map(|c| (c.label, &c.fixture))
.chain(std::iter::once(("silent", silent.as_ref())));
for (label, fixture) in fixtures {
let result = check(level, fixture);
for v in &result.violations {
assert!(
universe_set.contains(v.sarif_rule_id()),
"[{level:?}] fixture for {id} ({label}) emitted rule {rule} \
(sarif {sarif}) outside the declared rule universe {universe_set:?} — \
new check site? extend the universe and the matrix",
id = entry.sarif_id,
rule = v.rule_id,
sarif = v.sarif_rule_id(),
);
}
}
}
}