infigraph-core 1.5.0

AST-powered code analysis framework — parser, graph, diff, and analysis engine
Documentation
mod detect;
mod format;
mod rules;

pub use detect::*;
pub use format::*;
pub use rules::*;

#[cfg(test)]
mod tests {
    use super::detect::scan_file;
    use super::*;
    use std::io::Write;

    fn scan_str(content: &str, ext: &str) -> Vec<Finding> {
        let dir = tempfile::tempdir().unwrap();
        let file = dir.path().join(format!("test.{}", ext));
        let mut f = std::fs::File::create(&file).unwrap();
        f.write_all(content.as_bytes()).unwrap();
        let mut stats = ScanStats::default();
        scan_file(&file, &format!("test.{}", ext), ext, &mut stats).unwrap();
        stats.findings
    }

    #[test]
    fn detects_pickle_loads() {
        let findings = scan_str("data = pickle.loads(user_input)", "py");
        assert!(findings.iter().any(|f| f.rule_id == "SEC030"));
    }

    #[test]
    fn detects_hardcoded_password() {
        let findings = scan_str("password = \"s3cr3t\"", "py");
        assert!(findings.iter().any(|f| f.rule_id == "SEC010"));
    }

    #[test]
    fn detects_eval_js() {
        let findings = scan_str("eval(userInput)", "js");
        assert!(findings.iter().any(|f| f.rule_id == "SEC021"));
    }

    #[test]
    fn detects_md5() {
        let findings = scan_str("digest = md5(password)", "py");
        assert!(findings.iter().any(|f| f.category == Category::WeakCrypto));
    }

    #[test]
    fn detects_innerhtml() {
        let findings = scan_str("el.innerHTML = userInput", "js");
        assert!(findings.iter().any(|f| f.rule_id == "SEC100"));
    }

    #[test]
    fn no_false_positive_yaml_safe() {
        let findings = scan_str("data = yaml.load(f, loader=yaml.SafeLoader)", "py");
        assert!(!findings.iter().any(|f| f.rule_id == "SEC032"));
    }

    #[test]
    fn sanitizer_suppresses_sql_injection() {
        let code = "query = sanitize_sql(user_input)\ncursor.execute(query)";
        let findings = scan_str(code, "py");
        let sql_findings: Vec<_> = findings
            .iter()
            .filter(|f| f.category == Category::SqlInjection)
            .collect();
        assert!(!sql_findings.is_empty(), "should still detect execute()");
        assert!(
            sql_findings.iter().all(|f| f.suppressed),
            "should be suppressed by sanitize_sql"
        );
        assert!(sql_findings[0].sanitizer_hint.as_deref() == Some("sanitize_sql"));
    }

    #[test]
    fn sanitizer_suppresses_xss_dompurify() {
        let code = "const clean = DOMPurify.sanitize(content);\nel.innerHTML = clean;";
        let findings = scan_str(code, "js");
        let xss: Vec<_> = findings
            .iter()
            .filter(|f| f.category == Category::XssRisk)
            .collect();
        assert!(!xss.is_empty());
        assert!(
            xss.iter().all(|f| f.suppressed),
            "innerHTML near DOMPurify should be suppressed"
        );
    }

    #[test]
    fn no_suppression_without_sanitizer() {
        let code = "cursor.execute(\"SELECT * FROM users WHERE name = \" + user_input)";
        let findings = scan_str(code, "py");
        let sql: Vec<_> = findings
            .iter()
            .filter(|f| f.category == Category::SqlInjection)
            .collect();
        assert!(!sql.is_empty());
        assert!(
            sql.iter().all(|f| !f.suppressed),
            "no sanitizer = not suppressed"
        );
    }

    #[test]
    fn sanitizer_suppresses_command_injection() {
        let code = "safe_arg = shlex.quote(user_input)\nos.system(safe_arg)";
        let findings = scan_str(code, "py");
        let cmd: Vec<_> = findings
            .iter()
            .filter(|f| f.category == Category::CommandInjection)
            .collect();
        assert!(!cmd.is_empty());
        assert!(
            cmd.iter().all(|f| f.suppressed),
            "shlex.quote nearby should suppress"
        );
    }

    #[test]
    fn sanitizer_suppresses_path_traversal() {
        let code = "safe = os.path.realpath(user_path)\nopen(safe)";
        let findings = scan_str(code, "py");
        let path_findings: Vec<_> = findings
            .iter()
            .filter(|f| f.category == Category::PathTraversal)
            .collect();
        for f in &path_findings {
            assert!(
                f.suppressed,
                "realpath nearby should suppress path traversal"
            );
        }
    }
}