- 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>
191 lines
4.9 KiB
Rust
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,
|
|
}
|