use std::path::{Path, PathBuf};
use crate::assessment::{analyzer::AnalysisContext, Finding};
use crate::assessment::analyzer::Analyzer;
pub struct DataSourceAnalyzer;
const DATA_PREFIXES: &[(&str, &str)] = &[
("postgres://", "PostgreSQL"),
("postgresql://", "PostgreSQL"),
("mongodb://", "MongoDB"),
("mongodb+srv://", "MongoDB Atlas"),
("redis://", "Redis"),
("rediss://", "Redis (TLS)"),
("mysql://", "MySQL"),
("sqlite://", "SQLite"),
("s3://", "S3 bucket"),
("amqp://", "RabbitMQ"),
("nats://", "NATS"),
];
fn is_scannable(path: &Path) -> bool {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
matches!(
ext,
"rs" | "ts"
| "tsx"
| "js"
| "jsx"
| "py"
| "go"
| "rb"
| "toml"
| "yaml"
| "yml"
| "json"
| "env"
| "cfg"
| "ini"
| "conf"
| "properties"
) || path
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with(".env"))
.unwrap_or(false)
}
impl Analyzer for DataSourceAnalyzer {
fn id(&self) -> &str {
"data_source"
}
fn name(&self) -> &str {
"Data Source Analyzer"
}
fn categories(&self) -> &[&str] {
&["data_source"]
}
fn analyze(&self, project: &Path, files: &[PathBuf], _context: &AnalysisContext) -> Vec<Finding> {
let mut findings = Vec::new();
for path in files {
if !is_scannable(path) {
continue;
}
let rel = path.strip_prefix(project).unwrap_or(path);
let rel_str = rel.display().to_string();
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => continue,
};
for (i, line) in content.lines().enumerate() {
let lower = line.to_lowercase();
for &(prefix, label) in DATA_PREFIXES {
if lower.contains(prefix) {
findings.push(Finding {
severity: "info".into(),
category: "data_source".into(),
file: rel_str.clone(),
line: Some(i + 1),
message: format!("{label} connection reference detected"),
});
}
}
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if matches!(
ext,
"toml" | "yaml" | "yml" | "json" | "env" | "cfg" | "ini" | "conf" | "properties"
) || path
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with(".env"))
.unwrap_or(false)
{
if (lower.contains("base_url")
|| lower.contains("api_url")
|| lower.contains("endpoint")
|| lower.contains("base-url")
|| lower.contains("api-url"))
&& (lower.contains("http://") || lower.contains("https://"))
{
findings.push(Finding {
severity: "info".into(),
category: "data_source".into(),
file: rel_str.clone(),
line: Some(i + 1),
message: "HTTP API base URL reference detected".into(),
});
}
}
}
}
findings
}
}