Skip to main content

codemem_engine/enrichment/
security_scan.rs

1//! Enhanced security scanning: hardcoded credentials, SQL concatenation, unsafe blocks.
2
3use super::{resolve_path, EnrichResult};
4use crate::CodememEngine;
5use codemem_core::{CodememError, NodeKind};
6use serde_json::json;
7use std::path::Path;
8
9impl CodememEngine {
10    /// Scan actual file contents for security issues: hardcoded credentials,
11    /// SQL concatenation, unsafe blocks, etc.
12    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            // Check for hardcoded credentials
57            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            // Check for SQL concatenation
78            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            // Check for unsafe blocks (Rust-specific)
99            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}