use crate::attacks::database;
use crate::klr::ast::KlrRule;
use crate::results::RuleFinding;
use crate::scanner::FileData;
pub fn run_rules(rules: &[KlrRule], files: &[FileData]) -> Vec<RuleFinding> {
let mut findings = Vec::new();
for rule in rules {
if rule.contains.is_empty() && rule.reaches.is_none() {
continue;
}
for file in files {
for (line_no, line) in file.numbered_lines() {
if is_comment_only(line) {
continue;
}
if line_matches(rule, line) {
findings.push(RuleFinding {
rule: rule.name.clone(),
severity: rule.severity.label().to_string(),
file: file.path.clone(),
line: line_no,
message: rule.report.clone().unwrap_or_else(|| rule.name.clone()),
});
}
}
}
}
findings
}
fn is_comment_only(line: &str) -> bool {
let t = line.trim_start();
t.starts_with('#') || t.starts_with("//") || t.starts_with('*') || t.starts_with("/*")
}
fn line_matches(rule: &KlrRule, line: &str) -> bool {
let lower = line.to_ascii_lowercase();
for needle in &rule.contains {
if !lower.contains(&needle.to_ascii_lowercase()) {
return false;
}
}
if let Some(sink) = &rule.reaches {
if !lower.contains(&sink.to_ascii_lowercase()) {
return false;
}
if !database::line_reaches_sink(line) {
return false;
}
}
if !rule.without.is_empty() && database::line_is_sanitized(line) {
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::klr::parser::parse;
use crate::scanner::Language;
fn file(content: &str) -> FileData {
FileData {
path: "db.py".into(),
content: content.into(),
lines: content.lines().count(),
language: Language::Python,
}
}
fn unsafe_rule() -> Vec<KlrRule> {
let src = r#"
rule "unsafe database query"
when function contains "query"
and input reaches query
without sanitization
severity high
report: "User input reaches database directly"
"#;
parse(src).unwrap().rules
}
#[test]
fn flags_unsanitized_query_with_input() {
let rules = unsafe_rule();
let f = file("cursor.query(\"SELECT * FROM u WHERE id=\" + request.params['id'])\n");
let findings = run_rules(&rules, std::slice::from_ref(&f));
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].message, "User input reaches database directly");
}
#[test]
fn ignores_sanitized_query() {
let rules = unsafe_rule();
let f = file("cursor.query(\"SELECT * FROM u WHERE id=?\", [request.params['id']])\n");
assert!(run_rules(&rules, std::slice::from_ref(&f)).is_empty());
}
#[test]
fn ignores_query_without_input() {
let rules = unsafe_rule();
let f = file("cursor.query(\"SELECT 1\")\n");
assert!(run_rules(&rules, std::slice::from_ref(&f)).is_empty());
}
}