infigraph_core/security/
mod.rs1mod detect;
2mod format;
3mod rules;
4
5pub use detect::*;
6pub use format::*;
7pub use rules::*;
8
9#[cfg(test)]
10mod tests {
11 use super::detect::scan_file;
12 use super::*;
13 use std::io::Write;
14
15 fn scan_str(content: &str, ext: &str) -> Vec<Finding> {
16 let dir = tempfile::tempdir().unwrap();
17 let file = dir.path().join(format!("test.{}", ext));
18 let mut f = std::fs::File::create(&file).unwrap();
19 f.write_all(content.as_bytes()).unwrap();
20 let mut stats = ScanStats::default();
21 scan_file(&file, &format!("test.{}", ext), ext, &mut stats).unwrap();
22 stats.findings
23 }
24
25 #[test]
26 fn detects_pickle_loads() {
27 let findings = scan_str("data = pickle.loads(user_input)", "py");
28 assert!(findings.iter().any(|f| f.rule_id == "SEC030"));
29 }
30
31 #[test]
32 fn detects_hardcoded_password() {
33 let findings = scan_str("password = \"s3cr3t\"", "py");
34 assert!(findings.iter().any(|f| f.rule_id == "SEC010"));
35 }
36
37 #[test]
38 fn detects_eval_js() {
39 let findings = scan_str("eval(userInput)", "js");
40 assert!(findings.iter().any(|f| f.rule_id == "SEC021"));
41 }
42
43 #[test]
44 fn detects_md5() {
45 let findings = scan_str("digest = md5(password)", "py");
46 assert!(findings.iter().any(|f| f.category == Category::WeakCrypto));
47 }
48
49 #[test]
50 fn detects_innerhtml() {
51 let findings = scan_str("el.innerHTML = userInput", "js");
52 assert!(findings.iter().any(|f| f.rule_id == "SEC100"));
53 }
54
55 #[test]
56 fn no_false_positive_yaml_safe() {
57 let findings = scan_str("data = yaml.load(f, loader=yaml.SafeLoader)", "py");
58 assert!(!findings.iter().any(|f| f.rule_id == "SEC032"));
59 }
60
61 #[test]
62 fn sanitizer_suppresses_sql_injection() {
63 let code = "query = sanitize_sql(user_input)\ncursor.execute(query)";
64 let findings = scan_str(code, "py");
65 let sql_findings: Vec<_> = findings
66 .iter()
67 .filter(|f| f.category == Category::SqlInjection)
68 .collect();
69 assert!(!sql_findings.is_empty(), "should still detect execute()");
70 assert!(
71 sql_findings.iter().all(|f| f.suppressed),
72 "should be suppressed by sanitize_sql"
73 );
74 assert!(sql_findings[0].sanitizer_hint.as_deref() == Some("sanitize_sql"));
75 }
76
77 #[test]
78 fn sanitizer_suppresses_xss_dompurify() {
79 let code = "const clean = DOMPurify.sanitize(content);\nel.innerHTML = clean;";
80 let findings = scan_str(code, "js");
81 let xss: Vec<_> = findings
82 .iter()
83 .filter(|f| f.category == Category::XssRisk)
84 .collect();
85 assert!(!xss.is_empty());
86 assert!(
87 xss.iter().all(|f| f.suppressed),
88 "innerHTML near DOMPurify should be suppressed"
89 );
90 }
91
92 #[test]
93 fn no_suppression_without_sanitizer() {
94 let code = "cursor.execute(\"SELECT * FROM users WHERE name = \" + user_input)";
95 let findings = scan_str(code, "py");
96 let sql: Vec<_> = findings
97 .iter()
98 .filter(|f| f.category == Category::SqlInjection)
99 .collect();
100 assert!(!sql.is_empty());
101 assert!(
102 sql.iter().all(|f| !f.suppressed),
103 "no sanitizer = not suppressed"
104 );
105 }
106
107 #[test]
108 fn sanitizer_suppresses_command_injection() {
109 let code = "safe_arg = shlex.quote(user_input)\nos.system(safe_arg)";
110 let findings = scan_str(code, "py");
111 let cmd: Vec<_> = findings
112 .iter()
113 .filter(|f| f.category == Category::CommandInjection)
114 .collect();
115 assert!(!cmd.is_empty());
116 assert!(
117 cmd.iter().all(|f| f.suppressed),
118 "shlex.quote nearby should suppress"
119 );
120 }
121
122 #[test]
123 fn sanitizer_suppresses_path_traversal() {
124 let code = "safe = os.path.realpath(user_path)\nopen(safe)";
125 let findings = scan_str(code, "py");
126 let path_findings: Vec<_> = findings
127 .iter()
128 .filter(|f| f.category == Category::PathTraversal)
129 .collect();
130 for f in &path_findings {
131 assert!(
132 f.suppressed,
133 "realpath nearby should suppress path traversal"
134 );
135 }
136 }
137}