///|
fn json_string_field(object : Map[String, Json], key : String) -> String? {
match object.get(key) {
Some(String(value)) => Some(value)
_ => None
}
}
///|
fn sbom_finding(
record_id : String,
decision : SbomImportDecision,
input : String,
canonical : String,
code : String,
offset : Int,
) -> SbomImportFinding {
{ record_id, decision, input, canonical, code, offset, }
}
///|
fn import_one_purl(
record_id : String,
raw : String?,
entries : Array[InventoryEntry],
findings : Array[SbomImportFinding],
) -> Unit {
match raw {
None =>
findings.push(
sbom_finding(record_id, MissingPurl, "", "", "PURL_MISSING", -1),
)
Some(input) => {
let value = parse(input) catch {
Syntax(code, offset) => {
findings.push(
sbom_finding(record_id, InvalidPurl, input, "", code, offset),
)
return
}
Unsupported(code, offset) => {
findings.push(
sbom_finding(record_id, InvalidPurl, input, "", code, offset),
)
return
}
}
let canonical = value.to_string()
entries.push({ record_id, purl: canonical, })
findings.push(sbom_finding(record_id, Imported, input, canonical, "", -1))
}
}
}
///|
fn collect_cyclonedx_components(
values : Array[Json],
parent : String,
entries : Array[InventoryEntry],
findings : Array[SbomImportFinding],
) -> Unit raise PurlError {
for i = 0; i < values.length(); i = i + 1 {
guard values[i] is Object(object) else {
raise Syntax("CYCLONEDX_COMPONENT_OBJECT_REQUIRED", 0)
}
let fallback = if parent == "" {
"component-" + i.to_string()
} else {
parent + "/component-" + i.to_string()
}
let record_id = match json_string_field(object, "bom-ref") {
Some(value) => value
None =>
match json_string_field(object, "name") {
Some(value) => value
None => fallback
}
}
import_one_purl(
record_id,
json_string_field(object, "purl"),
entries,
findings,
)
match object.get("components") {
Some(Array(children)) =>
collect_cyclonedx_components(children, record_id, entries, findings)
Some(_) => raise Syntax("CYCLONEDX_COMPONENTS_ARRAY_REQUIRED", 0)
None => ()
}
}
}
///|
fn finish_sbom_report(
format : SbomFormat,
entries : Array[InventoryEntry],
findings : Array[SbomImportFinding],
) -> SbomImportReport {
let mut imported = 0
let mut missing = 0
let mut invalid = 0
for finding in findings {
match finding.decision {
Imported => imported += 1
MissingPurl => missing += 1
InvalidPurl => invalid += 1
}
}
{ format, entries, findings, imported, missing, invalid, }
}
///|
/// Import top-level and nested CycloneDX components from a JSON BOM. Every
/// component produces a finding; malformed PURLs do not abort sibling rows.
pub fn import_cyclonedx_json(
input : String,
) -> SbomImportReport raise PurlError {
let document = @json.parse(input) catch {
_ => raise Syntax("INVALID_JSON", 0)
}
guard document is Object(root) else {
raise Syntax("CYCLONEDX_OBJECT_REQUIRED", 0)
}
guard root.get("components") is Some(Array(components)) else {
raise Syntax("CYCLONEDX_COMPONENTS_REQUIRED", 0)
}
let entries : Array[InventoryEntry] = []
let findings : Array[SbomImportFinding] = []
collect_cyclonedx_components(components, "", entries, findings)
finish_sbom_report(CycloneDx, entries, findings)
}
///|
fn spdx_purl(object : Map[String, Json]) -> String? raise PurlError {
match object.get("externalRefs") {
None => return None
Some(Array(references)) =>
for value in references {
guard value is Object(reference) else {
raise Syntax("SPDX_EXTERNAL_REF_OBJECT_REQUIRED", 0)
}
let category = json_string_field(reference, "referenceCategory")
let kind = json_string_field(reference, "referenceType")
if category == Some("PACKAGE-MANAGER") &&
(kind == Some("purl") || kind == Some("package-url")) {
return json_string_field(reference, "referenceLocator")
}
}
Some(_) => raise Syntax("SPDX_EXTERNAL_REFS_ARRAY_REQUIRED", 0)
}
None
}
///|
/// Import Package URL external references from SPDX JSON packages.
pub fn import_spdx_json(input : String) -> SbomImportReport raise PurlError {
let document = @json.parse(input) catch {
_ => raise Syntax("INVALID_JSON", 0)
}
guard document is Object(root) else {
raise Syntax("SPDX_OBJECT_REQUIRED", 0)
}
guard root.get("packages") is Some(Array(packages)) else {
raise Syntax("SPDX_PACKAGES_REQUIRED", 0)
}
let entries : Array[InventoryEntry] = []
let findings : Array[SbomImportFinding] = []
for i = 0; i < packages.length(); i = i + 1 {
guard packages[i] is Object(package_object) else {
raise Syntax("SPDX_PACKAGE_OBJECT_REQUIRED", 0)
}
let record_id = match json_string_field(package_object, "SPDXID") {
Some(value) => value
None =>
match json_string_field(package_object, "name") {
Some(value) => value
None => "package-" + i.to_string()
}
}
import_one_purl(record_id, spdx_purl(package_object), entries, findings)
}
finish_sbom_report(Spdx, entries, findings)
}
///|
/// Import and apply inventory policy in one downstream-facing operation.
pub fn audit_cyclonedx_json(
input : String,
policy? : InventoryPolicy = InventoryPolicy::default(),
) -> SbomAuditReport raise PurlError {
let import_report = import_cyclonedx_json(input)
let inventory_report = audit_inventory(import_report.entries, policy~)
{ import_report, inventory_report, }
}
///|
/// Import and apply inventory policy in one downstream-facing operation.
pub fn audit_spdx_json(
input : String,
policy? : InventoryPolicy = InventoryPolicy::default(),
) -> SbomAuditReport raise PurlError {
let import_report = import_spdx_json(input)
let inventory_report = audit_inventory(import_report.entries, policy~)
{ import_report, inventory_report, }
}
///|
/// Compare any two imported SBOM documents through canonical package-version
/// identities. This supports format migration checks such as CycloneDX to SPDX.
pub fn diff_sbom_imports(
before : SbomImportReport,
after : SbomImportReport,
) -> InventoryDelta {
diff_inventory(before.entries, after.entries)
}