repotoire 0.9.0

Graph-powered code analysis CLI. 110 detectors for security, architecture, bus factor, and code quality.
Documentation
use super::*;

// Invariant: TaintPath must expose the sink callee text and the full list of
// sanitizers seen on the path so downstream detectors can act on both without
// re-parsing the call chain.
#[test]
fn test_taint_path_exposes_sink_callee_and_sanitizers() {
    let code = r#"
const cmd = req.body.x;
child_process.exec(cmd);
"#;

    let analyzer = TaintAnalyzer::new();
    let paths = analyzer.analyze_intra_function(
        code,
        "handler",
        "routes.js",
        1,
        crate::parsers::lightweight::Language::JavaScript,
        TaintCategory::CommandInjection,
    );

    assert!(
        !paths.is_empty(),
        "Should detect taint flow from req.body.x to child_process.exec"
    );

    let path = &paths[0];

    // sink_callee_text must be populated — non-empty and identify the sink
    assert!(
        !path.sink_callee_text.is_empty(),
        "sink_callee_text must be non-empty; got empty string"
    );
    assert!(
        path.sink_callee_text.contains("exec") || path.sink_callee_text.contains("child_process"),
        "sink_callee_text should identify the exec sink, got: {:?}",
        path.sink_callee_text
    );

    // No sanitizer between source and sink → sanitizers_on_path must be empty
    assert!(
        path.sanitizers_on_path.is_empty(),
        "No sanitizer applied — sanitizers_on_path should be empty, got: {:?}",
        path.sanitizers_on_path
    );

    // Regression: is_sanitized must also be false
    assert!(!path.is_sanitized, "Path should not be marked as sanitized");
}

#[test]
fn test_taint_category_cwe() {
    assert_eq!(TaintCategory::SqlInjection.cwe_id(), "CWE-89");
    assert_eq!(TaintCategory::CommandInjection.cwe_id(), "CWE-78");
    assert_eq!(TaintCategory::Xss.cwe_id(), "CWE-79");
    assert_eq!(TaintCategory::Xxe.cwe_id(), "CWE-611");
}

#[test]
fn test_xxe_sinks_registered() {
    // The XXE category must register XML-specific sinks (etree.parse,
    // lxml.etree.parse, ...) and NOT borrow path-traversal sinks
    // (os.path.join, fs.readFile). This is the architectural invariant
    // that fixes the Click utils.py:490 false positive.
    let analyzer = TaintAnalyzer::new();
    let xxe_sinks = analyzer
        .get_sinks(TaintCategory::Xxe)
        .expect("Xxe category should have sinks registered");
    assert!(!xxe_sinks.is_empty(), "Xxe should have at least one sink");
    assert!(
        xxe_sinks.contains("lxml.etree.parse"),
        "Xxe sinks must include lxml.etree.parse, got: {:?}",
        xxe_sinks
    );
    assert!(
        xxe_sinks.contains("DocumentBuilder"),
        "Xxe sinks must include DocumentBuilder (Java), got: {:?}",
        xxe_sinks
    );
    // Negative: must NOT borrow path-traversal sinks.
    assert!(
        !xxe_sinks.contains("os.path.join"),
        "Xxe sinks must NOT include os.path.join (that's path traversal)"
    );
    assert!(
        !xxe_sinks.contains("path.join"),
        "Xxe sinks must NOT include path.join (that's path traversal)"
    );
}

#[test]
fn test_is_source() {
    let analyzer = TaintAnalyzer::new();

    assert!(analyzer.is_source("req.body", TaintCategory::SqlInjection));
    assert!(analyzer.is_source("request.form", TaintCategory::SqlInjection));
    assert!(analyzer.is_source("c.Param", TaintCategory::SqlInjection));
    assert!(!analyzer.is_source("random_function", TaintCategory::SqlInjection));
}

#[test]
fn test_is_sink() {
    let analyzer = TaintAnalyzer::new();

    assert!(analyzer.is_sink("cursor.execute", TaintCategory::SqlInjection));
    assert!(analyzer.is_sink("db.query", TaintCategory::SqlInjection));
    assert!(analyzer.is_sink("os.system", TaintCategory::CommandInjection));
    assert!(analyzer.is_sink("innerHTML", TaintCategory::Xss));
    assert!(!analyzer.is_sink("print", TaintCategory::SqlInjection));
}

#[test]
fn test_is_sanitizer() {
    let analyzer = TaintAnalyzer::new();

    assert!(analyzer.is_sanitizer("escapeHtml", TaintCategory::Xss));
    assert!(analyzer.is_sanitizer("shlex.quote", TaintCategory::CommandInjection));
    assert!(analyzer.is_sanitizer("validate_input", TaintCategory::SqlInjection)); // generic
    assert!(analyzer.is_sanitizer("sanitize_data", TaintCategory::Xss)); // generic
}

#[test]
fn test_taint_path_is_vulnerable() {
    let vulnerable_path = TaintPath {
        source_function: "handler".to_string(),
        source_file: "app.py".to_string(),
        source_line: 10,
        sink_function: "execute".to_string(),
        sink_file: "db.py".to_string(),
        sink_line: 20,
        category: TaintCategory::SqlInjection,
        call_chain: vec![],
        is_sanitized: false,
        sanitizer: None,
        confidence: 0.8,
        sink_callee_text: "cursor.execute".to_string(),
        sanitizers_on_path: vec![],
    };

    let safe_path = TaintPath {
        is_sanitized: true,
        sanitizer: Some("escape".to_string()),
        sanitizers_on_path: vec!["escape".to_string()],
        ..vulnerable_path.clone()
    };

    assert!(vulnerable_path.is_vulnerable());
    assert!(!safe_path.is_vulnerable());
}

#[test]
fn test_taint_path_string() {
    let path = TaintPath {
        source_function: "handler".to_string(),
        source_file: "app.py".to_string(),
        source_line: 10,
        sink_function: "execute".to_string(),
        sink_file: "db.py".to_string(),
        sink_line: 20,
        category: TaintCategory::SqlInjection,
        call_chain: vec!["process".to_string(), "query".to_string()],
        is_sanitized: false,
        sanitizer: None,
        confidence: 0.8,
        sink_callee_text: "cursor.execute".to_string(),
        sanitizers_on_path: vec![],
    };

    assert_eq!(path.path_string(), "handler → process → query → execute");
}

#[test]
fn test_analysis_result() {
    let paths = vec![
        TaintPath {
            source_function: "a".to_string(),
            source_file: "a.py".to_string(),
            source_line: 1,
            sink_function: "b".to_string(),
            sink_file: "b.py".to_string(),
            sink_line: 2,
            category: TaintCategory::SqlInjection,
            call_chain: vec![],
            is_sanitized: false,
            sanitizer: None,
            confidence: 0.8,
            sink_callee_text: "b".to_string(),
            sanitizers_on_path: vec![],
        },
        TaintPath {
            source_function: "c".to_string(),
            source_file: "c.py".to_string(),
            source_line: 3,
            sink_function: "d".to_string(),
            sink_file: "d.py".to_string(),
            sink_line: 4,
            category: TaintCategory::SqlInjection,
            call_chain: vec![],
            is_sanitized: true,
            sanitizer: Some("escape".to_string()),
            confidence: 0.8,
            sink_callee_text: "d".to_string(),
            sanitizers_on_path: vec!["escape".to_string()],
        },
    ];

    let result = TaintAnalysisResult::from_paths(paths);

    assert_eq!(result.vulnerable_count, 1);
    assert_eq!(result.sanitized_count, 1);
    assert!(result.has_vulnerabilities());
    assert_eq!(result.vulnerable_paths().len(), 1);
}