diff --git a/compliance-agent/src/api/handlers/mod.rs b/compliance-agent/src/api/handlers/mod.rs index 43e39fc..d859d89 100644 --- a/compliance-agent/src/api/handlers/mod.rs +++ b/compliance-agent/src/api/handlers/mod.rs @@ -19,8 +19,12 @@ pub struct PaginationParams { pub limit: i64, } -fn default_page() -> u64 { 1 } -fn default_limit() -> i64 { 50 } +fn default_page() -> u64 { + 1 +} +fn default_limit() -> i64 { + 50 +} #[derive(Deserialize)] pub struct FindingsFilter { @@ -73,7 +77,9 @@ pub struct AddRepositoryRequest { pub scan_schedule: Option, } -fn default_branch() -> String { "main".to_string() } +fn default_branch() -> String { + "main".to_string() +} #[derive(Deserialize)] pub struct UpdateStatusRequest { @@ -90,15 +96,43 @@ pub async fn health() -> Json { pub async fn stats_overview(Extension(agent): AgentExt) -> ApiResult { let db = &agent.db; - let total_repositories = db.repositories().count_documents(doc! {}).await.unwrap_or(0); + let total_repositories = db + .repositories() + .count_documents(doc! {}) + .await + .unwrap_or(0); let total_findings = db.findings().count_documents(doc! {}).await.unwrap_or(0); - let critical_findings = db.findings().count_documents(doc! { "severity": "critical" }).await.unwrap_or(0); - let high_findings = db.findings().count_documents(doc! { "severity": "high" }).await.unwrap_or(0); - let medium_findings = db.findings().count_documents(doc! { "severity": "medium" }).await.unwrap_or(0); - let low_findings = db.findings().count_documents(doc! { "severity": "low" }).await.unwrap_or(0); - let total_sbom_entries = db.sbom_entries().count_documents(doc! {}).await.unwrap_or(0); + let critical_findings = db + .findings() + .count_documents(doc! { "severity": "critical" }) + .await + .unwrap_or(0); + let high_findings = db + .findings() + .count_documents(doc! { "severity": "high" }) + .await + .unwrap_or(0); + let medium_findings = db + .findings() + .count_documents(doc! { "severity": "medium" }) + .await + .unwrap_or(0); + let low_findings = db + .findings() + .count_documents(doc! { "severity": "low" }) + .await + .unwrap_or(0); + let total_sbom_entries = db + .sbom_entries() + .count_documents(doc! {}) + .await + .unwrap_or(0); let total_cve_alerts = db.cve_alerts().count_documents(doc! {}).await.unwrap_or(0); - let total_issues = db.tracker_issues().count_documents(doc! {}).await.unwrap_or(0); + let total_issues = db + .tracker_issues() + .count_documents(doc! {}) + .await + .unwrap_or(0); let recent_scans: Vec = match db .scan_runs() @@ -135,9 +169,19 @@ pub async fn list_repositories( ) -> ApiResult> { let db = &agent.db; let skip = (params.page.saturating_sub(1)) * params.limit as u64; - let total = db.repositories().count_documents(doc! {}).await.unwrap_or(0); + let total = db + .repositories() + .count_documents(doc! {}) + .await + .unwrap_or(0); - let repos = match db.repositories().find(doc! {}).skip(skip).limit(params.limit).await { + let repos = match db + .repositories() + .find(doc! {}) + .skip(skip) + .limit(params.limit) + .await + { Ok(cursor) => collect_cursor_async(cursor).await, Err(_) => Vec::new(), }; @@ -208,9 +252,20 @@ pub async fn list_findings( } let skip = (filter.page.saturating_sub(1)) * filter.limit as u64; - let total = db.findings().count_documents(query.clone()).await.unwrap_or(0); + let total = db + .findings() + .count_documents(query.clone()) + .await + .unwrap_or(0); - let findings = match db.findings().find(query).sort(doc! { "created_at": -1 }).skip(skip).limit(filter.limit).await { + let findings = match db + .findings() + .find(query) + .sort(doc! { "created_at": -1 }) + .skip(skip) + .limit(filter.limit) + .await + { Ok(cursor) => collect_cursor_async(cursor).await, Err(_) => Vec::new(), }; @@ -268,9 +323,19 @@ pub async fn list_sbom( ) -> ApiResult> { let db = &agent.db; let skip = (params.page.saturating_sub(1)) * params.limit as u64; - let total = db.sbom_entries().count_documents(doc! {}).await.unwrap_or(0); + let total = db + .sbom_entries() + .count_documents(doc! {}) + .await + .unwrap_or(0); - let entries = match db.sbom_entries().find(doc! {}).skip(skip).limit(params.limit).await { + let entries = match db + .sbom_entries() + .find(doc! {}) + .skip(skip) + .limit(params.limit) + .await + { Ok(cursor) => collect_cursor_async(cursor).await, Err(_) => Vec::new(), }; @@ -288,9 +353,20 @@ pub async fn list_issues( ) -> ApiResult> { let db = &agent.db; let skip = (params.page.saturating_sub(1)) * params.limit as u64; - let total = db.tracker_issues().count_documents(doc! {}).await.unwrap_or(0); + let total = db + .tracker_issues() + .count_documents(doc! {}) + .await + .unwrap_or(0); - let issues = match db.tracker_issues().find(doc! {}).sort(doc! { "created_at": -1 }).skip(skip).limit(params.limit).await { + let issues = match db + .tracker_issues() + .find(doc! {}) + .sort(doc! { "created_at": -1 }) + .skip(skip) + .limit(params.limit) + .await + { Ok(cursor) => collect_cursor_async(cursor).await, Err(_) => Vec::new(), }; @@ -310,7 +386,14 @@ pub async fn list_scan_runs( let skip = (params.page.saturating_sub(1)) * params.limit as u64; let total = db.scan_runs().count_documents(doc! {}).await.unwrap_or(0); - let scans = match db.scan_runs().find(doc! {}).sort(doc! { "started_at": -1 }).skip(skip).limit(params.limit).await { + let scans = match db + .scan_runs() + .find(doc! {}) + .sort(doc! { "started_at": -1 }) + .skip(skip) + .limit(params.limit) + .await + { Ok(cursor) => collect_cursor_async(cursor).await, Err(_) => Vec::new(), }; diff --git a/compliance-agent/src/api/routes.rs b/compliance-agent/src/api/routes.rs index b7a1190..88f5bdf 100644 --- a/compliance-agent/src/api/routes.rs +++ b/compliance-agent/src/api/routes.rs @@ -9,10 +9,16 @@ pub fn build_router() -> Router { .route("/api/v1/stats/overview", get(handlers::stats_overview)) .route("/api/v1/repositories", get(handlers::list_repositories)) .route("/api/v1/repositories", post(handlers::add_repository)) - .route("/api/v1/repositories/{id}/scan", post(handlers::trigger_scan)) + .route( + "/api/v1/repositories/{id}/scan", + post(handlers::trigger_scan), + ) .route("/api/v1/findings", get(handlers::list_findings)) .route("/api/v1/findings/{id}", get(handlers::get_finding)) - .route("/api/v1/findings/{id}/status", patch(handlers::update_finding_status)) + .route( + "/api/v1/findings/{id}/status", + patch(handlers::update_finding_status), + ) .route("/api/v1/sbom", get(handlers::list_sbom)) .route("/api/v1/issues", get(handlers::list_issues)) .route("/api/v1/scan-runs", get(handlers::list_scan_runs)) diff --git a/compliance-agent/src/config.rs b/compliance-agent/src/config.rs index 92b27a6..03ede73 100644 --- a/compliance-agent/src/config.rs +++ b/compliance-agent/src/config.rs @@ -18,8 +18,10 @@ fn env_secret_opt(key: &str) -> Option { pub fn load_config() -> Result { Ok(AgentConfig { mongodb_uri: env_var("MONGODB_URI")?, - mongodb_database: env_var_opt("MONGODB_DATABASE").unwrap_or_else(|| "compliance_scanner".to_string()), - litellm_url: env_var_opt("LITELLM_URL").unwrap_or_else(|| "http://localhost:4000".to_string()), + mongodb_database: env_var_opt("MONGODB_DATABASE") + .unwrap_or_else(|| "compliance_scanner".to_string()), + litellm_url: env_var_opt("LITELLM_URL") + .unwrap_or_else(|| "http://localhost:4000".to_string()), litellm_api_key: SecretString::from(env_var_opt("LITELLM_API_KEY").unwrap_or_default()), litellm_model: env_var_opt("LITELLM_MODEL").unwrap_or_else(|| "gpt-4o".to_string()), github_token: env_secret_opt("GITHUB_TOKEN"), @@ -37,7 +39,9 @@ pub fn load_config() -> Result { .and_then(|p| p.parse().ok()) .unwrap_or(3001), scan_schedule: env_var_opt("SCAN_SCHEDULE").unwrap_or_else(|| "0 0 */6 * * *".to_string()), - cve_monitor_schedule: env_var_opt("CVE_MONITOR_SCHEDULE").unwrap_or_else(|| "0 0 0 * * *".to_string()), - git_clone_base_path: env_var_opt("GIT_CLONE_BASE_PATH").unwrap_or_else(|| "/tmp/compliance-scanner/repos".to_string()), + cve_monitor_schedule: env_var_opt("CVE_MONITOR_SCHEDULE") + .unwrap_or_else(|| "0 0 0 * * *".to_string()), + git_clone_base_path: env_var_opt("GIT_CLONE_BASE_PATH") + .unwrap_or_else(|| "/tmp/compliance-scanner/repos".to_string()), }) } diff --git a/compliance-agent/src/database.rs b/compliance-agent/src/database.rs index 16b3408..a9010d2 100644 --- a/compliance-agent/src/database.rs +++ b/compliance-agent/src/database.rs @@ -1,6 +1,6 @@ use mongodb::bson::doc; -use mongodb::{Client, Collection, IndexModel}; use mongodb::options::IndexOptions; +use mongodb::{Client, Collection, IndexModel}; use compliance_core::models::*; @@ -116,6 +116,7 @@ impl Database { self.inner.collection("tracker_issues") } + #[allow(dead_code)] pub fn raw_collection(&self, name: &str) -> Collection { self.inner.collection(name) } diff --git a/compliance-agent/src/llm/client.rs b/compliance-agent/src/llm/client.rs index ebd72fb..f9a1653 100644 --- a/compliance-agent/src/llm/client.rs +++ b/compliance-agent/src/llm/client.rs @@ -58,7 +58,10 @@ impl LlmClient { user_prompt: &str, temperature: Option, ) -> Result { - let url = format!("{}/v1/chat/completions", self.base_url.trim_end_matches('/')); + let url = format!( + "{}/v1/chat/completions", + self.base_url.trim_end_matches('/') + ); let request_body = ChatCompletionRequest { model: self.model.clone(), @@ -87,19 +90,23 @@ impl LlmClient { req = req.header("Authorization", format!("Bearer {key}")); } - let resp = req.send().await.map_err(|e| { - AgentError::Other(format!("LiteLLM request failed: {e}")) - })?; + let resp = req + .send() + .await + .map_err(|e| AgentError::Other(format!("LiteLLM request failed: {e}")))?; if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); - return Err(AgentError::Other(format!("LiteLLM returned {status}: {body}"))); + return Err(AgentError::Other(format!( + "LiteLLM returned {status}: {body}" + ))); } - let body: ChatCompletionResponse = resp.json().await.map_err(|e| { - AgentError::Other(format!("Failed to parse LiteLLM response: {e}")) - })?; + let body: ChatCompletionResponse = resp + .json() + .await + .map_err(|e| AgentError::Other(format!("Failed to parse LiteLLM response: {e}")))?; body.choices .first() @@ -107,12 +114,16 @@ impl LlmClient { .ok_or_else(|| AgentError::Other("Empty response from LiteLLM".to_string())) } + #[allow(dead_code)] pub async fn chat_with_messages( &self, messages: Vec<(String, String)>, temperature: Option, ) -> Result { - let url = format!("{}/v1/chat/completions", self.base_url.trim_end_matches('/')); + let url = format!( + "{}/v1/chat/completions", + self.base_url.trim_end_matches('/') + ); let request_body = ChatCompletionRequest { model: self.model.clone(), @@ -135,19 +146,23 @@ impl LlmClient { req = req.header("Authorization", format!("Bearer {key}")); } - let resp = req.send().await.map_err(|e| { - AgentError::Other(format!("LiteLLM request failed: {e}")) - })?; + let resp = req + .send() + .await + .map_err(|e| AgentError::Other(format!("LiteLLM request failed: {e}")))?; if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); - return Err(AgentError::Other(format!("LiteLLM returned {status}: {body}"))); + return Err(AgentError::Other(format!( + "LiteLLM returned {status}: {body}" + ))); } - let body: ChatCompletionResponse = resp.json().await.map_err(|e| { - AgentError::Other(format!("Failed to parse LiteLLM response: {e}")) - })?; + let body: ChatCompletionResponse = resp + .json() + .await + .map_err(|e| AgentError::Other(format!("Failed to parse LiteLLM response: {e}")))?; body.choices .first() diff --git a/compliance-agent/src/llm/descriptions.rs b/compliance-agent/src/llm/descriptions.rs index 093493e..518cf93 100644 --- a/compliance-agent/src/llm/descriptions.rs +++ b/compliance-agent/src/llm/descriptions.rs @@ -40,14 +40,19 @@ pub async fn generate_issue_description( finding.title, finding.description, finding.file_path.as_deref().unwrap_or("N/A"), - finding.line_number.map(|n| n.to_string()).unwrap_or_else(|| "N/A".to_string()), + finding + .line_number + .map(|n| n.to_string()) + .unwrap_or_else(|| "N/A".to_string()), finding.code_snippet.as_deref().unwrap_or("N/A"), finding.cwe.as_deref().unwrap_or("N/A"), finding.cve.as_deref().unwrap_or("N/A"), finding.remediation.as_deref().unwrap_or("N/A"), ); - let response = llm.chat(DESCRIPTION_SYSTEM_PROMPT, &user_prompt, Some(0.3)).await?; + let response = llm + .chat(DESCRIPTION_SYSTEM_PROMPT, &user_prompt, Some(0.3)) + .await?; // Extract title from first line, rest is body let mut lines = response.lines(); diff --git a/compliance-agent/src/llm/fixes.rs b/compliance-agent/src/llm/fixes.rs index 518d01a..920acd4 100644 --- a/compliance-agent/src/llm/fixes.rs +++ b/compliance-agent/src/llm/fixes.rs @@ -7,10 +7,7 @@ use crate::llm::LlmClient; const FIX_SYSTEM_PROMPT: &str = r#"You are a security engineer. Given a security finding with code context, suggest a concrete code fix. Return ONLY the fixed code snippet that can directly replace the vulnerable code. Include brief inline comments explaining the fix."#; -pub async fn suggest_fix( - llm: &Arc, - finding: &Finding, -) -> Result { +pub async fn suggest_fix(llm: &Arc, finding: &Finding) -> Result { let user_prompt = format!( "Suggest a fix for this vulnerability:\n\ Language context from file: {}\n\ diff --git a/compliance-agent/src/llm/pr_review.rs b/compliance-agent/src/llm/pr_review.rs index a256bea..50bda17 100644 --- a/compliance-agent/src/llm/pr_review.rs +++ b/compliance-agent/src/llm/pr_review.rs @@ -19,7 +19,10 @@ pub async fn generate_pr_review( findings: &[Finding], ) -> Result<(String, Vec), AgentError> { if findings.is_empty() { - return Ok(("No security issues found in this PR.".to_string(), Vec::new())); + return Ok(( + "No security issues found in this PR.".to_string(), + Vec::new(), + )); } let findings_text: Vec = findings @@ -30,7 +33,10 @@ pub async fn generate_pr_review( severity = f.severity, title = f.title, file = f.file_path.as_deref().unwrap_or("unknown"), - line = f.line_number.map(|n| n.to_string()).unwrap_or_else(|| "?".to_string()), + line = f + .line_number + .map(|n| n.to_string()) + .unwrap_or_else(|| "?".to_string()), code = f.code_snippet.as_deref().unwrap_or("N/A"), rule = f.rule_id.as_deref().unwrap_or("N/A"), ) @@ -43,7 +49,9 @@ pub async fn generate_pr_review( findings_text.join("\n"), ); - let response = llm.chat(PR_REVIEW_SYSTEM_PROMPT, &user_prompt, Some(0.3)).await?; + let response = llm + .chat(PR_REVIEW_SYSTEM_PROMPT, &user_prompt, Some(0.3)) + .await?; // Parse comments from LLM response let comments: Vec = serde_json::from_str::>(&response) @@ -61,7 +69,12 @@ pub async fn generate_pr_review( findings.len(), findings .iter() - .map(|f| format!("- **[{}]** {} in `{}`", f.severity, f.title, f.file_path.as_deref().unwrap_or("unknown"))) + .map(|f| format!( + "- **[{}]** {} in `{}`", + f.severity, + f.title, + f.file_path.as_deref().unwrap_or("unknown") + )) .collect::>() .join("\n"), ); diff --git a/compliance-agent/src/llm/triage.rs b/compliance-agent/src/llm/triage.rs index b7bdbb5..87d745c 100644 --- a/compliance-agent/src/llm/triage.rs +++ b/compliance-agent/src/llm/triage.rs @@ -28,7 +28,10 @@ pub async fn triage_findings(llm: &Arc, findings: &mut Vec) finding.code_snippet.as_deref().unwrap_or("N/A"), ); - match llm.chat(TRIAGE_SYSTEM_PROMPT, &user_prompt, Some(0.1)).await { + match llm + .chat(TRIAGE_SYSTEM_PROMPT, &user_prompt, Some(0.1)) + .await + { Ok(response) => { if let Ok(result) = serde_json::from_str::(&response) { finding.confidence = Some(result.confidence); @@ -46,7 +49,10 @@ pub async fn triage_findings(llm: &Arc, findings: &mut Vec) // If LLM response doesn't parse, keep the finding finding.status = FindingStatus::Triaged; passed += 1; - tracing::warn!("Failed to parse triage response for {}: {response}", finding.fingerprint); + tracing::warn!( + "Failed to parse triage response for {}: {response}", + finding.fingerprint + ); } } Err(e) => { @@ -66,6 +72,7 @@ pub async fn triage_findings(llm: &Arc, findings: &mut Vec) #[derive(serde::Deserialize)] struct TriageResult { #[serde(default)] + #[allow(dead_code)] true_positive: bool, #[serde(default)] confidence: f64, diff --git a/compliance-agent/src/main.rs b/compliance-agent/src/main.rs index d4a5f87..98cf902 100644 --- a/compliance-agent/src/main.rs +++ b/compliance-agent/src/main.rs @@ -1,10 +1,10 @@ use tracing_subscriber::EnvFilter; mod agent; +mod api; mod config; mod database; mod error; -mod api; mod llm; mod pipeline; mod scheduler; @@ -15,7 +15,9 @@ mod webhooks; #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt() - .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) .init(); dotenvy::dotenv().ok(); diff --git a/compliance-agent/src/pipeline/cve.rs b/compliance-agent/src/pipeline/cve.rs index 004ed7a..6c61feb 100644 --- a/compliance-agent/src/pipeline/cve.rs +++ b/compliance-agent/src/pipeline/cve.rs @@ -3,13 +3,22 @@ use compliance_core::CoreError; pub struct CveScanner { http: reqwest::Client, + #[allow(dead_code)] searxng_url: Option, nvd_api_key: Option, } impl CveScanner { - pub fn new(http: reqwest::Client, searxng_url: Option, nvd_api_key: Option) -> Self { - Self { http, searxng_url, nvd_api_key } + pub fn new( + http: reqwest::Client, + searxng_url: Option, + nvd_api_key: Option, + ) -> Self { + Self { + http, + searxng_url, + nvd_api_key, + } } pub async fn scan_dependencies( @@ -87,9 +96,10 @@ impl CveScanner { return Ok(Vec::new()); } - let result: OsvBatchResponse = resp.json().await.map_err(|e| { - CoreError::Http(format!("Failed to parse OSV.dev response: {e}")) - })?; + let result: OsvBatchResponse = resp + .json() + .await + .map_err(|e| CoreError::Http(format!("Failed to parse OSV.dev response: {e}")))?; let vulns = result .results @@ -101,8 +111,9 @@ impl CveScanner { .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)), + severity: v.database_specific.and_then(|d| { + d.get("severity").and_then(|s| s.as_str()).map(String::from) + }), }) .collect() }) @@ -123,17 +134,19 @@ impl CveScanner { req = req.header("apiKey", key.as_str()); } - let resp = req.send().await.map_err(|e| { - CoreError::Http(format!("NVD request failed: {e}")) - })?; + 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}")) - })?; + 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"] @@ -146,15 +159,22 @@ impl CveScanner { Ok(score) } + #[allow(dead_code)] pub async fn search_context(&self, cve_id: &str) -> Result, 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}")) - })?; + 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()); diff --git a/compliance-agent/src/pipeline/git.rs b/compliance-agent/src/pipeline/git.rs index d490558..4544fe5 100644 --- a/compliance-agent/src/pipeline/git.rs +++ b/compliance-agent/src/pipeline/git.rs @@ -41,12 +41,7 @@ impl GitOps { let head_ref = repo.head()?; let head_name = head_ref.name().unwrap_or("HEAD"); - repo.reference( - head_name, - fetch_commit.id(), - true, - "fast-forward", - )?; + repo.reference(head_name, fetch_commit.id(), true, "fast-forward")?; repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?; tracing::info!("Fetched and fast-forwarded {}", repo_path.display()); @@ -68,6 +63,7 @@ impl GitOps { } } + #[allow(dead_code)] pub fn get_changed_files( repo_path: &Path, old_sha: &str, diff --git a/compliance-agent/src/pipeline/orchestrator.rs b/compliance-agent/src/pipeline/orchestrator.rs index 7688d42..0393dee 100644 --- a/compliance-agent/src/pipeline/orchestrator.rs +++ b/compliance-agent/src/pipeline/orchestrator.rs @@ -29,14 +29,15 @@ impl PipelineOrchestrator { llm: Arc, http: reqwest::Client, ) -> Self { - Self { config, db, llm, http } + Self { + config, + db, + llm, + http, + } } - pub async fn run( - &self, - repo_id: &str, - trigger: ScanTrigger, - ) -> Result<(), AgentError> { + pub async fn run(&self, repo_id: &str, trigger: ScanTrigger) -> Result<(), AgentError> { // Look up the repository let repo = self .db @@ -48,7 +49,9 @@ impl PipelineOrchestrator { // Create scan run let scan_run = ScanRun::new(repo_id.to_string(), trigger); let insert = self.db.scan_runs().insert_one(&scan_run).await?; - let scan_run_id = insert.inserted_id.as_object_id() + let scan_run_id = insert + .inserted_id + .as_object_id() .map(|id| id.to_hex()) .unwrap_or_default(); @@ -57,29 +60,35 @@ impl PipelineOrchestrator { // Update scan run status match &result { Ok(count) => { - self.db.scan_runs().update_one( - doc! { "_id": &insert.inserted_id }, - doc! { - "$set": { - "status": "completed", - "current_phase": "completed", - "new_findings_count": *count as i64, - "completed_at": mongodb::bson::DateTime::now(), - } - }, - ).await?; + self.db + .scan_runs() + .update_one( + doc! { "_id": &insert.inserted_id }, + doc! { + "$set": { + "status": "completed", + "current_phase": "completed", + "new_findings_count": *count as i64, + "completed_at": mongodb::bson::DateTime::now(), + } + }, + ) + .await?; } Err(e) => { - self.db.scan_runs().update_one( - doc! { "_id": &insert.inserted_id }, - doc! { - "$set": { - "status": "failed", - "error_message": e.to_string(), - "completed_at": mongodb::bson::DateTime::now(), - } - }, - ).await?; + self.db + .scan_runs() + .update_one( + doc! { "_id": &insert.inserted_id }, + doc! { + "$set": { + "status": "failed", + "error_message": e.to_string(), + "completed_at": mongodb::bson::DateTime::now(), + } + }, + ) + .await?; } } @@ -91,9 +100,7 @@ impl PipelineOrchestrator { repo: &TrackedRepository, scan_run_id: &str, ) -> Result { - let repo_id = repo.id.as_ref() - .map(|id| id.to_hex()) - .unwrap_or_default(); + let repo_id = repo.id.as_ref().map(|id| id.to_hex()).unwrap_or_default(); // Stage 0: Change detection tracing::info!("[{repo_id}] Stage 0: Change detection"); @@ -140,7 +147,10 @@ impl PipelineOrchestrator { k.expose_secret().to_string() }), ); - let cve_alerts = match cve_scanner.scan_dependencies(&repo_id, &mut sbom_entries).await { + let cve_alerts = match cve_scanner + .scan_dependencies(&repo_id, &mut sbom_entries) + .await + { Ok(alerts) => alerts, Err(e) => { tracing::warn!("[{repo_id}] CVE scanning failed: {e}"); @@ -163,7 +173,10 @@ impl PipelineOrchestrator { } // Stage 5: LLM Triage - tracing::info!("[{repo_id}] Stage 5: LLM Triage ({} findings)", all_findings.len()); + tracing::info!( + "[{repo_id}] Stage 5: LLM Triage ({} findings)", + all_findings.len() + ); self.update_phase(scan_run_id, "llm_triage").await; let triaged = crate::llm::triage::triage_findings(&self.llm, &mut all_findings).await; tracing::info!("[{repo_id}] Triaged: {triaged} findings passed confidence threshold"); @@ -223,16 +236,19 @@ impl PipelineOrchestrator { // Issue creation is handled by the trackers module - deferred to agent // Stage 7: Update repository - self.db.repositories().update_one( - doc! { "_id": repo.id }, - doc! { - "$set": { - "last_scanned_commit": ¤t_sha, - "updated_at": mongodb::bson::DateTime::now(), + self.db + .repositories() + .update_one( + doc! { "_id": repo.id }, + doc! { + "$set": { + "last_scanned_commit": ¤t_sha, + "updated_at": mongodb::bson::DateTime::now(), + }, + "$inc": { "findings_count": new_count as i64 }, }, - "$inc": { "findings_count": new_count as i64 }, - }, - ).await?; + ) + .await?; tracing::info!("[{repo_id}] Scan complete: {new_count} new findings"); Ok(new_count) @@ -240,13 +256,17 @@ impl PipelineOrchestrator { async fn update_phase(&self, scan_run_id: &str, phase: &str) { if let Ok(oid) = mongodb::bson::oid::ObjectId::parse_str(scan_run_id) { - let _ = self.db.scan_runs().update_one( - doc! { "_id": oid }, - doc! { - "$set": { "current_phase": phase }, - "$push": { "phases_completed": phase }, - }, - ).await; + let _ = self + .db + .scan_runs() + .update_one( + doc! { "_id": oid }, + doc! { + "$set": { "current_phase": phase }, + "$push": { "phases_completed": phase }, + }, + ) + .await; } } } diff --git a/compliance-agent/src/pipeline/patterns.rs b/compliance-agent/src/pipeline/patterns.rs index 87fa125..27afe88 100644 --- a/compliance-agent/src/pipeline/patterns.rs +++ b/compliance-agent/src/pipeline/patterns.rs @@ -7,6 +7,15 @@ use regex::Regex; use crate::pipeline::dedup; +fn compile_regex(pattern: &str) -> Regex { + Regex::new(pattern).unwrap_or_else(|e| { + tracing::warn!("Invalid regex pattern '{pattern}': {e}, using empty fallback"); + // SAFETY: "^$" is a known-valid regex that matches only empty strings + #[allow(clippy::unwrap_used)] + Regex::new("^$").unwrap() + }) +} + pub struct GdprPatternScanner { patterns: Vec, } @@ -31,7 +40,7 @@ impl GdprPatternScanner { id: "gdpr-pii-logging".to_string(), title: "PII data potentially logged".to_string(), description: "Logging statements that may contain personally identifiable information (email, SSN, phone, IP address).".to_string(), - pattern: Regex::new(r#"(?i)(log|print|console\.|logger\.|tracing::)\s*[\.(].*\b(email|ssn|social.?security|phone.?number|ip.?addr|passport|date.?of.?birth|credit.?card)\b"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + pattern: compile_regex(r#"(?i)(log|print|console\.|logger\.|tracing::)\s*[\.(].*\b(email|ssn|social.?security|phone.?number|ip.?addr|passport|date.?of.?birth|credit.?card)\b"#), severity: Severity::High, file_extensions: vec!["rs", "py", "js", "ts", "java", "go", "rb"].into_iter().map(String::from).collect(), }, @@ -39,7 +48,7 @@ impl GdprPatternScanner { id: "gdpr-no-consent".to_string(), title: "Data collection without apparent consent mechanism".to_string(), description: "Data collection endpoint that doesn't reference consent or opt-in mechanisms.".to_string(), - pattern: Regex::new(r#"(?i)(collect|store|save|persist|record).*\b(personal|user.?data|pii|biometric)\b"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + pattern: compile_regex(r#"(?i)(collect|store|save|persist|record).*\b(personal|user.?data|pii|biometric)\b"#), severity: Severity::Medium, file_extensions: vec!["rs", "py", "js", "ts", "java", "go"].into_iter().map(String::from).collect(), }, @@ -47,7 +56,7 @@ impl GdprPatternScanner { id: "gdpr-no-delete-endpoint".to_string(), title: "Missing data deletion capability".to_string(), description: "User data models or controllers without corresponding deletion endpoints (right to erasure).".to_string(), - pattern: Regex::new(r#"(?i)(class|struct|model)\s+User(?!.*[Dd]elete)"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + pattern: compile_regex(r#"(?i)(class|struct|model)\s+User"#), severity: Severity::Medium, file_extensions: vec!["rs", "py", "js", "ts", "java", "go", "rb"].into_iter().map(String::from).collect(), }, @@ -55,7 +64,7 @@ impl GdprPatternScanner { id: "gdpr-hardcoded-retention".to_string(), title: "Hardcoded data retention period".to_string(), description: "Data retention periods should be configurable for GDPR compliance.".to_string(), - pattern: Regex::new(r#"(?i)(retention|ttl|expire|keep.?for)\s*[=:]\s*\d+"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + pattern: compile_regex(r#"(?i)(retention|ttl|expire|keep.?for)\s*[=:]\s*\d+"#), severity: Severity::Low, file_extensions: vec!["rs", "py", "js", "ts", "java", "go", "yaml", "yml", "toml", "json"].into_iter().map(String::from).collect(), }, @@ -74,7 +83,13 @@ impl Scanner for GdprPatternScanner { } async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result { - let findings = scan_with_patterns(repo_path, repo_id, &self.patterns, ScanType::Gdpr, "gdpr-patterns")?; + let findings = scan_with_patterns( + repo_path, + repo_id, + &self.patterns, + ScanType::Gdpr, + "gdpr-patterns", + )?; Ok(ScanOutput { findings, sbom_entries: Vec::new(), @@ -89,7 +104,7 @@ impl OAuthPatternScanner { id: "oauth-implicit-grant".to_string(), title: "OAuth implicit grant flow detected".to_string(), description: "Implicit grant flow is deprecated and insecure. Use authorization code flow with PKCE instead.".to_string(), - pattern: Regex::new(r#"(?i)(response_type\s*[=:]\s*["']?token|grant_type\s*[=:]\s*["']?implicit)"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + pattern: compile_regex(r#"(?i)(response_type\s*[=:]\s*["']?token|grant_type\s*[=:]\s*["']?implicit)"#), severity: Severity::High, file_extensions: vec!["rs", "py", "js", "ts", "java", "go", "yaml", "yml", "json"].into_iter().map(String::from).collect(), }, @@ -97,7 +112,7 @@ impl OAuthPatternScanner { id: "oauth-missing-pkce".to_string(), title: "OAuth flow without PKCE".to_string(), description: "Authorization code flow should use PKCE (code_challenge/code_verifier) for public clients.".to_string(), - pattern: Regex::new(r#"(?i)authorization.?code(?!.*code.?challenge)(?!.*pkce)"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + pattern: compile_regex(r#"(?i)authorization.?code"#), severity: Severity::Medium, file_extensions: vec!["rs", "py", "js", "ts", "java", "go"].into_iter().map(String::from).collect(), }, @@ -105,7 +120,7 @@ impl OAuthPatternScanner { id: "oauth-token-localstorage".to_string(), title: "Token stored in localStorage".to_string(), description: "Storing tokens in localStorage is vulnerable to XSS. Use httpOnly cookies or secure session storage.".to_string(), - pattern: Regex::new(r#"(?i)localStorage\.(set|get)Item\s*\(\s*["'].*token"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + pattern: compile_regex(r#"(?i)localStorage\.(set|get)Item\s*\(\s*["'].*token"#), severity: Severity::High, file_extensions: vec!["js", "ts", "jsx", "tsx"].into_iter().map(String::from).collect(), }, @@ -113,7 +128,7 @@ impl OAuthPatternScanner { id: "oauth-token-url".to_string(), title: "Token passed in URL parameters".to_string(), description: "Tokens in URLs can leak via referrer headers, server logs, and browser history.".to_string(), - pattern: Regex::new(r#"(?i)(access_token|bearer)\s*[=]\s*.*\b(url|query|param|href)\b"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + pattern: compile_regex(r#"(?i)(access_token|bearer)\s*[=]\s*.*\b(url|query|param|href)\b"#), severity: Severity::High, file_extensions: vec!["rs", "py", "js", "ts", "java", "go"].into_iter().map(String::from).collect(), }, @@ -132,7 +147,13 @@ impl Scanner for OAuthPatternScanner { } async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result { - let findings = scan_with_patterns(repo_path, repo_id, &self.patterns, ScanType::OAuth, "oauth-patterns")?; + let findings = scan_with_patterns( + repo_path, + repo_id, + &self.patterns, + ScanType::OAuth, + "oauth-patterns", + )?; Ok(ScanOutput { findings, sbom_entries: Vec::new(), @@ -211,7 +232,16 @@ fn scan_with_patterns( fn walkdir(path: &Path) -> Result, CoreError> { // Simple recursive file walk, skipping hidden dirs and common non-source dirs - let skip_dirs = [".git", "node_modules", "target", "vendor", ".venv", "__pycache__", "dist", "build"]; + let skip_dirs = [ + ".git", + "node_modules", + "target", + "vendor", + ".venv", + "__pycache__", + "dist", + "build", + ]; let entries: Vec<_> = walkdir::WalkDir::new(path) .into_iter() diff --git a/compliance-agent/src/pipeline/sbom.rs b/compliance-agent/src/pipeline/sbom.rs index a19a97d..ab67663 100644 --- a/compliance-agent/src/pipeline/sbom.rs +++ b/compliance-agent/src/pipeline/sbom.rs @@ -72,7 +72,9 @@ async fn run_syft(repo_path: &Path, repo_id: &str) -> Result, Cor 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()) + lic.id + .clone() + .unwrap_or_else(|| lic.name.clone().unwrap_or_default()) }) }) }); @@ -99,8 +101,10 @@ async fn run_cargo_audit(repo_path: &Path, _repo_id: &str) -> Result Result, vulns: Vec) { +fn merge_audit_vulns(entries: &mut [SbomEntry], vulns: Vec) { for vuln in vulns { if let Some(entry) = entries.iter_mut().find(|e| e.name == vuln.package) { entry.known_vulnerabilities.push(VulnRef { diff --git a/compliance-agent/src/pipeline/semgrep.rs b/compliance-agent/src/pipeline/semgrep.rs index 13e2bb4..6a595b4 100644 --- a/compliance-agent/src/pipeline/semgrep.rs +++ b/compliance-agent/src/pipeline/semgrep.rs @@ -66,11 +66,10 @@ impl Scanner for SemgrepScanner { finding.file_path = Some(r.path); finding.line_number = Some(r.start.line); finding.code_snippet = Some(r.extra.lines); - finding.cwe = r.extra.metadata.and_then(|m| { - m.get("cwe") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - }); + finding.cwe = r + .extra + .metadata + .and_then(|m| m.get("cwe").and_then(|v| v.as_str()).map(|s| s.to_string())); finding }) .collect(); diff --git a/compliance-agent/src/scheduler.rs b/compliance-agent/src/scheduler.rs index 498d2db..66de09c 100644 --- a/compliance-agent/src/scheduler.rs +++ b/compliance-agent/src/scheduler.rs @@ -22,7 +22,9 @@ pub async fn start_scheduler(agent: &ComplianceAgent) -> Result<(), AgentError> }) }) .map_err(|e| AgentError::Scheduler(format!("Failed to create scan job: {e}")))?; - sched.add(scan_job).await + sched + .add(scan_job) + .await .map_err(|e| AgentError::Scheduler(format!("Failed to add scan job: {e}")))?; // CVE monitor job (daily) @@ -36,10 +38,14 @@ pub async fn start_scheduler(agent: &ComplianceAgent) -> Result<(), AgentError> }) }) .map_err(|e| AgentError::Scheduler(format!("Failed to create CVE monitor job: {e}")))?; - sched.add(cve_job).await + sched + .add(cve_job) + .await .map_err(|e| AgentError::Scheduler(format!("Failed to add CVE monitor job: {e}")))?; - sched.start().await + sched + .start() + .await .map_err(|e| AgentError::Scheduler(format!("Failed to start scheduler: {e}")))?; tracing::info!( @@ -65,10 +71,7 @@ async fn scan_all_repos(agent: &ComplianceAgent) { } }; - let repos: Vec<_> = cursor - .filter_map(|r| async { r.ok() }) - .collect() - .await; + let repos: Vec<_> = cursor.filter_map(|r| async { r.ok() }).collect().await; for repo in repos { let repo_id = repo.id.map(|id| id.to_hex()).unwrap_or_default(); @@ -90,10 +93,7 @@ async fn monitor_cves(agent: &ComplianceAgent) { } }; - let entries: Vec<_> = cursor - .filter_map(|r| async { r.ok() }) - .collect() - .await; + let entries: Vec<_> = cursor.filter_map(|r| async { r.ok() }).collect().await; if entries.is_empty() { return; diff --git a/compliance-agent/src/trackers/github.rs b/compliance-agent/src/trackers/github.rs index b383a61..732209c 100644 --- a/compliance-agent/src/trackers/github.rs +++ b/compliance-agent/src/trackers/github.rs @@ -69,8 +69,9 @@ impl IssueTracker for GitHubTracker { // Use the REST API directly for state update let route = format!("/repos/{owner}/{repo}/issues/{issue_number}"); let body = serde_json::json!({ "state": state_str }); - self.client - .post::(route, Some(&body)) + let _: serde_json::Value = self + .client + .post(route, Some(&body)) .await .map_err(|e| CoreError::IssueTracker(format!("GitHub update issue failed: {e}")))?; @@ -123,8 +124,9 @@ impl IssueTracker for GitHubTracker { }); let route = format!("/repos/{owner}/{repo}/pulls/{pr_number}/reviews"); - self.client - .post::(route, Some(&review_body)) + let _: serde_json::Value = self + .client + .post(route, Some(&review_body)) .await .map_err(|e| CoreError::IssueTracker(format!("GitHub PR review failed: {e}")))?; diff --git a/compliance-agent/src/trackers/gitlab.rs b/compliance-agent/src/trackers/gitlab.rs index 6791ec2..330019a 100644 --- a/compliance-agent/src/trackers/gitlab.rs +++ b/compliance-agent/src/trackers/gitlab.rs @@ -63,11 +63,14 @@ impl IssueTracker for GitLabTracker { if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); - return Err(CoreError::IssueTracker(format!("GitLab returned {status}: {body}"))); + return Err(CoreError::IssueTracker(format!( + "GitLab returned {status}: {body}" + ))); } - let issue: serde_json::Value = resp.json().await - .map_err(|e| CoreError::IssueTracker(format!("Failed to parse GitLab response: {e}")))?; + let issue: serde_json::Value = resp.json().await.map_err(|e| { + CoreError::IssueTracker(format!("Failed to parse GitLab response: {e}")) + })?; Ok(TrackerIssue::new( String::new(), @@ -136,7 +139,9 @@ impl IssueTracker for GitLabTracker { let project = Self::project_path(owner, repo); // Post overall review as MR note - let note_url = self.api_url(&format!("/projects/{project}/merge_requests/{pr_number}/notes")); + let note_url = self.api_url(&format!( + "/projects/{project}/merge_requests/{pr_number}/notes" + )); self.http .post(¬e_url) .header("PRIVATE-TOKEN", self.token.expose_secret()) @@ -147,7 +152,9 @@ impl IssueTracker for GitLabTracker { // Post individual line comments as MR discussions for comment in comments { - let disc_url = self.api_url(&format!("/projects/{project}/merge_requests/{pr_number}/discussions")); + let disc_url = self.api_url(&format!( + "/projects/{project}/merge_requests/{pr_number}/discussions" + )); let payload = serde_json::json!({ "body": comment.body, "position": { diff --git a/compliance-agent/src/trackers/jira.rs b/compliance-agent/src/trackers/jira.rs index 048e238..965e77d 100644 --- a/compliance-agent/src/trackers/jira.rs +++ b/compliance-agent/src/trackers/jira.rs @@ -12,7 +12,12 @@ pub struct JiraTracker { } impl JiraTracker { - pub fn new(base_url: String, email: String, api_token: SecretString, project_key: String) -> Self { + pub fn new( + base_url: String, + email: String, + api_token: SecretString, + project_key: String, + ) -> Self { Self { base_url: base_url.trim_end_matches('/').to_string(), email, @@ -25,7 +30,10 @@ impl JiraTracker { fn auth_header(&self) -> String { use base64::Engine; let credentials = format!("{}:{}", self.email, self.api_token.expose_secret()); - format!("Basic {}", base64::engine::general_purpose::STANDARD.encode(credentials)) + format!( + "Basic {}", + base64::engine::general_purpose::STANDARD.encode(credentials) + ) } } @@ -65,7 +73,10 @@ impl IssueTracker for JiraTracker { if !labels.is_empty() { payload["fields"]["labels"] = serde_json::Value::Array( - labels.iter().map(|l| serde_json::Value::String(l.clone())).collect(), + labels + .iter() + .map(|l| serde_json::Value::String(l.clone())) + .collect(), ); } @@ -82,10 +93,14 @@ impl IssueTracker for JiraTracker { if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); - return Err(CoreError::IssueTracker(format!("Jira returned {status}: {body}"))); + return Err(CoreError::IssueTracker(format!( + "Jira returned {status}: {body}" + ))); } - let issue: serde_json::Value = resp.json().await + let issue: serde_json::Value = resp + .json() + .await .map_err(|e| CoreError::IssueTracker(format!("Failed to parse Jira response: {e}")))?; let key = issue["key"].as_str().unwrap_or("").to_string(); @@ -108,7 +123,10 @@ impl IssueTracker for JiraTracker { status: &str, ) -> Result<(), CoreError> { // Get available transitions - let url = format!("{}/rest/api/3/issue/{external_id}/transitions", self.base_url); + let url = format!( + "{}/rest/api/3/issue/{external_id}/transitions", + self.base_url + ); let resp = self .http .get(&url) @@ -129,11 +147,17 @@ impl IssueTracker for JiraTracker { }; if let Some(transition) = transitions.iter().find(|t| { - t["name"].as_str().map(|n| n.eq_ignore_ascii_case(target)).unwrap_or(false) + t["name"] + .as_str() + .map(|n| n.eq_ignore_ascii_case(target)) + .unwrap_or(false) }) { - let transition_id = transition["id"].as_str().unwrap_or(""); + let transition_id = transition["id"].as_str().unwrap_or_default(); self.http - .post(&format!("{}/rest/api/3/issue/{external_id}/transitions", self.base_url)) + .post(format!( + "{}/rest/api/3/issue/{external_id}/transitions", + self.base_url + )) .header("Authorization", self.auth_header()) .json(&serde_json::json!({ "transition": { "id": transition_id } })) .send() @@ -216,7 +240,10 @@ impl IssueTracker for JiraTracker { if let Some(issue) = body["issues"].as_array().and_then(|arr| arr.first()) { let key = issue["key"].as_str().unwrap_or("").to_string(); let url = format!("{}/browse/{}", self.base_url, key); - let title = issue["fields"]["summary"].as_str().unwrap_or("").to_string(); + let title = issue["fields"]["summary"] + .as_str() + .unwrap_or("") + .to_string(); Ok(Some(TrackerIssue::new( String::new(), TrackerType::Jira, diff --git a/compliance-agent/src/webhooks/github.rs b/compliance-agent/src/webhooks/github.rs index e94548e..6f567f9 100644 --- a/compliance-agent/src/webhooks/github.rs +++ b/compliance-agent/src/webhooks/github.rs @@ -98,9 +98,7 @@ async fn handle_pull_request( return StatusCode::OK; } - let repo_url = payload["repository"]["clone_url"] - .as_str() - .unwrap_or(""); + let repo_url = payload["repository"]["clone_url"].as_str().unwrap_or(""); let pr_number = payload["pull_request"]["number"].as_u64().unwrap_or(0); if repo_url.is_empty() || pr_number == 0 { diff --git a/compliance-agent/src/webhooks/gitlab.rs b/compliance-agent/src/webhooks/gitlab.rs index 8c2d82d..b811675 100644 --- a/compliance-agent/src/webhooks/gitlab.rs +++ b/compliance-agent/src/webhooks/gitlab.rs @@ -83,7 +83,9 @@ async fn handle_merge_request( _agent: Arc, payload: &serde_json::Value, ) -> StatusCode { - let action = payload["object_attributes"]["action"].as_str().unwrap_or(""); + let action = payload["object_attributes"]["action"] + .as_str() + .unwrap_or(""); if action != "open" && action != "update" { return StatusCode::OK; } diff --git a/compliance-core/src/models/cve.rs b/compliance-core/src/models/cve.rs index b7ef144..199fc08 100644 --- a/compliance-core/src/models/cve.rs +++ b/compliance-core/src/models/cve.rs @@ -27,7 +27,13 @@ pub struct CveAlert { } impl CveAlert { - pub fn new(cve_id: String, repo_id: String, affected_package: String, affected_version: String, source: CveSource) -> Self { + pub fn new( + cve_id: String, + repo_id: String, + affected_package: String, + affected_version: String, + source: CveSource, + ) -> Self { Self { id: None, cve_id, diff --git a/compliance-dashboard/src/app.rs b/compliance-dashboard/src/app.rs index 4f5252d..24f5d40 100644 --- a/compliance-dashboard/src/app.rs +++ b/compliance-dashboard/src/app.rs @@ -5,6 +5,7 @@ use crate::pages::*; #[derive(Debug, Clone, Routable, PartialEq)] #[rustfmt::skip] +#[allow(clippy::enum_variant_names)] pub enum Route { #[layout(AppShell)] #[route("/")] diff --git a/compliance-dashboard/src/components/stat_card.rs b/compliance-dashboard/src/components/stat_card.rs index ee55c26..c6c790b 100644 --- a/compliance-dashboard/src/components/stat_card.rs +++ b/compliance-dashboard/src/components/stat_card.rs @@ -1,11 +1,7 @@ use dioxus::prelude::*; #[component] -pub fn StatCard( - label: String, - value: String, - #[props(default)] color: String, -) -> Element { +pub fn StatCard(label: String, value: String, #[props(default)] color: String) -> Element { let value_style = if color.is_empty() { String::new() } else { diff --git a/compliance-dashboard/src/infrastructure/findings.rs b/compliance-dashboard/src/infrastructure/findings.rs index 2373a1f..0eba0bb 100644 --- a/compliance-dashboard/src/infrastructure/findings.rs +++ b/compliance-dashboard/src/infrastructure/findings.rs @@ -21,7 +21,10 @@ pub async fn fetch_findings( let state: super::server_state::ServerState = dioxus_fullstack::FullstackContext::extract().await?; - let mut url = format!("{}/api/v1/findings?page={page}&limit=20", state.agent_api_url); + let mut url = format!( + "{}/api/v1/findings?page={page}&limit=20", + state.agent_api_url + ); if !severity.is_empty() { url.push_str(&format!("&severity={severity}")); } @@ -35,8 +38,13 @@ pub async fn fetch_findings( url.push_str(&format!("&repo_id={repo_id}")); } - let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; - let body: FindingsListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + let resp = reqwest::get(&url) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + let body: FindingsListResponse = resp + .json() + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; Ok(body) } @@ -46,8 +54,13 @@ pub async fn fetch_finding_detail(id: String) -> Result dioxus_fullstack::FullstackContext::extract().await?; let url = format!("{}/api/v1/findings/{id}", state.agent_api_url); - let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; - let body: serde_json::Value = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + let resp = reqwest::get(&url) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + let body: serde_json::Value = resp + .json() + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; let finding: Finding = serde_json::from_value(body["data"].clone()) .map_err(|e| ServerFnError::new(e.to_string()))?; Ok(finding) diff --git a/compliance-dashboard/src/infrastructure/issues.rs b/compliance-dashboard/src/infrastructure/issues.rs index 9195868..e1bc580 100644 --- a/compliance-dashboard/src/infrastructure/issues.rs +++ b/compliance-dashboard/src/infrastructure/issues.rs @@ -16,7 +16,12 @@ pub async fn fetch_issues(page: u64) -> Result Result { let state: super::server_state::ServerState = dioxus_fullstack::FullstackContext::extract().await?; - let url = format!("{}/api/v1/repositories?page={page}&limit=20", state.agent_api_url); + let url = format!( + "{}/api/v1/repositories?page={page}&limit=20", + state.agent_api_url + ); - let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; - let body: RepositoryListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + let resp = reqwest::get(&url) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + let body: RepositoryListResponse = resp + .json() + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; Ok(body) } #[server] -pub async fn add_repository(name: String, git_url: String, default_branch: String) -> Result<(), ServerFnError> { +pub async fn add_repository( + name: String, + git_url: String, + default_branch: String, +) -> Result<(), ServerFnError> { let state: super::server_state::ServerState = dioxus_fullstack::FullstackContext::extract().await?; let url = format!("{}/api/v1/repositories", state.agent_api_url); @@ -41,7 +53,9 @@ pub async fn add_repository(name: String, git_url: String, default_branch: Strin if !resp.status().is_success() { let body = resp.text().await.unwrap_or_default(); - return Err(ServerFnError::new(format!("Failed to add repository: {body}"))); + return Err(ServerFnError::new(format!( + "Failed to add repository: {body}" + ))); } Ok(()) diff --git a/compliance-dashboard/src/infrastructure/sbom.rs b/compliance-dashboard/src/infrastructure/sbom.rs index 566037f..a456f0d 100644 --- a/compliance-dashboard/src/infrastructure/sbom.rs +++ b/compliance-dashboard/src/infrastructure/sbom.rs @@ -16,7 +16,12 @@ pub async fn fetch_sbom(page: u64) -> Result { dioxus_fullstack::FullstackContext::extract().await?; let url = format!("{}/api/v1/sbom?page={page}&limit=50", state.agent_api_url); - let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; - let body: SbomListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + let resp = reqwest::get(&url) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + let body: SbomListResponse = resp + .json() + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; Ok(body) } diff --git a/compliance-dashboard/src/infrastructure/scans.rs b/compliance-dashboard/src/infrastructure/scans.rs index 5ca51ab..0eb8dd9 100644 --- a/compliance-dashboard/src/infrastructure/scans.rs +++ b/compliance-dashboard/src/infrastructure/scans.rs @@ -14,9 +14,17 @@ pub struct ScansListResponse { pub async fn fetch_scan_runs(page: u64) -> Result { let state: super::server_state::ServerState = dioxus_fullstack::FullstackContext::extract().await?; - let url = format!("{}/api/v1/scan-runs?page={page}&limit=20", state.agent_api_url); + let url = format!( + "{}/api/v1/scan-runs?page={page}&limit=20", + state.agent_api_url + ); - let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; - let body: ScansListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + let resp = reqwest::get(&url) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + let body: ScansListResponse = resp + .json() + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; Ok(body) } diff --git a/compliance-dashboard/src/infrastructure/stats.rs b/compliance-dashboard/src/infrastructure/stats.rs index 7976051..743fe49 100644 --- a/compliance-dashboard/src/infrastructure/stats.rs +++ b/compliance-dashboard/src/infrastructure/stats.rs @@ -20,8 +20,13 @@ pub async fn fetch_overview_stats() -> Result { dioxus_fullstack::FullstackContext::extract().await?; let url = format!("{}/api/v1/stats/overview", state.agent_api_url); - let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; - let body: serde_json::Value = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + let resp = reqwest::get(&url) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + let body: serde_json::Value = resp + .json() + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; let stats: OverviewStats = serde_json::from_value(body["data"].clone()).unwrap_or_default(); Ok(stats) } diff --git a/compliance-dashboard/src/pages/finding_detail.rs b/compliance-dashboard/src/pages/finding_detail.rs index 050a4ff..7b0a645 100644 --- a/compliance-dashboard/src/pages/finding_detail.rs +++ b/compliance-dashboard/src/pages/finding_detail.rs @@ -11,7 +11,9 @@ pub fn FindingDetailPage(id: String) -> Element { let finding = use_resource(move || { let fid = finding_id.clone(); async move { - crate::infrastructure::findings::fetch_finding_detail(fid).await.ok() + crate::infrastructure::findings::fetch_finding_detail(fid) + .await + .ok() } }); @@ -106,7 +108,7 @@ pub fn FindingDetailPage(id: String) -> Element { } } } - }, + } Some(None) => rsx! { div { class: "card", p { "Finding not found." } } }, diff --git a/compliance-dashboard/src/pages/findings.rs b/compliance-dashboard/src/pages/findings.rs index 66f2162..e8537b7 100644 --- a/compliance-dashboard/src/pages/findings.rs +++ b/compliance-dashboard/src/pages/findings.rs @@ -18,7 +18,9 @@ pub fn FindingsPage() -> Element { let typ = type_filter(); let stat = status_filter(); async move { - crate::infrastructure::findings::fetch_findings(p, sev, typ, stat, String::new()).await.ok() + crate::infrastructure::findings::fetch_findings(p, sev, typ, stat, String::new()) + .await + .ok() } }); @@ -84,7 +86,7 @@ pub fn FindingsPage() -> Element { td { SeverityBadge { severity: finding.severity.to_string() } } td { Link { - to: Route::FindingDetailPage { id: id }, + to: Route::FindingDetailPage { id }, style: "color: var(--accent); text-decoration: none;", "{finding.title}" } diff --git a/compliance-dashboard/src/pages/issues.rs b/compliance-dashboard/src/pages/issues.rs index b69de92..23fc9de 100644 --- a/compliance-dashboard/src/pages/issues.rs +++ b/compliance-dashboard/src/pages/issues.rs @@ -9,9 +9,7 @@ pub fn IssuesPage() -> Element { let issues = use_resource(move || { let p = page(); - async move { - crate::infrastructure::issues::fetch_issues(p).await.ok() - } + async move { crate::infrastructure::issues::fetch_issues(p).await.ok() } }); rsx! { diff --git a/compliance-dashboard/src/pages/overview.rs b/compliance-dashboard/src/pages/overview.rs index 2c3bda8..d846783 100644 --- a/compliance-dashboard/src/pages/overview.rs +++ b/compliance-dashboard/src/pages/overview.rs @@ -15,7 +15,9 @@ pub fn OverviewPage() -> Element { } #[cfg(not(feature = "server"))] { - crate::infrastructure::stats::fetch_overview_stats().await.ok() + crate::infrastructure::stats::fetch_overview_stats() + .await + .ok() } }); @@ -82,7 +84,11 @@ pub fn OverviewPage() -> Element { #[component] fn SeverityBar(label: String, count: u64, max: u64, color: String) -> Element { - let height_pct = if max > 0 { (count as f64 / max as f64) * 100.0 } else { 0.0 }; + let height_pct = if max > 0 { + (count as f64 / max as f64) * 100.0 + } else { + 0.0 + }; let height = format!("{}%", height_pct.max(2.0)); rsx! { diff --git a/compliance-dashboard/src/pages/repositories.rs b/compliance-dashboard/src/pages/repositories.rs index e502b30..f4208f3 100644 --- a/compliance-dashboard/src/pages/repositories.rs +++ b/compliance-dashboard/src/pages/repositories.rs @@ -14,7 +14,9 @@ pub fn RepositoriesPage() -> Element { let repos = use_resource(move || { let p = page(); async move { - crate::infrastructure::repositories::fetch_repositories(p).await.ok() + crate::infrastructure::repositories::fetch_repositories(p) + .await + .ok() } }); diff --git a/compliance-dashboard/src/pages/sbom.rs b/compliance-dashboard/src/pages/sbom.rs index 9bd085a..0cfcb66 100644 --- a/compliance-dashboard/src/pages/sbom.rs +++ b/compliance-dashboard/src/pages/sbom.rs @@ -9,9 +9,7 @@ pub fn SbomPage() -> Element { let sbom = use_resource(move || { let p = page(); - async move { - crate::infrastructure::sbom::fetch_sbom(p).await.ok() - } + async move { crate::infrastructure::sbom::fetch_sbom(p).await.ok() } }); rsx! {