bext-waf 0.2.0

Web Application Firewall for bext — rate limiting, IP filtering, GeoIP, rule engine
Documentation
//! Sensitive path detection — blocks probes for known-dangerous files
//! and admin endpoints that attackers routinely scan for on nginx/Apache servers.

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([
            // 0: .git/ directory
            r"(?i)/\.git(/|$)",
            // 1: .svn/ directory
            r"(?i)/\.svn(/|$)",
            // 2: .env file (root or nested)
            r"(?i)/\.env(\.[a-z]+)?$",
            // 3: Backup file extensions
            r"(?i)\.(bak|old|orig|save|backup|copy|tmp)$",
            // 4: Editor swap/backup files
            r"(?i)(\.(swp|swo)|~)$",
            // 5: .DS_Store
            r"(?i)/\.DS_Store$",
            // 6: Server status/info
            r"(?i)^/(server-status|server-info|nginx_status|stub_status)(/|$)",
            // 7: PHP info/config
            r"(?i)/(phpinfo|php-info|info)\.php",
            // 8: WordPress sensitive files
            r"(?i)/(wp-config\.php|xmlrpc\.php|wp-admin/install\.php)",
            // 9: SQL dumps
            r"(?i)/[^/]*\.(sql|sql\.gz|sql\.bz2|sql\.zip|dump)$",
            // 10: Docker/CI config
            r"(?i)/(docker-compose\.(yml|yaml)|Dockerfile|\.gitlab-ci\.yml|\.github|Jenkinsfile)(/|$)",
            // 11: Hidden dotfiles (generic, after specific patterns above)
            r"(?i)/\.(aws|ssh|gnupg|npmrc|docker|kube|config)(/|$)",
            // 12: Admin panels (common paths)
            r"(?i)^/(admin|administrator|manager|cpanel|plesk|webmail)(/|$)",
            // 13: PHPMyAdmin / Adminer
            r"(?i)/(phpmyadmin|pma|adminer|dbadmin|myadmin|mysql-admin)(/|$)",
            // 14: Debug/actuator/trace endpoints
            r"(?i)^/(actuator|debug|trace|metrics|health|_profiler)(/|$)",
            // 15: htaccess/htpasswd
            r"(?i)/\.(htaccess|htpasswd|htdigest)$",
            // 16: Log files
            r"(?i)/[^/]*\.(log|access|error)$",
            // 17: SSH/PEM key files
            r"(?i)/[^/]*\.(pem|key|crt|cer|p12|pfx|jks)$",
            // 18: CGI-bin
            r"(?i)^/cgi-bin/",
            // 19: nginx/Apache config files
            r"(?i)/(nginx\.conf|httpd\.conf|apache2?\.conf|\.nginx|sites-available|sites-enabled)(/|$)",
        ])
        .expect("sensitive path patterns must compile")
    })
}

/// Check a path for sensitive file/endpoint access.
/// Returns `Some(description)` if a pattern matches.
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::*;

    // ---- Positive detections ----

    #[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());
    }

    // ---- False positives ----

    #[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());
    }
}