Files
compliance-scanner-agent/compliance-agent/src/pipeline/sbom.rs
Sharang Parnerkar 03ee69834d
All checks were successful
CI / Format (push) Successful in 3s
CI / Clippy (push) Successful in 2m15s
CI / Security Audit (push) Successful in 1m34s
CI / Tests (push) Successful in 3m4s
Fix formatting and clippy warnings across workspace
- Run cargo fmt on all crates
- Fix regex patterns using unsupported lookahead in patterns.rs
- Replace unwrap() calls with compile_regex() helper
- Fix never type fallback in GitHub tracker
- Fix redundant field name in findings page
- Allow enum_variant_names for Dioxus Route enum
- Fix &mut Vec -> &mut [T] clippy lint in sbom.rs
- Mark unused-but-intended APIs with #[allow(dead_code)]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:41:03 +01:00

191 lines
4.9 KiB
Rust

use std::path::Path;
use compliance_core::models::{SbomEntry, ScanType, VulnRef};
use compliance_core::traits::{ScanOutput, Scanner};
use compliance_core::CoreError;
pub struct SbomScanner;
impl Scanner for SbomScanner {
fn name(&self) -> &str {
"sbom"
}
fn scan_type(&self) -> ScanType {
ScanType::Sbom
}
async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result<ScanOutput, CoreError> {
let mut entries = Vec::new();
// Run syft for SBOM generation
match run_syft(repo_path, repo_id).await {
Ok(syft_entries) => entries.extend(syft_entries),
Err(e) => tracing::warn!("syft failed: {e}"),
}
// Run cargo-audit for Rust-specific vulns
match run_cargo_audit(repo_path, repo_id).await {
Ok(vulns) => merge_audit_vulns(&mut entries, vulns),
Err(e) => tracing::warn!("cargo-audit skipped: {e}"),
}
Ok(ScanOutput {
findings: Vec::new(),
sbom_entries: entries,
})
}
}
async fn run_syft(repo_path: &Path, repo_id: &str) -> Result<Vec<SbomEntry>, CoreError> {
let output = tokio::process::Command::new("syft")
.arg(repo_path)
.args(["-o", "cyclonedx-json"])
.output()
.await
.map_err(|e| CoreError::Scanner {
scanner: "syft".to_string(),
source: Box::new(e),
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(CoreError::Scanner {
scanner: "syft".to_string(),
source: format!("syft exited with {}: {stderr}", output.status).into(),
});
}
let cdx: CycloneDxBom = serde_json::from_slice(&output.stdout)?;
let entries = cdx
.components
.unwrap_or_default()
.into_iter()
.map(|c| {
let mut entry = SbomEntry::new(
repo_id.to_string(),
c.name,
c.version.unwrap_or_else(|| "unknown".to_string()),
c.component_type.unwrap_or_else(|| "library".to_string()),
);
entry.purl = c.purl;
entry.license = c.licenses.and_then(|ls| {
ls.first().and_then(|l| {
l.license.as_ref().map(|lic| {
lic.id
.clone()
.unwrap_or_else(|| lic.name.clone().unwrap_or_default())
})
})
});
entry
})
.collect();
Ok(entries)
}
async fn run_cargo_audit(repo_path: &Path, _repo_id: &str) -> Result<Vec<AuditVuln>, CoreError> {
let cargo_lock = repo_path.join("Cargo.lock");
if !cargo_lock.exists() {
return Ok(Vec::new());
}
let output = tokio::process::Command::new("cargo")
.args(["audit", "--json"])
.current_dir(repo_path)
.output()
.await
.map_err(|e| CoreError::Scanner {
scanner: "cargo-audit".to_string(),
source: Box::new(e),
})?;
let result: CargoAuditOutput =
serde_json::from_slice(&output.stdout).unwrap_or_else(|_| CargoAuditOutput {
vulnerabilities: CargoAuditVulns { list: Vec::new() },
});
let vulns = result
.vulnerabilities
.list
.into_iter()
.map(|v| AuditVuln {
package: v.advisory.package,
id: v.advisory.id,
url: v.advisory.url,
})
.collect();
Ok(vulns)
}
fn merge_audit_vulns(entries: &mut [SbomEntry], vulns: Vec<AuditVuln>) {
for vuln in vulns {
if let Some(entry) = entries.iter_mut().find(|e| e.name == vuln.package) {
entry.known_vulnerabilities.push(VulnRef {
id: vuln.id.clone(),
source: "cargo-audit".to_string(),
severity: None,
url: Some(vuln.url),
});
}
}
}
// CycloneDX JSON types
#[derive(serde::Deserialize)]
struct CycloneDxBom {
components: Option<Vec<CdxComponent>>,
}
#[derive(serde::Deserialize)]
struct CdxComponent {
name: String,
version: Option<String>,
#[serde(rename = "type")]
component_type: Option<String>,
purl: Option<String>,
licenses: Option<Vec<CdxLicenseWrapper>>,
}
#[derive(serde::Deserialize)]
struct CdxLicenseWrapper {
license: Option<CdxLicense>,
}
#[derive(serde::Deserialize)]
struct CdxLicense {
id: Option<String>,
name: Option<String>,
}
// Cargo audit types
#[derive(serde::Deserialize)]
struct CargoAuditOutput {
vulnerabilities: CargoAuditVulns,
}
#[derive(serde::Deserialize)]
struct CargoAuditVulns {
list: Vec<CargoAuditEntry>,
}
#[derive(serde::Deserialize)]
struct CargoAuditEntry {
advisory: CargoAuditAdvisory,
}
#[derive(serde::Deserialize)]
struct CargoAuditAdvisory {
id: String,
package: String,
url: String,
}
struct AuditVuln {
package: String,
id: String,
url: String,
}