Skip to main content

clawft_kernel/assessment/analyzers/
data_source.rs

1//! Data-source analyzer — detects database URIs, S3 buckets, and API base URLs.
2
3use std::path::{Path, PathBuf};
4
5use crate::assessment::analyzer::Analyzer;
6use crate::assessment::{Finding, analyzer::AnalysisContext};
7
8/// Analyzer that greps for connection strings and external service references.
9pub struct DataSourceAnalyzer;
10
11/// URI scheme prefixes that indicate database or storage connections.
12const 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
26/// File extensions we consider as config/source that might contain URLs.
27fn 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                // Check data source prefixes
92                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                // Detect HTTP API base URLs in config-like files
105                // Look for patterns like BASE_URL, API_URL, ENDPOINT assigned to http(s)://
106                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}