Initial commit: Compliance Scanner Agent
Autonomous security and compliance scanning agent for git repositories. Features: SAST (Semgrep), SBOM (Syft), CVE monitoring (OSV.dev/NVD), GDPR/OAuth pattern detection, LLM triage, issue creation (GitHub/GitLab/Jira), PR reviews, and Dioxus fullstack dashboard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
199
compliance-agent/src/pipeline/cve.rs
Normal file
199
compliance-agent/src/pipeline/cve.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use compliance_core::models::{CveAlert, CveSource, SbomEntry, VulnRef};
|
||||
use compliance_core::CoreError;
|
||||
|
||||
pub struct CveScanner {
|
||||
http: reqwest::Client,
|
||||
searxng_url: Option<String>,
|
||||
nvd_api_key: Option<String>,
|
||||
}
|
||||
|
||||
impl CveScanner {
|
||||
pub fn new(http: reqwest::Client, searxng_url: Option<String>, nvd_api_key: Option<String>) -> Self {
|
||||
Self { http, searxng_url, nvd_api_key }
|
||||
}
|
||||
|
||||
pub async fn scan_dependencies(
|
||||
&self,
|
||||
repo_id: &str,
|
||||
entries: &mut [SbomEntry],
|
||||
) -> Result<Vec<CveAlert>, CoreError> {
|
||||
let mut alerts = Vec::new();
|
||||
|
||||
// Batch query OSV.dev
|
||||
let osv_results = self.query_osv_batch(entries).await?;
|
||||
for (idx, vulns) in osv_results.into_iter().enumerate() {
|
||||
if let Some(entry) = entries.get_mut(idx) {
|
||||
for vuln in &vulns {
|
||||
entry.known_vulnerabilities.push(VulnRef {
|
||||
id: vuln.id.clone(),
|
||||
source: "osv".to_string(),
|
||||
severity: vuln.severity.clone(),
|
||||
url: Some(format!("https://osv.dev/vulnerability/{}", vuln.id)),
|
||||
});
|
||||
|
||||
let mut alert = CveAlert::new(
|
||||
vuln.id.clone(),
|
||||
repo_id.to_string(),
|
||||
entry.name.clone(),
|
||||
entry.version.clone(),
|
||||
CveSource::Osv,
|
||||
);
|
||||
alert.summary = vuln.summary.clone();
|
||||
alerts.push(alert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich with NVD CVSS scores
|
||||
for alert in &mut alerts {
|
||||
if let Ok(Some(cvss)) = self.query_nvd(&alert.cve_id).await {
|
||||
alert.cvss_score = Some(cvss);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(alerts)
|
||||
}
|
||||
|
||||
async fn query_osv_batch(&self, entries: &[SbomEntry]) -> Result<Vec<Vec<OsvVuln>>, CoreError> {
|
||||
let queries: Vec<_> = entries
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
e.purl.as_ref().map(|purl| {
|
||||
serde_json::json!({
|
||||
"package": { "purl": purl }
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
if queries.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let body = serde_json::json!({ "queries": queries });
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.post("https://api.osv.dev/v1/querybatch")
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| CoreError::Http(format!("OSV.dev request failed: {e}")))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
tracing::warn!("OSV.dev returned {status}: {body}");
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let result: OsvBatchResponse = resp.json().await.map_err(|e| {
|
||||
CoreError::Http(format!("Failed to parse OSV.dev response: {e}"))
|
||||
})?;
|
||||
|
||||
let vulns = result
|
||||
.results
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
r.vulns
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|v| OsvVuln {
|
||||
id: v.id,
|
||||
summary: v.summary,
|
||||
severity: v.database_specific
|
||||
.and_then(|d| d.get("severity").and_then(|s| s.as_str()).map(String::from)),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(vulns)
|
||||
}
|
||||
|
||||
async fn query_nvd(&self, cve_id: &str) -> Result<Option<f64>, CoreError> {
|
||||
if !cve_id.starts_with("CVE-") {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let url = format!("https://services.nvd.nist.gov/rest/json/cves/2.0?cveId={cve_id}");
|
||||
let mut req = self.http.get(&url);
|
||||
|
||||
if let Some(key) = &self.nvd_api_key {
|
||||
req = req.header("apiKey", key.as_str());
|
||||
}
|
||||
|
||||
let resp = req.send().await.map_err(|e| {
|
||||
CoreError::Http(format!("NVD request failed: {e}"))
|
||||
})?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let body: serde_json::Value = resp.json().await.map_err(|e| {
|
||||
CoreError::Http(format!("Failed to parse NVD response: {e}"))
|
||||
})?;
|
||||
|
||||
// Extract CVSS v3.1 base score
|
||||
let score = body["vulnerabilities"]
|
||||
.as_array()
|
||||
.and_then(|v| v.first())
|
||||
.and_then(|v| v["cve"]["metrics"]["cvssMetricV31"].as_array())
|
||||
.and_then(|m| m.first())
|
||||
.and_then(|m| m["cvssData"]["baseScore"].as_f64());
|
||||
|
||||
Ok(score)
|
||||
}
|
||||
|
||||
pub async fn search_context(&self, cve_id: &str) -> Result<Vec<String>, CoreError> {
|
||||
let Some(searxng_url) = &self.searxng_url else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
let url = format!("{}/search?q={cve_id}&format=json&engines=duckduckgo", searxng_url.trim_end_matches('/'));
|
||||
let resp = self.http.get(&url).send().await.map_err(|e| {
|
||||
CoreError::Http(format!("SearXNG request failed: {e}"))
|
||||
})?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let body: serde_json::Value = resp.json().await.unwrap_or_default();
|
||||
let results = body["results"]
|
||||
.as_array()
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.take(5)
|
||||
.filter_map(|r| r["url"].as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct OsvBatchResponse {
|
||||
results: Vec<OsvBatchResult>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct OsvBatchResult {
|
||||
vulns: Option<Vec<OsvVulnEntry>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct OsvVulnEntry {
|
||||
id: String,
|
||||
summary: Option<String>,
|
||||
database_specific: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
struct OsvVuln {
|
||||
id: String,
|
||||
summary: Option<String>,
|
||||
severity: Option<String>,
|
||||
}
|
||||
Reference in New Issue
Block a user