feat(pipeline): analysis-based firmware SBOM from tramiton (#156)
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Successful in 5m3s
CI / Deploy Dashboard (push) Successful in 3m46s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2m14s
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Successful in 5m3s
CI / Deploy Dashboard (push) Successful in 3m46s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2m14s
This commit was merged in pull request #156.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
//! Analysis-based firmware SBOM.
|
||||
//!
|
||||
//! Derives a Software Bill of Materials for a firmware / embedded target from
|
||||
//! tramiton's build-plan analysis — the resolved external libraries and the
|
||||
//! cross-toolchain — *without* running a reproducible build. It reuses the same
|
||||
//! `tramiton_core::provider::analyze` pass classification runs, so an SBOM comes
|
||||
//! out of the source tree with no binary upload and no build toolchain in the
|
||||
//! agent image.
|
||||
//!
|
||||
//! A full reproducible-build SBOM (with artifact-level content hashes from a
|
||||
//! sealed `tramiton.lock`) is a later, opt-in phase — it needs tramiton's nix
|
||||
//! build backend available to the agent.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use compliance_core::models::{SbomEntry, TargetType};
|
||||
|
||||
/// Whether analysis-based firmware SBOM applies to this target family.
|
||||
pub fn is_firmware_target(target_type: TargetType) -> bool {
|
||||
matches!(
|
||||
target_type,
|
||||
TargetType::FirmwareBareMetal | TargetType::FirmwareRtos | TargetType::EmbeddedLinuxYocto
|
||||
)
|
||||
}
|
||||
|
||||
/// Build SBOM entries for a firmware target by analyzing its source tree with
|
||||
/// tramiton. Returns an empty vector when tramiton cannot form a build plan
|
||||
/// (e.g. no recognizable embedded build system), so callers can treat "no
|
||||
/// firmware SBOM" as simply an empty result.
|
||||
pub async fn firmware_sbom_entries(path: &Path, repo_id: &str) -> Vec<SbomEntry> {
|
||||
let p = path.to_path_buf();
|
||||
// `analyze` is CPU-bound source inspection — keep it off the async runtime.
|
||||
let plan = match tokio::task::spawn_blocking(move || {
|
||||
let repo = tramiton_core::Repo::new(&p);
|
||||
tramiton_core::provider::analyze(&repo)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(plan)) => plan,
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!(repo_id, error = %e, "Firmware SBOM: tramiton analyze failed");
|
||||
return Vec::new();
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(repo_id, error = %e, "Firmware SBOM: analyze task join error");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
let Some(bp) = plan else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
// The cross-toolchain, recorded as a component so the SBOM captures how the
|
||||
// firmware is built (arm-none-eabi-gcc, zephyr-sdk, ...).
|
||||
if let Some(id) = bp.toolchain.id.clone() {
|
||||
let version = bp.toolchain.version.clone().unwrap_or_default();
|
||||
entries.push(SbomEntry::new(
|
||||
repo_id.to_string(),
|
||||
id,
|
||||
version,
|
||||
"toolchain".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Resolved external libraries — the SBOM-friendly view of the plan's fetched
|
||||
// build inputs (name @ revision, with the upstream source when known).
|
||||
for lib in tramiton_repro::lock::libraries_from_inputs(&bp.inputs) {
|
||||
let mut entry = SbomEntry::new(
|
||||
repo_id.to_string(),
|
||||
lib.name,
|
||||
lib.revision,
|
||||
"tramiton".to_string(),
|
||||
);
|
||||
entry.purl = lib.source;
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
Reference in New Issue
Block a user