use std::sync::OnceLock;
use regex::RegexSet;
static SENSITIVE_PATTERNS: OnceLock<RegexSet> = OnceLock::new();
static SENSITIVE_DESCRIPTIONS: &[&str] = &[
"Git repository exposure (.git/)",
"SVN repository exposure (.svn/)",
"Environment file exposure (.env)",
"Backup file exposure (.bak/.old/.orig/.save)",
"Editor swap/backup file (.swp/.swo/~)",
"DS_Store exposure",
"Server status/info endpoint",
"PHP info/config exposure",
"WordPress sensitive file (wp-config, xmlrpc)",
"Database dump exposure (.sql)",
"Docker/CI config exposure",
"Hidden dotfile exposure",
"Admin panel probe",
"PHPMyAdmin/Adminer probe",
"Debug/actuator/trace endpoint",
"htaccess/htpasswd exposure",
"Log file exposure (.log)",
"SSH/PEM key exposure",
"CGI-bin probe",
"nginx/Apache config exposure",
];
fn patterns() -> &'static RegexSet {
SENSITIVE_PATTERNS.get_or_init(|| {
RegexSet::new([
r"(?i)/\.git(/|$)",
r"(?i)/\.svn(/|$)",
r"(?i)/\.env(\.[a-z]+)?$",
r"(?i)\.(bak|old|orig|save|backup|copy|tmp)$",
r"(?i)(\.(swp|swo)|~)$",
r"(?i)/\.DS_Store$",
r"(?i)^/(server-status|server-info|nginx_status|stub_status)(/|$)",
r"(?i)/(phpinfo|php-info|info)\.php",
r"(?i)/(wp-config\.php|xmlrpc\.php|wp-admin/install\.php)",
r"(?i)/[^/]*\.(sql|sql\.gz|sql\.bz2|sql\.zip|dump)$",
r"(?i)/(docker-compose\.(yml|yaml)|Dockerfile|\.gitlab-ci\.yml|\.github|Jenkinsfile)(/|$)",
r"(?i)/\.(aws|ssh|gnupg|npmrc|docker|kube|config)(/|$)",
r"(?i)^/(admin|administrator|manager|cpanel|plesk|webmail)(/|$)",
r"(?i)/(phpmyadmin|pma|adminer|dbadmin|myadmin|mysql-admin)(/|$)",
r"(?i)^/(actuator|debug|trace|metrics|health|_profiler)(/|$)",
r"(?i)/\.(htaccess|htpasswd|htdigest)$",
r"(?i)/[^/]*\.(log|access|error)$",
r"(?i)/[^/]*\.(pem|key|crt|cer|p12|pfx|jks)$",
r"(?i)^/cgi-bin/",
r"(?i)/(nginx\.conf|httpd\.conf|apache2?\.conf|\.nginx|sites-available|sites-enabled)(/|$)",
])
.expect("sensitive path patterns must compile")
})
}
pub fn check_sensitive_path(path: &str) -> Option<String> {
let set = patterns();
let matches: Vec<_> = set.matches(path).into_iter().collect();
if matches.is_empty() {
None
} else {
let idx = matches[0];
Some(
SENSITIVE_DESCRIPTIONS
.get(idx)
.unwrap_or(&"sensitive path access")
.to_string(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_git_directory() {
assert!(check_sensitive_path("/.git/config").is_some());
assert!(check_sensitive_path("/.git/HEAD").is_some());
assert!(check_sensitive_path("/.git/").is_some());
}
#[test]
fn detects_svn_directory() {
assert!(check_sensitive_path("/.svn/entries").is_some());
}
#[test]
fn detects_env_file() {
assert!(check_sensitive_path("/.env").is_some());
assert!(check_sensitive_path("/.env.production").is_some());
assert!(check_sensitive_path("/.env.local").is_some());
}
#[test]
fn detects_backup_files() {
assert!(check_sensitive_path("/config.php.bak").is_some());
assert!(check_sensitive_path("/database.old").is_some());
assert!(check_sensitive_path("/settings.orig").is_some());
}
#[test]
fn detects_swap_files() {
assert!(check_sensitive_path("/.config.swp").is_some());
assert!(check_sensitive_path("/config~").is_some());
}
#[test]
fn detects_ds_store() {
assert!(check_sensitive_path("/.DS_Store").is_some());
}
#[test]
fn detects_server_status() {
assert!(check_sensitive_path("/server-status").is_some());
assert!(check_sensitive_path("/server-info").is_some());
assert!(check_sensitive_path("/nginx_status").is_some());
}
#[test]
fn detects_phpinfo() {
assert!(check_sensitive_path("/phpinfo.php").is_some());
}
#[test]
fn detects_wp_config() {
assert!(check_sensitive_path("/wp-config.php").is_some());
assert!(check_sensitive_path("/xmlrpc.php").is_some());
}
#[test]
fn detects_sql_dump() {
assert!(check_sensitive_path("/db.sql").is_some());
assert!(check_sensitive_path("/backup.sql.gz").is_some());
}
#[test]
fn detects_docker_compose() {
assert!(check_sensitive_path("/docker-compose.yml").is_some());
}
#[test]
fn detects_admin_panels() {
assert!(check_sensitive_path("/admin").is_some());
assert!(check_sensitive_path("/admin/").is_some());
assert!(check_sensitive_path("/administrator/").is_some());
}
#[test]
fn detects_phpmyadmin() {
assert!(check_sensitive_path("/phpmyadmin/").is_some());
}
#[test]
fn detects_actuator() {
assert!(check_sensitive_path("/actuator/env").is_some());
assert!(check_sensitive_path("/debug/vars").is_some());
assert!(check_sensitive_path("/trace").is_some());
}
#[test]
fn detects_htaccess() {
assert!(check_sensitive_path("/.htaccess").is_some());
assert!(check_sensitive_path("/.htpasswd").is_some());
}
#[test]
fn detects_cgi_bin() {
assert!(check_sensitive_path("/cgi-bin/test-cgi").is_some());
}
#[test]
fn detects_nginx_config() {
assert!(check_sensitive_path("/nginx.conf").is_some());
}
#[test]
fn allows_normal_api_paths() {
assert!(check_sensitive_path("/api/users/123").is_none());
assert!(check_sensitive_path("/api/v2/products").is_none());
}
#[test]
fn allows_static_assets() {
assert!(check_sensitive_path("/static/css/main.css").is_none());
assert!(check_sensitive_path("/images/logo.png").is_none());
}
#[test]
fn allows_normal_pages() {
assert!(check_sensitive_path("/about").is_none());
assert!(check_sensitive_path("/contact").is_none());
assert!(check_sensitive_path("/login").is_none());
}
}