From fabd3974786d939dac5e92c3e327ba8fd4d985f5 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:14:55 +0200 Subject: [PATCH] fix: create CVE notifications during scan, fix help chat doc loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug fixes: 1. CVE notifications now created during scan pipeline (not just hourly) - Previously, notifications were only created by the scheduled monitor_cves job. Users with 4 CVE alerts saw 0 notifications. - Now the scan pipeline (Stage 3) creates notifications immediately when CVE alerts are discovered, with the same dedup logic. 2. Help chat doc context loading fixed for Docker/production - Added HELP_DOCS_PATH env var for explicit doc root configuration - Added fallback chain: env var → binary location → cwd → Docker paths - Dockerfile.agent now copies README.md and docs/ into /app and sets HELP_DOCS_PATH=/app so the help chat has doc context in production Co-Authored-By: Claude Opus 4.6 (1M context) --- Dockerfile.agent | 5 ++ .../src/api/handlers/help_chat.rs | 56 ++++++++++---- compliance-agent/src/pipeline/orchestrator.rs | 75 +++++++++++++++---- 3 files changed, 109 insertions(+), 27 deletions(-) diff --git a/Dockerfile.agent b/Dockerfile.agent index b20f8e3..8c54dd2 100644 --- a/Dockerfile.agent +++ b/Dockerfile.agent @@ -33,6 +33,11 @@ RUN pip3 install --break-system-packages ruff COPY --from=builder /app/target/release/compliance-agent /usr/local/bin/compliance-agent +# Copy documentation for the help chat assistant +COPY --from=builder /app/README.md /app/README.md +COPY --from=builder /app/docs /app/docs +ENV HELP_DOCS_PATH=/app + # Ensure SSH key directory exists RUN mkdir -p /data/compliance-scanner/ssh diff --git a/compliance-agent/src/api/handlers/help_chat.rs b/compliance-agent/src/api/handlers/help_chat.rs index 78a64cf..dfbc4ec 100644 --- a/compliance-agent/src/api/handlers/help_chat.rs +++ b/compliance-agent/src/api/handlers/help_chat.rs @@ -104,28 +104,58 @@ fn load_docs(root: &Path) -> String { /// Returns a reference to the cached doc context string, initialised on /// first call via `OnceLock`. +/// +/// Discovery order: +/// 1. `HELP_DOCS_PATH` env var (explicit override) +/// 2. Walk up from the binary location +/// 3. Current working directory +/// 4. Common Docker paths (/app, /opt/compliance-scanner) fn doc_context() -> &'static str { DOC_CONTEXT.get_or_init(|| { + // 1. Explicit env var + if let Ok(path) = std::env::var("HELP_DOCS_PATH") { + let p = PathBuf::from(&path); + if p.join("README.md").is_file() || p.join("docs").is_dir() { + tracing::info!("help_chat: loading docs from HELP_DOCS_PATH={path}"); + return load_docs(&p); + } + tracing::warn!("help_chat: HELP_DOCS_PATH={path} has no README.md or docs/"); + } + + // 2. Walk up from binary location let start = std::env::current_exe() .ok() .and_then(|p| p.parent().map(Path::to_path_buf)) .unwrap_or_else(|| PathBuf::from(".")); - match find_project_root(&start) { - Some(root) => load_docs(&root), - None => { - // Fallback: try current working directory - let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - if cwd.join("README.md").is_file() { - return load_docs(&cwd); - } - tracing::error!( - "help_chat: could not locate project root from {}; doc context will be empty", - start.display() - ); - String::new() + if let Some(root) = find_project_root(&start) { + return load_docs(&root); + } + + // 3. Current working directory + if let Ok(cwd) = std::env::current_dir() { + if let Some(root) = find_project_root(&cwd) { + return load_docs(&root); + } + if cwd.join("README.md").is_file() { + return load_docs(&cwd); } } + + // 4. Common Docker/deployment paths + for candidate in ["/app", "/opt/compliance-scanner", "/srv/compliance-scanner"] { + let p = PathBuf::from(candidate); + if p.join("README.md").is_file() || p.join("docs").is_dir() { + tracing::info!("help_chat: found docs at {candidate}"); + return load_docs(&p); + } + } + + tracing::error!( + "help_chat: could not locate project root; doc context will be empty. \ + Set HELP_DOCS_PATH to the directory containing README.md and docs/" + ); + String::new() }) } diff --git a/compliance-agent/src/pipeline/orchestrator.rs b/compliance-agent/src/pipeline/orchestrator.rs index b02dc7a..9a68606 100644 --- a/compliance-agent/src/pipeline/orchestrator.rs +++ b/compliance-agent/src/pipeline/orchestrator.rs @@ -315,20 +315,67 @@ impl PipelineOrchestrator { .await?; } - // Persist CVE alerts (upsert by cve_id + repo_id) - for alert in &cve_alerts { - let filter = doc! { - "cve_id": &alert.cve_id, - "repo_id": &alert.repo_id, - }; - let update = mongodb::bson::to_document(alert) - .map(|d| doc! { "$set": d }) - .unwrap_or_else(|_| doc! {}); - self.db - .cve_alerts() - .update_one(filter, update) - .upsert(true) - .await?; + // Persist CVE alerts and create notifications + { + use compliance_core::models::notification::{parse_severity, CveNotification}; + + let repo_name = repo.name.clone(); + let mut new_notif_count = 0u32; + + for alert in &cve_alerts { + // Upsert the alert + let filter = doc! { + "cve_id": &alert.cve_id, + "repo_id": &alert.repo_id, + }; + let update = mongodb::bson::to_document(alert) + .map(|d| doc! { "$set": d }) + .unwrap_or_else(|_| doc! {}); + self.db + .cve_alerts() + .update_one(filter, update) + .upsert(true) + .await?; + + // Create notification (dedup by cve_id + repo + package + version) + let notif_filter = doc! { + "cve_id": &alert.cve_id, + "repo_id": &alert.repo_id, + "package_name": &alert.affected_package, + "package_version": &alert.affected_version, + }; + let severity = parse_severity(alert.severity.as_deref(), alert.cvss_score); + let mut notification = CveNotification::new( + alert.cve_id.clone(), + repo_id.clone(), + repo_name.clone(), + alert.affected_package.clone(), + alert.affected_version.clone(), + severity, + ); + notification.cvss_score = alert.cvss_score; + notification.summary = alert.summary.clone(); + notification.url = Some(format!("https://osv.dev/vulnerability/{}", alert.cve_id)); + + let notif_update = doc! { + "$setOnInsert": mongodb::bson::to_bson(¬ification).unwrap_or_default() + }; + if let Ok(result) = self + .db + .cve_notifications() + .update_one(notif_filter, notif_update) + .upsert(true) + .await + { + if result.upserted_id.is_some() { + new_notif_count += 1; + } + } + } + + if new_notif_count > 0 { + tracing::info!("[{repo_id}] Created {new_notif_count} CVE notification(s)"); + } } // Stage 6: Issue Creation