mod sbom

module sbom

Planned CycloneDX 1.5 SBOM from a stack lock (pre-install inventory).

Built with the official cyclonedx_bom crate (same models as cargo-cyclonedx / CycloneDX Rust Cargo). Documents are serialized as JSON 1.5 with serial numbers, tool metadata, lifecycle phase, and declared dependency edges — not a post-build compliance scan.

Functions

fn artifact_facts_for_lock(lock: &StackLock) -> HashMap<String, ArtifactFacts>

Read the easyconfigs a lock names, for the facts only they carry.

This is the one place the module touches the filesystem, and it earns it: a lock records which easyconfig was selected, and the checksums and source URLs that make the document verifiable live in that file rather than in the lock. A path that cannot be read or parsed contributes nothing rather than failing the document, since an SBOM missing one component’s hashes is worth more than no SBOM, and the count of what was read is reported to the caller.

fn build_dep_map_from_universe(lock: &StackLock, universe: &Universe) -> HashMap<String, Vec<String>>

Build dep map name -> build-time dependency names (builddependencies) from universe candidates matching the lock.

fn dep_map_from_universe(lock: &StackLock, universe: &Universe) -> HashMap<String, Vec<String>>

Build dep map name -> runtime dependency names from universe candidates matching the lock. Build-time deps are intentionally omitted here so SBOM dependsOn edges stay role-specific; use build_dep_map_from_universe for the build-time list (same shape, separate map).

fn lock_to_bom(lock: &StackLock, runtime_dep_map: Option<&HashMap<String, Vec<String>>>, build_dep_map: Option<&HashMap<String, Vec<String>>>) -> Bom

Typed CycloneDX BOM (1.5 models) — preferred when callers want validation.

fn lock_to_bom_with_facts(lock: &StackLock, facts: SbomFacts<'_>) -> Bom

Build the BOM from a lock plus whatever else the caller knows.

Everything beyond the lock is optional, and what is absent is left absent rather than guessed: a component with no stated checksum carries no hashes, and a plan with nothing unresolved is the only one that claims complete.

fn lock_to_cyclonedx(lock: &StackLock) -> Value

Build a CycloneDX JSON document from a lock only (no dependency map).

Without declared edges each component gets an empty dependsOn list — never all-to-all co-stack edges (those create invalid cyclic BOMs).

fn lock_to_cyclonedx_with_deps(lock: &StackLock, selected_dep_map: Option<&HashMap<String, Vec<String>>>) -> Value

Preferred: when the selected candidates (or full universe selection map) are known, emit dependsOn from each package’s declared EasyBuild-style dependency list intersected with co-selected lock members. When selected_dep_map is None, each package’s dependsOn is empty (unknown), not all-to-all.

fn lock_to_cyclonedx_with_facts(lock: &StackLock, facts: SbomFacts<'_>) -> Value

JSON document from a lock plus caller-supplied facts.

fn lock_to_cyclonedx_with_runtime_and_build(lock: &StackLock, runtime_dep_map: Option<&HashMap<String, Vec<String>>>, build_dep_map: Option<&HashMap<String, Vec<String>>>) -> Value

Like lock_to_cyclonedx_with_deps, also records build-time edges as a component property (eb_stack:buildDependsOn) while runtime edges fill the CycloneDX dependencies graph.

Structs and Unions

struct ArtifactFacts

What an easyconfig states about the artifact one component builds from.

A lock records a selection, not an artifact. These are the fields that make the difference between an inventory and a document someone can verify: which bytes were expected, and where they were fetched from.

checksums: Vec<String>

Checksums the easyconfig states, in the order it states them. Only 64-character hex values are emitted as SHA-256; anything else is carried as a property rather than asserted as a hash of the wrong kind.

source_urls: Vec<String>

Source URLs the easyconfig downloads from.

patches: Vec<String>

Patch filenames applied on top of the source.

struct SbomFacts<'a>

Everything a caller can tell the SBOM builder beyond the lock itself.

Grouped into one struct so the builder keeps a single entry point as more of the spec is filled in, rather than growing another positional argument per field.

runtime_dep_map: Option<&'a HashMap<String, Vec<String>>>

Runtime edges, name to names, which become the dependencies graph.

build_dep_map: Option<&'a HashMap<String, Vec<String>>>

Build edges, kept as a property because CycloneDX dependencies does not distinguish build from runtime in 1.5.

artifacts: Option<&'a HashMap<String, ArtifactFacts>>

Per-package artifact facts, keyed by package name.

unresolved: Option<&'a [String]>

Requirements the plan could not resolve. Their presence is what makes the document’s compositions say incomplete rather than complete.

input_hashes: Option<&'a HashMap<String, String>>

Input hash per package name, from crate::input_hash. Used as the uid of the task that builds it, which is what CycloneDX means by a unique identifier for a resource instance: two plans producing the same hash describe the same build.

build_environment: Option<&'a BTreeMap<String, String>>

The build environment the plan targets: optimisation flags, compute capabilities, the EasyBuild version. A build is reproducible only against a stated environment, since the same recipe compiled with different flags is a different binary, and an SBOM that omits it describes something nobody can reproduce (doi:10.1109/ms.2021.3073045).