fix: SBOM multi-ecosystem support with correct package managers and licenses
Some checks failed
CI / Format (push) Failing after 39s
CI / Clippy (push) Successful in 4m24s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Format (pull_request) Failing after 3s
CI / Clippy (pull_request) Successful in 4m24s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
CI / Detect Changes (push) Has been skipped
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (push) Has been skipped
CI / Deploy Dashboard (push) Has been skipped
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped

- Extract package manager from PURL instead of CycloneDX component type
  (was showing "library"/"file" instead of "npm"/"cargo"/"pip" etc.)
- Generate missing lock files (Cargo.lock, package-lock.json) before Syft
  scan so repos that gitignore them still get full dependency trees
- Enable Syft remote license lookups for Go, JS, Python, and Java
- Enrich Cargo entries with license data from cargo metadata
- Parse CycloneDX license expressions (e.g. "MIT OR Apache-2.0")
- Delete stale SBOM entries on rescan instead of only upserting
- Add /api/v1/sbom/filters endpoint for dynamic filter options
- Make manager and license dropdowns dynamic from actual DB data
- Add cargo, npm, go, php, ruby, composer, bundler to Docker image

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-03-10 12:49:41 +01:00
parent 0065c7c4b2
commit 9da1d057d5
7 changed files with 329 additions and 28 deletions

View File

@@ -77,8 +77,32 @@ pub struct SbomDiffResponse {
pub data: SbomDiffResultData,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SbomFiltersResponse {
pub package_managers: Vec<String>,
pub licenses: Vec<String>,
}
// ── Server functions ──
#[server]
pub async fn fetch_sbom_filters() -> Result<SbomFiltersResponse, ServerFnError> {
let state: super::server_state::ServerState =
dioxus_fullstack::FullstackContext::extract().await?;
let url = format!("{}/api/v1/sbom/filters", state.agent_api_url);
let resp = reqwest::get(&url)
.await
.map_err(|e| ServerFnError::new(e.to_string()))?;
let text = resp
.text()
.await
.map_err(|e| ServerFnError::new(e.to_string()))?;
let body: SbomFiltersResponse = serde_json::from_str(&text)
.map_err(|e| ServerFnError::new(format!("Parse error: {e} — body: {text}")))?;
Ok(body)
}
#[server]
pub async fn fetch_sbom_filtered(
repo_id: Option<String>,

View File

@@ -36,6 +36,11 @@ pub fn SbomPage() -> Element {
.ok()
});
// ── Dynamic filter options (package managers + licenses from DB) ──
let sbom_filters = use_resource(|| async {
fetch_sbom_filters().await.ok()
});
// ── SBOM list (filtered) ──
let sbom = use_resource(move || {
let p = page();
@@ -132,14 +137,20 @@ pub fn SbomPage() -> Element {
class: "sbom-filter-select",
onchange: move |e| { pm_filter.set(e.value()); page.set(1); },
option { value: "", "All Managers" }
option { value: "npm", "npm" }
option { value: "cargo", "Cargo" }
option { value: "pip", "pip" }
option { value: "go", "Go" }
option { value: "maven", "Maven" }
option { value: "nuget", "NuGet" }
option { value: "composer", "Composer" }
option { value: "gem", "RubyGems" }
{
match &*sbom_filters.read() {
Some(Some(f)) => rsx! {
for pm in &f.package_managers {
{
let val = pm.clone();
let label = pm_display_name(&val);
rsx! { option { value: "{val}", "{label}" } }
}
}
},
_ => rsx! {},
}
}
}
input {
class: "sbom-filter-input",
@@ -166,14 +177,19 @@ pub fn SbomPage() -> Element {
class: "sbom-filter-select",
onchange: move |e| { license_filter.set(e.value()); page.set(1); },
option { value: "", "All Licenses" }
option { value: "MIT", "MIT" }
option { value: "Apache-2.0", "Apache 2.0" }
option { value: "BSD-3-Clause", "BSD 3-Clause" }
option { value: "ISC", "ISC" }
option { value: "GPL-3.0", "GPL 3.0" }
option { value: "GPL-2.0", "GPL 2.0" }
option { value: "LGPL-2.1", "LGPL 2.1" }
option { value: "MPL-2.0", "MPL 2.0" }
{
match &*sbom_filters.read() {
Some(Some(f)) => rsx! {
for lic in &f.licenses {
{
let val = lic.clone();
rsx! { option { value: "{val}", "{val}" } }
}
}
},
_ => rsx! {},
}
}
}
// ── Export button ──
@@ -633,6 +649,21 @@ pub fn SbomPage() -> Element {
}
}
fn pm_display_name(pm: &str) -> &str {
match pm {
"npm" => "npm",
"cargo" => "Cargo",
"pip" => "pip",
"go" | "golang" => "Go",
"maven" => "Maven",
"nuget" => "NuGet",
"composer" => "Composer",
"gem" => "RubyGems",
"github" => "GitHub Actions",
other => other,
}
}
fn license_css_class(license: Option<&str>) -> &'static str {
match license {
Some(l) => {