pathbuster 0.5.6

A path-normalization pentesting tool.
Documentation
use std::collections::{HashMap, HashSet};

use tokio::sync::mpsc;

#[test]
fn parse_threshold_range_ok() {
    let t = crate::utils::parse_sift3_threshold_range("5-1000").unwrap();
    assert_eq!(t.start, 5.0);
    assert_eq!(t.end, 1000.0);
}

#[test]
fn parse_threshold_range_rejects_invalid() {
    assert!(crate::utils::parse_sift3_threshold_range("500").is_err());
    assert!(crate::utils::parse_sift3_threshold_range("500-").is_err());
    assert!(crate::utils::parse_sift3_threshold_range("-500").is_err());
    assert!(crate::utils::parse_sift3_threshold_range("500-100").is_err());
}

#[test]
fn detect_cloudflare_from_headers() {
    let wafs = crate::fingerprint::detect_waf_for_tests(
        403,
        HashMap::from([
            ("server".to_string(), "cloudflare".to_string()),
            ("cf-ray".to_string(), "123".to_string()),
        ]),
        "Attention Required! | Cloudflare".to_string(),
        None,
    );
    assert!(wafs
        .iter()
        .any(|w| w.name == "Cloudflare" && w.confidence > 0.5));
}

#[test]
fn urlencode_encodes_dots_and_slashes() {
    let payloads = crate::transform::generate_payloads("../..//etc/passwd", &[], 1, &[], false);
    assert!(payloads.iter().any(|p| {
        p.family == "urlencode" && p.mutated.contains("%2e") && p.mutated.contains("%2f")
    }));
}

#[test]
fn generator_deduplicates() {
    let payloads = crate::transform::generate_payloads(
        "../../../etc/passwd",
        &["ModSecurity".to_string()],
        2,
        &[],
        false,
    );
    let uniques: HashSet<_> = payloads.iter().map(|p| p.mutated.clone()).collect();
    assert_eq!(uniques.len(), payloads.len());
}

#[test]
fn minimal_urlencode_keeps_letters() {
    let payloads = crate::transform::generate_payloads("../etc/passwd", &[], 1, &[], false);
    let has_min = payloads.iter().any(|p| {
        p.family == "urlencode_min"
            && p.mutated.contains("etc")
            && (p.mutated.contains("%2e") || p.mutated.contains("%2E"))
    });
    assert!(has_min);
}

#[test]
fn bypass_level_2_adds_overlong_utf8() {
    let payloads = crate::transform::generate_payloads("../etc/passwd", &[], 2, &[], false);
    assert!(payloads
        .iter()
        .any(|p| p.family == "overlong_utf8" && p.mutated.contains("%c0%ae")));
}

#[test]
fn bypass_level_2_adds_unicode_u_encoding() {
    let payloads = crate::transform::generate_payloads("../etc/passwd", &[], 2, &[], false);
    assert!(payloads
        .iter()
        .any(|p| p.family == "unicode_u" && p.mutated.contains("%u002e")));
}

#[test]
fn bypass_level_3_adds_multi_layer_encoding() {
    let payloads = crate::transform::generate_payloads("../etc/passwd", &[], 3, &[], false);
    assert!(payloads
        .iter()
        .any(|p| p.family == "multi_layer_encoding" && p.mutated.contains("%252f")));
}

#[test]
fn bypass_level_3_adds_advanced_null_byte() {
    let payloads = crate::transform::generate_payloads("../etc/passwd", &[], 3, &[], false);
    assert!(payloads
        .iter()
        .any(|p| p.family == "advanced_null_byte" && p.mutated.contains("%00")));
}

#[test]
fn bypass_level_3_adds_path_normalization() {
    let payloads = crate::transform::generate_payloads("../etc/passwd", &[], 3, &[], false);
    assert!(payloads
        .iter()
        .any(|p| p.family == "path_normalization" && p.mutated.contains("%2e%2e%2f")));
}

#[test]
fn bypass_level_3_adds_mixed_slash_techniques() {
    let payloads = crate::transform::generate_payloads("../etc/passwd", &[], 3, &[], false);
    assert!(payloads.iter().any(|p| p.family == "mixed_slash"
        && (p.mutated.contains("%2f%5c") || p.mutated.contains("/\\"))));
}

#[test]
fn bypass_level_3_adds_protocol_relative() {
    let payloads =
        crate::transform::generate_payloads("http://example.com/../etc/passwd", &[], 3, &[], false);
    assert!(payloads
        .iter()
        .any(|p| p.family == "protocol_relative" && p.mutated.starts_with("//")));
}

#[test]
fn bypass_level_3_adds_rfc3986_edge_cases() {
    let payloads = crate::transform::generate_payloads("../etc/passwd", &[], 3, &[], false);
    assert!(payloads
        .iter()
        .any(|p| p.family == "rfc3986_edge_cases" && p.mutated.contains("%252f")));
}

#[test]
fn bypass_level_3_generates_more_payloads_than_level_2() {
    let payloads_level_2 = crate::transform::generate_payloads("../etc/passwd", &[], 2, &[], false);
    let payloads_level_3 = crate::transform::generate_payloads("../etc/passwd", &[], 3, &[], false);
    assert!(payloads_level_3.len() > payloads_level_2.len());
}

#[test]
fn wordlist_smart_break_splits_common_styles() {
    assert_eq!(crate::utils::smart_break("adminNew"), vec!["admin", "New"]);
    assert_eq!(crate::utils::smart_break("admin_new"), vec!["admin", "new"]);
    assert_eq!(crate::utils::smart_break("admin-old"), vec!["admin", "old"]);
}

#[test]
fn wordlist_smart_join_parsing_and_apply() {
    let spec = crate::utils::parse_smart_join_spec("c:_").unwrap();
    let cfg = crate::utils::WordlistManipulation {
        smart_join: Some(spec),
        ..Default::default()
    };
    let out = crate::utils::apply_wordlist_manipulations(vec!["admin-old".to_string()], &cfg);
    assert_eq!(out, vec!["admin_Old"]);
}

#[tokio::test]
async fn path_parameter_produces_single_wordlist_entry() {
    let fingerprints: HashMap<String, crate::fingerprint::TargetFingerprint> = HashMap::new();
    let cfg = crate::runner::WordlistLoadConfig {
        path: Some("admin"),
        wordlist_dir: None,
        tech_override: None,
        manipulation: &crate::utils::WordlistManipulation::default(),
        extensions: &[],
        dirsearch_compat: false,
        skip_brute: false,
        skip_validation: false,
    };
    let (wordlist, loaded) = crate::runner::load_wordlist(None, &fingerprints, cfg)
        .await
        .unwrap();
    assert_eq!(wordlist, vec!["admin".to_string()]);
    assert!(loaded.is_empty());
}

#[tokio::test]
async fn bruteforce_queue_deduplicates_discoveries_and_enqueues_words() {
    let (job_tx, mut job_rx) = mpsc::channel::<crate::bruteforcer::BruteJob>(16);
    let (disc_tx, disc_rx) = mpsc::channel::<String>(16);

    disc_tx
        .send("http://example.com/".to_string())
        .await
        .unwrap();
    disc_tx
        .send("http://example.com/".to_string())
        .await
        .unwrap();
    drop(disc_tx);

    let wordlists = vec!["admin".to_string(), "login".to_string()];
    let sender = crate::bruteforcer::send_word_to_url_queue(job_tx, disc_rx, wordlists, 1000);

    let mut jobs = Vec::new();
    tokio::select! {
        _ = sender => {
            while let Some(job) = job_rx.recv().await {
                jobs.push(job);
            }
        }
    }

    let urls: Vec<_> = jobs.iter().map(|j| j.url.clone().unwrap()).collect();
    let words: Vec<_> = jobs.iter().map(|j| j.word.clone().unwrap()).collect();

    assert_eq!(urls, vec!["http://example.com/".to_string(); 2]);
    assert!(words.contains(&"admin".to_string()));
    assert!(words.contains(&"login".to_string()));
}