clawft_kernel/assessment/analyzers/
security.rs1use std::path::{Path, PathBuf};
4
5use crate::assessment::analyzer::Analyzer;
6use crate::assessment::{Finding, analyzer::AnalysisContext};
7
8pub struct SecurityAnalyzer;
10
11const SECRET_PATTERNS: &[&str] = &[
13 "api_key=",
14 "api_key =",
15 "apikey=",
16 "apikey =",
17 "password=",
18 "password =",
19 "passwd=",
20 "passwd =",
21 "secret=",
22 "secret =",
23 "token=",
24 "token =",
25 "aws_secret",
26 "private_key",
27];
28
29impl Analyzer for SecurityAnalyzer {
30 fn id(&self) -> &str {
31 "security"
32 }
33
34 fn name(&self) -> &str {
35 "Security Analyzer"
36 }
37
38 fn categories(&self) -> &[&str] {
39 &["security"]
40 }
41
42 fn analyze(
43 &self,
44 project: &Path,
45 files: &[PathBuf],
46 _context: &AnalysisContext,
47 ) -> Vec<Finding> {
48 let mut findings = Vec::new();
49
50 for path in files {
51 let rel = path.strip_prefix(project).unwrap_or(path);
52 let rel_str = rel.display().to_string();
53 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
54
55 if name == ".env" || name.starts_with(".env.") {
57 findings.push(Finding {
58 severity: "error".into(),
59 category: "security".into(),
60 file: rel_str.clone(),
61 line: None,
62 message: "Environment file should not be committed to version control".into(),
63 });
64 continue;
65 }
66
67 let is_test = rel_str.contains("test")
69 || rel_str.contains("spec")
70 || rel_str.contains("fixture")
71 || rel_str.contains("mock");
72
73 let content = match std::fs::read_to_string(path) {
74 Ok(c) => c,
75 Err(_) => continue,
76 };
77
78 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
79
80 if !is_test {
82 for (i, line) in content.lines().enumerate() {
83 let lower = line.to_lowercase();
84 let trimmed = lower.trim();
86 if trimmed.starts_with("//")
87 || trimmed.starts_with('#')
88 || trimmed.starts_with("/*")
89 || trimmed.starts_with('*')
90 {
91 continue;
92 }
93
94 for pattern in SECRET_PATTERNS {
95 if lower.contains(pattern) {
96 if let Some(pos) = lower.find('=') {
98 let after = lower[pos + 1..].trim();
99 if !after.is_empty()
101 && after != "\"\""
102 && after != "''"
103 && !after.starts_with("env")
104 && !after.starts_with("std::env")
105 && !after.starts_with("process.env")
106 && !after.starts_with("os.environ")
107 {
108 findings.push(Finding {
109 severity: "error".into(),
110 category: "security".into(),
111 file: rel_str.clone(),
112 line: Some(i + 1),
113 message: format!(
114 "Possible hardcoded secret (matched '{pattern}')"
115 ),
116 });
117 break; }
119 }
120 }
121 }
122 }
123 }
124
125 if ext == "rs" {
127 for (i, line) in content.lines().enumerate() {
128 let trimmed = line.trim();
129 if trimmed.contains("unsafe {")
130 || trimmed.contains("unsafe{")
131 || trimmed == "unsafe {"
132 {
133 findings.push(Finding {
134 severity: "warning".into(),
135 category: "security".into(),
136 file: rel_str.clone(),
137 line: Some(i + 1),
138 message: "Unsafe block detected — review for memory safety".into(),
139 });
140 }
141 }
142 }
143 }
144
145 findings
146 }
147}