codemem_engine/enrichment/
security_scan.rs1use super::{resolve_path, EnrichResult};
4use crate::CodememEngine;
5use codemem_core::{CodememError, NodeKind};
6use serde_json::json;
7use std::path::Path;
8
9impl CodememEngine {
10 pub fn enrich_security_scan(
13 &self,
14 namespace: Option<&str>,
15 project_root: Option<&Path>,
16 ) -> Result<EnrichResult, CodememError> {
17 use std::sync::LazyLock;
18
19 static CREDENTIAL_PATTERN: LazyLock<regex::Regex> = LazyLock::new(|| {
20 regex::Regex::new(
21 r#"(?i)(password|secret|api_key|apikey|token|private_key)\s*[:=]\s*["'][^"']{8,}["']"#,
22 )
23 .expect("valid regex")
24 });
25
26 static SQL_CONCAT_PATTERN: LazyLock<regex::Regex> = LazyLock::new(|| {
27 regex::Regex::new(
28 r#"(?i)(SELECT|INSERT|UPDATE|DELETE|DROP)\s+.*\+\s*[a-zA-Z_]|format!\s*\(\s*"[^"]*(?:SELECT|INSERT|UPDATE|DELETE)"#,
29 )
30 .expect("valid regex")
31 });
32
33 static UNSAFE_PATTERN: LazyLock<regex::Regex> =
34 LazyLock::new(|| regex::Regex::new(r"unsafe\s*\{").expect("valid regex"));
35
36 let file_nodes: Vec<String> = {
37 let graph = self.lock_graph()?;
38 graph
39 .get_all_nodes()
40 .into_iter()
41 .filter(|n| n.kind == NodeKind::File)
42 .map(|n| n.label.clone())
43 .collect()
44 };
45
46 let mut insights_stored = 0;
47 let mut files_scanned = 0;
48
49 for file_path in &file_nodes {
50 let content = match std::fs::read_to_string(resolve_path(file_path, project_root)) {
51 Ok(c) => c,
52 Err(_) => continue,
53 };
54 files_scanned += 1;
55
56 if CREDENTIAL_PATTERN.is_match(&content) {
58 let text = format!(
59 "Security: Potential hardcoded credential in {} — use environment variables or secrets manager",
60 file_path
61 );
62 if self
63 .store_insight(
64 &text,
65 "security",
66 &["severity:critical", "credentials"],
67 0.95,
68 namespace,
69 &[format!("file:{file_path}")],
70 )
71 .is_some()
72 {
73 insights_stored += 1;
74 }
75 }
76
77 if SQL_CONCAT_PATTERN.is_match(&content) {
79 let text = format!(
80 "Security: Potential SQL injection in {} — use parameterized queries",
81 file_path
82 );
83 if self
84 .store_insight(
85 &text,
86 "security",
87 &["severity:critical", "sql-injection"],
88 0.9,
89 namespace,
90 &[format!("file:{file_path}")],
91 )
92 .is_some()
93 {
94 insights_stored += 1;
95 }
96 }
97
98 if file_path.ends_with(".rs") {
100 let unsafe_count = UNSAFE_PATTERN.find_iter(&content).count();
101 if unsafe_count > 0 {
102 let text = format!(
103 "Security: {} unsafe block(s) in {} — review for memory safety",
104 unsafe_count, file_path
105 );
106 let importance = if unsafe_count > 3 { 0.8 } else { 0.6 };
107 if self
108 .store_insight(
109 &text,
110 "security",
111 &["severity:medium", "unsafe"],
112 importance,
113 namespace,
114 &[format!("file:{file_path}")],
115 )
116 .is_some()
117 {
118 insights_stored += 1;
119 }
120 }
121 }
122 }
123
124 self.save_index();
125
126 Ok(EnrichResult {
127 insights_stored,
128 details: json!({
129 "files_scanned": files_scanned,
130 "insights_stored": insights_stored,
131 }),
132 })
133 }
134}