clawft_kernel/assessment/analyzers/
data_source.rs1use std::path::{Path, PathBuf};
4
5use crate::assessment::analyzer::Analyzer;
6use crate::assessment::{Finding, analyzer::AnalysisContext};
7
8pub struct DataSourceAnalyzer;
10
11const DATA_PREFIXES: &[(&str, &str)] = &[
13 ("postgres://", "PostgreSQL"),
14 ("postgresql://", "PostgreSQL"),
15 ("mongodb://", "MongoDB"),
16 ("mongodb+srv://", "MongoDB Atlas"),
17 ("redis://", "Redis"),
18 ("rediss://", "Redis (TLS)"),
19 ("mysql://", "MySQL"),
20 ("sqlite://", "SQLite"),
21 ("s3://", "S3 bucket"),
22 ("amqp://", "RabbitMQ"),
23 ("nats://", "NATS"),
24];
25
26fn is_scannable(path: &Path) -> bool {
28 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
29 matches!(
30 ext,
31 "rs" | "ts"
32 | "tsx"
33 | "js"
34 | "jsx"
35 | "py"
36 | "go"
37 | "rb"
38 | "toml"
39 | "yaml"
40 | "yml"
41 | "json"
42 | "env"
43 | "cfg"
44 | "ini"
45 | "conf"
46 | "properties"
47 ) || path
48 .file_name()
49 .and_then(|n| n.to_str())
50 .map(|n| n.starts_with(".env"))
51 .unwrap_or(false)
52}
53
54impl Analyzer for DataSourceAnalyzer {
55 fn id(&self) -> &str {
56 "data_source"
57 }
58
59 fn name(&self) -> &str {
60 "Data Source Analyzer"
61 }
62
63 fn categories(&self) -> &[&str] {
64 &["data_source"]
65 }
66
67 fn analyze(
68 &self,
69 project: &Path,
70 files: &[PathBuf],
71 _context: &AnalysisContext,
72 ) -> Vec<Finding> {
73 let mut findings = Vec::new();
74
75 for path in files {
76 if !is_scannable(path) {
77 continue;
78 }
79
80 let rel = path.strip_prefix(project).unwrap_or(path);
81 let rel_str = rel.display().to_string();
82
83 let content = match std::fs::read_to_string(path) {
84 Ok(c) => c,
85 Err(_) => continue,
86 };
87
88 for (i, line) in content.lines().enumerate() {
89 let lower = line.to_lowercase();
90
91 for &(prefix, label) in DATA_PREFIXES {
93 if lower.contains(prefix) {
94 findings.push(Finding {
95 severity: "info".into(),
96 category: "data_source".into(),
97 file: rel_str.clone(),
98 line: Some(i + 1),
99 message: format!("{label} connection reference detected"),
100 });
101 }
102 }
103
104 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
107 if (matches!(
108 ext,
109 "toml"
110 | "yaml"
111 | "yml"
112 | "json"
113 | "env"
114 | "cfg"
115 | "ini"
116 | "conf"
117 | "properties"
118 ) || path
119 .file_name()
120 .and_then(|n| n.to_str())
121 .map(|n| n.starts_with(".env"))
122 .unwrap_or(false))
123 && (lower.contains("base_url")
124 || lower.contains("api_url")
125 || lower.contains("endpoint")
126 || lower.contains("base-url")
127 || lower.contains("api-url"))
128 && (lower.contains("http://") || lower.contains("https://"))
129 {
130 findings.push(Finding {
131 severity: "info".into(),
132 category: "data_source".into(),
133 file: rel_str.clone(),
134 line: Some(i + 1),
135 message: "HTTP API base URL reference detected".into(),
136 });
137 }
138 }
139 }
140
141 findings
142 }
143}