Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9717a1efda |
Generated
+6
-21
@@ -693,7 +693,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tramiton-core",
|
"tramiton-core",
|
||||||
"tramiton-repro",
|
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@@ -2102,7 +2101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3697,7 +3696,7 @@ version = "0.50.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4682,7 +4681,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys 0.12.1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5560,10 +5559,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
|
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.4.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6150,20 +6149,6 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tramiton-repro"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "git+ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git?tag=v0.4.0#e3dc1bf7027a2f6d7b1fe43043d6dfa887ce4af3"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"sha2",
|
|
||||||
"tempfile",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"toml",
|
|
||||||
"tramiton-core",
|
|
||||||
"walkdir",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter"
|
name = "tree-sitter"
|
||||||
version = "0.24.7"
|
version = "0.24.7"
|
||||||
@@ -6722,7 +6707,7 @@ version = "0.1.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ compliance-dast = { path = "../compliance-dast" }
|
|||||||
# available to the onboarding classifier. NOTE: CI must be able to fetch this
|
# available to the onboarding classifier. NOTE: CI must be able to fetch this
|
||||||
# private repo (see the git-auth step in .gitea/workflows/ci.yml).
|
# private repo (see the git-auth step in .gitea/workflows/ci.yml).
|
||||||
tramiton-core = { git = "ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git", tag = "v0.4.0" }
|
tramiton-core = { git = "ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git", tag = "v0.4.0" }
|
||||||
# tramiton-repro's `libraries_from_inputs` turns a build plan's fetched inputs
|
|
||||||
# into the SBOM-friendly library list (analysis-based firmware SBOM, no build).
|
|
||||||
tramiton-repro = { git = "ssh://git@gitea.meghsakha.com:22222/sharang/tramiton.git", tag = "v0.4.0" }
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
//! 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
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
pub mod code_review;
|
pub mod code_review;
|
||||||
pub mod cve;
|
pub mod cve;
|
||||||
pub mod dedup;
|
pub mod dedup;
|
||||||
pub mod firmware_sbom;
|
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod gitleaks;
|
pub mod gitleaks;
|
||||||
mod graph_build;
|
mod graph_build;
|
||||||
|
|||||||
@@ -461,25 +461,6 @@ impl PipelineOrchestrator {
|
|||||||
} },
|
} },
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
// Refresh the target's cached findings count. The shared pipeline
|
|
||||||
// (Stage 7) increments `repositories`, which the unified path does
|
|
||||||
// not use, so set the accurate total on the target itself.
|
|
||||||
let total = self
|
|
||||||
.db
|
|
||||||
.findings()
|
|
||||||
.count_documents(doc! { "repo_id": target_id })
|
|
||||||
.await
|
|
||||||
.unwrap_or(*count as u64);
|
|
||||||
self.db
|
|
||||||
.onboarded_targets()
|
|
||||||
.update_one(
|
|
||||||
doc! { "_id": oid },
|
|
||||||
doc! { "$set": {
|
|
||||||
"findings_count": total as i64,
|
|
||||||
"updated_at": mongodb::bson::DateTime::now(),
|
|
||||||
} },
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(target_id, error = %e, "Unified scan pipeline failed");
|
tracing::error!(target_id, error = %e, "Unified scan pipeline failed");
|
||||||
@@ -601,47 +582,6 @@ impl PipelineOrchestrator {
|
|||||||
tracing::warn!(target_id, error = %e, "Unified pipeline: classification failed")
|
tracing::warn!(target_id, error = %e, "Unified pipeline: classification failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Analysis-based firmware SBOM: for embedded targets, derive components
|
|
||||||
// (resolved libraries + cross-toolchain) from tramiton's build-plan
|
|
||||||
// analysis over the already-ingested source — no build, no binary
|
|
||||||
// upload. Best-effort; empty when no build plan forms.
|
|
||||||
if crate::pipeline::firmware_sbom::is_firmware_target(target.target_type) {
|
|
||||||
if let Some(code) = target.code_artifact() {
|
|
||||||
if let Some(path) = working_paths.get(&code.id) {
|
|
||||||
let entries =
|
|
||||||
crate::pipeline::firmware_sbom::firmware_sbom_entries(path, target_id)
|
|
||||||
.await;
|
|
||||||
if !entries.is_empty() {
|
|
||||||
let _ = self
|
|
||||||
.db
|
|
||||||
.sbom_entries()
|
|
||||||
.delete_many(doc! { "repo_id": target_id })
|
|
||||||
.await;
|
|
||||||
for entry in &entries {
|
|
||||||
let filter = doc! {
|
|
||||||
"repo_id": &entry.repo_id,
|
|
||||||
"name": &entry.name,
|
|
||||||
"version": &entry.version,
|
|
||||||
};
|
|
||||||
if let Ok(d) = mongodb::bson::to_document(entry) {
|
|
||||||
let _ = self
|
|
||||||
.db
|
|
||||||
.sbom_entries()
|
|
||||||
.update_one(filter, doc! { "$set": d })
|
|
||||||
.upsert(true)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tracing::info!(
|
|
||||||
target_id,
|
|
||||||
count = entries.len(),
|
|
||||||
"Firmware SBOM: stored components from tramiton analysis"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If the target has a `LiveUrl` artifact and DAST is planned, provision a
|
/// If the target has a `LiveUrl` artifact and DAST is planned, provision a
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pub fn FindingsPage() -> Element {
|
|||||||
let mut selected_ids = use_signal(Vec::<String>::new);
|
let mut selected_ids = use_signal(Vec::<String>::new);
|
||||||
|
|
||||||
let repos = use_resource(|| async {
|
let repos = use_resource(|| async {
|
||||||
crate::infrastructure::onboarding::fetch_targets()
|
crate::infrastructure::repositories::fetch_repositories(1)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
});
|
});
|
||||||
@@ -86,14 +86,14 @@ pub fn FindingsPage() -> Element {
|
|||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
onchange: move |e| { repo_filter.set(e.value()); page.set(1); },
|
onchange: move |e| { repo_filter.set(e.value()); page.set(1); },
|
||||||
option { value: "", "All Targets" }
|
option { value: "", "All Repositories" }
|
||||||
{
|
{
|
||||||
match &*repos.read() {
|
match &*repos.read() {
|
||||||
Some(Some(resp)) => rsx! {
|
Some(Some(resp)) => rsx! {
|
||||||
for t in &resp.data {
|
for repo in &resp.data {
|
||||||
{
|
{
|
||||||
let id = t.get("_id").and_then(|o| o.get("$oid")).and_then(|s| s.as_str()).unwrap_or_default().to_string();
|
let id = repo.id.as_ref().map(|id| id.to_hex()).unwrap_or_default();
|
||||||
let name = t.get("name").and_then(|n| n.as_str()).unwrap_or_default().to_string();
|
let name = repo.name.clone();
|
||||||
rsx! {
|
rsx! {
|
||||||
option { value: "{id}", "{name}" }
|
option { value: "{id}", "{name}" }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ pub fn SbomPage() -> Element {
|
|||||||
let mut diff_repo_a = use_signal(String::new);
|
let mut diff_repo_a = use_signal(String::new);
|
||||||
let mut diff_repo_b = use_signal(String::new);
|
let mut diff_repo_b = use_signal(String::new);
|
||||||
|
|
||||||
// ── Targets for dropdowns ──
|
// ── Repos for dropdowns ──
|
||||||
let repos = use_resource(|| async {
|
let repos = use_resource(|| async {
|
||||||
crate::infrastructure::onboarding::fetch_targets()
|
crate::infrastructure::repositories::fetch_repositories(1)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
});
|
});
|
||||||
@@ -114,14 +114,14 @@ pub fn SbomPage() -> Element {
|
|||||||
select {
|
select {
|
||||||
class: "sbom-filter-select",
|
class: "sbom-filter-select",
|
||||||
onchange: move |e| { repo_filter.set(e.value()); page.set(1); },
|
onchange: move |e| { repo_filter.set(e.value()); page.set(1); },
|
||||||
option { value: "", "All Targets" }
|
option { value: "", "All Repositories" }
|
||||||
{
|
{
|
||||||
match &*repos.read() {
|
match &*repos.read() {
|
||||||
Some(Some(resp)) => rsx! {
|
Some(Some(resp)) => rsx! {
|
||||||
for repo in &resp.data {
|
for repo in &resp.data {
|
||||||
{
|
{
|
||||||
let id = repo.get("_id").and_then(|o| o.get("$oid")).and_then(|s| s.as_str()).unwrap_or_default().to_string();
|
let id = repo.id.as_ref().map(|id| id.to_hex()).unwrap_or_default();
|
||||||
let name = repo.get("name").and_then(|n| n.as_str()).unwrap_or_default().to_string();
|
let name = repo.name.clone();
|
||||||
rsx! { option { value: "{id}", "{name}" } }
|
rsx! { option { value: "{id}", "{name}" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -476,8 +476,8 @@ pub fn SbomPage() -> Element {
|
|||||||
Some(Some(resp)) => rsx! {
|
Some(Some(resp)) => rsx! {
|
||||||
for repo in &resp.data {
|
for repo in &resp.data {
|
||||||
{
|
{
|
||||||
let id = repo.get("_id").and_then(|o| o.get("$oid")).and_then(|s| s.as_str()).unwrap_or_default().to_string();
|
let id = repo.id.as_ref().map(|id| id.to_hex()).unwrap_or_default();
|
||||||
let name = repo.get("name").and_then(|n| n.as_str()).unwrap_or_default().to_string();
|
let name = repo.name.clone();
|
||||||
rsx! { option { value: "{id}", "{name}" } }
|
rsx! { option { value: "{id}", "{name}" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -498,8 +498,8 @@ pub fn SbomPage() -> Element {
|
|||||||
Some(Some(resp)) => rsx! {
|
Some(Some(resp)) => rsx! {
|
||||||
for repo in &resp.data {
|
for repo in &resp.data {
|
||||||
{
|
{
|
||||||
let id = repo.get("_id").and_then(|o| o.get("$oid")).and_then(|s| s.as_str()).unwrap_or_default().to_string();
|
let id = repo.id.as_ref().map(|id| id.to_hex()).unwrap_or_default();
|
||||||
let name = repo.get("name").and_then(|n| n.as_str()).unwrap_or_default().to_string();
|
let name = repo.name.clone();
|
||||||
rsx! { option { value: "{id}", "{name}" } }
|
rsx! { option { value: "{id}", "{name}" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user