use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
use killer::attacks::http::StdHttpClient;
use killer::klr::interpreter::{Interpreter, RunConfig};
use killer::klr::parse;
use killer::results::{TestRun, Verdict};
fn spawn_server(response: &'static str, max_conns: usize) -> u16 {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port");
let port = listener.local_addr().unwrap().port();
thread::spawn(move || {
let mut handled = 0;
for stream in listener.incoming() {
let Ok(mut stream) = stream else { break };
handle(&mut stream, response);
handled += 1;
if handled >= max_conns {
break;
}
}
});
port
}
fn handle(stream: &mut TcpStream, response: &str) {
let mut buf = [0u8; 2048];
let _ = stream.read(&mut buf);
let _ = stream.write_all(response.as_bytes());
let _ = stream.flush();
}
fn run_first(src: &str, port: u16) -> killer::results::AttackOutcome {
let program = parse(src).expect("parse");
let client = StdHttpClient::new();
let config = RunConfig {
base_url: format!("http://127.0.0.1:{port}"),
..RunConfig::default()
};
Interpreter::new(&client, config)
.run(&program.attacks)
.remove(0)
}
fn run_first_expanded(src: &str, port: u16) -> killer::results::AttackOutcome {
let program = parse(src).expect("parse");
let client = StdHttpClient::new();
let config = RunConfig {
base_url: format!("http://127.0.0.1:{port}"),
..RunConfig::default()
};
killer::klr::runner::run_all(&program.all_attacks(), &client, &config, 1).remove(0)
}
#[test]
fn detects_sql_injection_over_real_socket() {
let response =
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 16\r\nConnection: close\r\n\r\n{\"token\":\"abc\"}\n";
let port = spawn_server(response, 1);
let src = r#"
attack authentication {
target "/api/login"
send {
username = "' OR 1=1"
password = "anything"
}
expect {
status != 200
response does_not_contain "token"
}
severity critical
message: "SQL injection vulnerability detected"
}
"#;
let out = run_first(src, port);
assert_eq!(out.verdict, Verdict::Vulnerable);
assert_eq!(out.issue_id.as_deref(), Some("KLR-SQLI"));
}
#[test]
fn secure_server_defends_over_real_socket() {
let response =
"HTTP/1.1 401 Unauthorized\r\nContent-Type: text/plain\r\nContent-Length: 20\r\nConnection: close\r\n\r\ninvalid credentials\n";
let port = spawn_server(response, 1);
let src = r#"
attack authentication {
target "/api/login"
send { username = "' OR 1=1" }
expect {
status != 200
response does_not_contain "token"
}
}
"#;
let out = run_first(src, port);
assert_eq!(out.verdict, Verdict::Secure);
}
#[test]
fn unimplemented_check_does_not_pass_over_real_socket() {
let response =
"HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 13\r\nConnection: close\r\n\r\n404 Not Found";
let port = spawn_server(response, 1);
let src = r#"
test csrf_protection {
endpoint "/transfer"
check csrf
}
"#;
let out = run_first_expanded(src, port);
assert_eq!(out.verdict, Verdict::Inconclusive);
assert!(out.checks.iter().all(|c| !c.evaluated && !c.passed));
let run = TestRun {
project: None,
timestamp: "t".into(),
sources: vec![],
attacks: vec![out],
rule_findings: vec![],
workers: 1,
elapsed_ms: 0,
};
assert!(run.has_inconclusive());
assert!(!killer::report::render_attack_report(&run).contains("All tests passed"));
}
#[test]
fn errors_when_target_unreachable() {
let src = r#"
attack a {
target "/x"
expect { status != 200 }
}
"#;
let out = run_first(src, 1);
assert_eq!(out.verdict, Verdict::Errored);
}