use crate::detectors::base::{is_test_file, Detector, DetectorConfig};
use crate::models::{deterministic_finding_id, Finding, Severity};
use anyhow::Result;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use tracing::{debug, info};
const PYTHON_PATTERNS: &[(&str, &str, Severity)] = &[
(
"verify=False",
"requests/urllib call with certificate verification disabled",
Severity::High,
),
(
"verify = False",
"requests/urllib call with certificate verification disabled",
Severity::High,
),
(
"CERT_NONE",
"SSL context with no certificate verification",
Severity::Critical,
),
(
"check_hostname = False",
"SSL hostname verification disabled",
Severity::High,
),
(
"check_hostname=False",
"SSL hostname verification disabled",
Severity::High,
),
(
"InsecureRequestWarning",
"urllib3 insecure request warning suppressed",
Severity::Medium,
),
(
"create_default_context",
"Custom SSL context (verify usage)",
Severity::Low,
), ];
const JS_PATTERNS: &[(&str, &str, Severity)] = &[
(
"rejectUnauthorized: false",
"TLS certificate verification disabled",
Severity::High,
),
(
"rejectUnauthorized:false",
"TLS certificate verification disabled",
Severity::High,
),
(
"rejectUnauthorized : false",
"TLS certificate verification disabled",
Severity::High,
),
(
"NODE_TLS_REJECT_UNAUTHORIZED",
"Environment variable to disable TLS verification",
Severity::Critical,
),
(
"process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'",
"TLS verification disabled via environment",
Severity::Critical,
),
(
"process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\"",
"TLS verification disabled via environment",
Severity::Critical,
),
(
"agent: new https.Agent",
"Custom HTTPS agent (check rejectUnauthorized)",
Severity::Low,
),
];
const GO_PATTERNS: &[(&str, &str, Severity)] = &[
(
"InsecureSkipVerify: true",
"TLS certificate verification skipped",
Severity::High,
),
(
"InsecureSkipVerify:true",
"TLS certificate verification skipped",
Severity::High,
),
];
const JAVA_PATTERNS: &[(&str, &str, Severity)] = &[
(
"TrustAllCerts",
"Trust-all certificate manager (no validation)",
Severity::Critical,
),
(
"X509TrustManager",
"Custom trust manager (may bypass validation)",
Severity::Medium,
),
(
"ALLOW_ALL_HOSTNAME_VERIFIER",
"Hostname verification disabled",
Severity::High,
),
(
"NoopHostnameVerifier",
"Hostname verification disabled",
Severity::High,
),
(
"setHostnameVerifier(allHostsValid)",
"Hostname verification disabled",
Severity::High,
),
(
"SSLContext.getInstance(\"SSL\")",
"Outdated SSL protocol (use TLS)",
Severity::Medium,
),
];
const RUST_PATTERNS: &[(&str, &str, Severity)] = &[
(
"danger_accept_invalid_certs(true)",
"Certificate validation disabled (reqwest)",
Severity::High,
),
(
"danger_accept_invalid_hostnames(true)",
"Hostname validation disabled (reqwest)",
Severity::High,
),
(
"set_verify(SslVerifyMode::NONE)",
"OpenSSL verification disabled",
Severity::High,
),
];
pub struct InsecureTlsDetector {
repository_path: PathBuf,
max_findings: usize,
}
impl InsecureTlsDetector {
pub fn new(repository_path: impl Into<PathBuf>) -> Self {
Self {
repository_path: repository_path.into(),
max_findings: 50,
}
}
fn get_patterns_for_ext(&self, ext: &str) -> Vec<(&'static str, &'static str, Severity)> {
match ext {
"py" | "pyi" => PYTHON_PATTERNS.to_vec(),
"js" | "jsx" | "mjs" | "cjs" | "ts" | "tsx" => JS_PATTERNS.to_vec(),
"go" => GO_PATTERNS.to_vec(),
"java" | "kt" | "kts" => JAVA_PATTERNS.to_vec(),
"rs" => RUST_PATTERNS.to_vec(),
_ => vec![],
}
}
fn scan_files(&self) -> Vec<Finding> {
use crate::detectors::walk_source_files;
let mut findings = Vec::new();
let mut seen: HashSet<(String, u32)> = HashSet::new();
if !self.repository_path.exists() {
return findings;
}
let extensions = &[
"py", "pyi", "js", "jsx", "ts", "tsx", "mjs", "cjs", "go", "java", "kt", "kts", "rs",
];
for path in walk_source_files(&self.repository_path, Some(extensions)) {
if findings.len() >= self.max_findings {
break;
}
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let patterns = self.get_patterns_for_ext(ext);
if patterns.is_empty() {
continue;
}
let rel_path = path
.strip_prefix(&self.repository_path)
.unwrap_or(&path)
.to_string_lossy()
.to_string();
let is_test = is_test_file(Path::new(&rel_path));
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
if content.len() > 500_000 {
continue;
}
let lines: Vec<&str> = content.lines().collect();
for (line_no, line) in lines.iter().enumerate() {
let line_num = (line_no + 1) as u32;
let trimmed = line.trim();
if trimmed.starts_with('#') || trimmed.starts_with("//") || trimmed.starts_with('*')
{
continue;
}
let prev_line = if line_no > 0 {
Some(lines[line_no - 1])
} else {
None
};
if crate::detectors::is_line_suppressed(line, prev_line) {
continue;
}
for (pattern, description, severity) in &patterns {
if !line.contains(pattern) {
continue;
}
if ext == "rs" && (trimmed.starts_with('"') || trimmed.starts_with("&\"") || trimmed.starts_with("r#\"") || trimmed.starts_with("r\"")) {
continue;
}
if *severity == Severity::Low {
continue; }
if *pattern == "InsecureRequestWarning"
&& !line.contains("disable")
&& !line.contains("filter")
&& !line.contains("suppress")
{
continue;
}
if *pattern == "X509TrustManager"
&& !line.contains("implements")
&& !line.contains("new")
&& !line.contains("class")
{
continue;
}
if *pattern == "create_default_context" {
continue;
}
let loc = (rel_path.clone(), line_num);
if seen.contains(&loc) {
continue;
}
seen.insert(loc);
let effective_severity = if is_test {
match severity {
Severity::Critical => Severity::Medium,
Severity::High => Severity::Low,
_ => Severity::Low,
}
} else {
*severity
};
let language = match ext {
"py" | "pyi" => "python",
"js" | "jsx" | "mjs" | "cjs" => "javascript",
"ts" | "tsx" => "typescript",
"go" => "go",
"java" => "java",
"kt" | "kts" => "kotlin",
"rs" => "rust",
_ => "unknown",
};
findings.push(Finding {
id: deterministic_finding_id(
"InsecureTlsDetector",
&rel_path,
line_num,
description,
),
detector: "InsecureTlsDetector".to_string(),
title: format!("Insecure TLS/Certificate Validation ({})", language),
description: format!(
"**{}** (CWE-295)\n\n\
Disabled certificate or hostname verification detected.\n\n\
**Pattern**: `{}`\n\
**File**: {}:{}\n\
**Code**: `{}`\n\n\
This allows man-in-the-middle attacks. An attacker on the network \
can intercept and modify traffic without detection.{}",
description,
pattern,
rel_path,
line_num,
trimmed.chars().take(120).collect::<String>(),
if is_test {
"\n\n*Note: Found in test file — severity reduced.*"
} else {
""
},
),
severity: effective_severity,
affected_files: vec![PathBuf::from(&rel_path)],
line_start: Some(line_num),
line_end: None,
suggested_fix: Some(self.get_fix_suggestion(pattern, language)),
cwe_id: Some("CWE-295".to_string()),
confidence: Some(if is_test { 0.7 } else { 0.95 }),
category: Some("security".to_string()),
..Default::default()
});
if findings.len() >= self.max_findings {
return findings;
}
}
}
}
findings
}
fn get_fix_suggestion(&self, pattern: &str, language: &str) -> String {
match language {
"python" => {
if pattern.contains("verify") {
"Remove `verify=False` or set `verify=True` (default). For self-signed certs in dev, \
use `verify='/path/to/ca-bundle.crt'` instead.".to_string()
} else if pattern.contains("CERT_NONE") {
"Use `ssl.CERT_REQUIRED` instead of `ssl.CERT_NONE`.".to_string()
} else if pattern.contains("check_hostname") {
"Set `check_hostname = True` (default in Python 3.4+).".to_string()
} else {
"Enable certificate verification. Never disable TLS validation in production."
.to_string()
}
}
"javascript" | "typescript" => {
if pattern.contains("rejectUnauthorized") {
"Remove `rejectUnauthorized: false`. For self-signed certs, provide the CA cert \
via `ca` option instead.".to_string()
} else if pattern.contains("NODE_TLS") {
"Remove `NODE_TLS_REJECT_UNAUTHORIZED=0`. This disables TLS for the entire process.".to_string()
} else {
"Enable certificate verification on HTTPS connections.".to_string()
}
}
"go" => "Remove `InsecureSkipVerify: true` from tls.Config. For self-signed certs, \
provide a custom CA pool via `RootCAs`."
.to_string(),
"java" | "kotlin" => {
"Use the default TrustManager and HostnameVerifier. For self-signed certs, \
add the CA to your trust store."
.to_string()
}
"rust" => {
"Remove `danger_accept_invalid_certs(true)`. For self-signed certs, add the CA \
to the client builder via `add_root_certificate()`."
.to_string()
}
_ => "Enable certificate verification. Never disable TLS validation in production."
.to_string(),
}
}
}
impl Detector for InsecureTlsDetector {
fn name(&self) -> &'static str {
"InsecureTlsDetector"
}
fn description(&self) -> &'static str {
"Detects disabled TLS/certificate verification (CWE-295)"
}
fn detect(&self, _graph: &dyn crate::graph::GraphQuery) -> Result<Vec<Finding>> {
debug!("Starting insecure TLS detection");
let findings = self.scan_files();
info!("InsecureTlsDetector found {} findings", findings.len());
Ok(findings)
}
fn category(&self) -> &'static str {
"security"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_python_verify_false() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("client.py");
std::fs::write(&file, "response = requests.get(url, verify=False)\n").unwrap();
let detector = InsecureTlsDetector::new(dir.path());
let findings = detector.scan_files();
assert!(!findings.is_empty(), "Should detect verify=False");
assert_eq!(findings[0].severity, Severity::High);
}
#[test]
fn test_js_reject_unauthorized() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("client.js");
std::fs::write(
&file,
"const agent = new https.Agent({ rejectUnauthorized: false });\n",
)
.unwrap();
let detector = InsecureTlsDetector::new(dir.path());
let findings = detector.scan_files();
assert!(
!findings.is_empty(),
"Should detect rejectUnauthorized: false"
);
}
#[test]
fn test_go_insecure_skip_verify() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("client.go");
std::fs::write(
&file,
"tlsConfig := &tls.Config{InsecureSkipVerify: true}\n",
)
.unwrap();
let detector = InsecureTlsDetector::new(dir.path());
let findings = detector.scan_files();
assert!(!findings.is_empty(), "Should detect InsecureSkipVerify");
}
#[test]
fn test_rust_danger_accept() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("client.rs");
std::fs::write(
&file,
"let client = reqwest::Client::builder().danger_accept_invalid_certs(true).build()?;\n",
)
.unwrap();
let detector = InsecureTlsDetector::new(dir.path());
let findings = detector.scan_files();
assert!(
!findings.is_empty(),
"Should detect danger_accept_invalid_certs"
);
}
#[test]
fn test_java_trust_all() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("Client.java");
std::fs::write(
&file,
"TrustManager[] trustAllCerts = new TrustAllCerts();\n",
)
.unwrap();
let detector = InsecureTlsDetector::new(dir.path());
let findings = detector.scan_files();
assert!(!findings.is_empty(), "Should detect TrustAllCerts");
}
#[test]
fn test_test_file_downgraded() {
let dir = tempfile::tempdir().unwrap();
let test_dir = dir.path().join("tests");
std::fs::create_dir_all(&test_dir).unwrap();
let file = test_dir.join("test_client.py");
std::fs::write(&file, "response = requests.get(url, verify=False)\n").unwrap();
let detector = InsecureTlsDetector::new(dir.path());
let findings = detector.scan_files();
assert!(!findings.is_empty(), "Should still detect in test files");
assert_eq!(
findings[0].severity,
Severity::Low,
"Should be downgraded in tests"
);
}
#[test]
fn test_clean_code_no_findings() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("client.py");
std::fs::write(
&file,
"response = requests.get(url)\nprint(response.status_code)\n",
)
.unwrap();
let detector = InsecureTlsDetector::new(dir.path());
let findings = detector.scan_files();
assert!(findings.is_empty(), "Clean code should have no findings");
}
#[test]
fn test_python_cert_none() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("server.py");
std::fs::write(&file, "ctx = ssl.create_default_context()\nctx.check_hostname = False\nctx.verify_mode = ssl.CERT_NONE\n").unwrap();
let detector = InsecureTlsDetector::new(dir.path());
let findings = detector.scan_files();
assert!(
findings.len() >= 2,
"Should detect both check_hostname and CERT_NONE. Found: {}",
findings.len()
);
}
}