mod common;
use common::{rsigma, temp_file};
use predicates::prelude::*;
const RULE: &str = r#"
title: Suspicious Process Start
id: 00000000-0000-0000-0000-000000000101
logsource:
category: process_creation
detection:
selection:
Image: /usr/bin/curl
CommandLine|contains: "--insecure"
condition: selection
level: medium
"#;
const CORRELATION: &str = r#"
title: Repeated Hits
correlation:
type: event_count
rules:
- Suspicious Process Start
group-by:
- Image
timespan: 10m
condition:
gte: 5
"#;
#[test]
fn emit_sql_prints_wrapped_query_without_connecting() {
let rule = temp_file(".yml", RULE);
rsigma()
.args([
"hunt",
"run",
"-r",
rule.path().to_str().unwrap(),
"--target",
"postgres",
"--since",
"2026-07-01T00:00:00Z",
"--until",
"2026-07-02T00:00:00Z",
"--emit",
"sql",
])
.assert()
.success()
.stdout(predicate::str::contains("-- timestamp_field: time\n"))
.stdout(predicate::str::contains(
"-- rule: Suspicious Process Start (id: 00000000-0000-0000-0000-000000000101)\n",
))
.stdout(predicate::str::contains(
"SELECT * FROM (SELECT * FROM security_events WHERE \"Image\" = '/usr/bin/curl'",
))
.stdout(predicate::str::contains(
"WHERE time >= '2026-07-01T00:00:00+00:00'::timestamptz \
AND time < '2026-07-02T00:00:00+00:00'::timestamptz \
ORDER BY time LIMIT 1000;",
));
}
#[test]
fn emit_sql_jsonb_mode_extracts_from_the_json_column() {
let rule = temp_file(".yml", RULE);
rsigma()
.args([
"hunt",
"run",
"-r",
rule.path().to_str().unwrap(),
"-t",
"postgres",
"-O",
"table=events",
"-O",
"json_field=data",
"--emit",
"sql",
])
.assert()
.success()
.stdout(predicate::str::contains("-- json_field: data\n"))
.stdout(predicate::str::contains(
"FROM events WHERE data->>'Image' = '/usr/bin/curl'",
));
}
#[test]
fn correlation_rules_are_rejected_with_a_pointer() {
let rules = temp_file(".yml", &format!("{RULE}\n---\n{CORRELATION}"));
rsigma()
.args([
"hunt",
"run",
"-r",
rules.path().to_str().unwrap(),
"-t",
"postgres",
"--emit",
"sql",
])
.assert()
.code(2)
.stderr(predicate::str::contains("detection rules only"))
.stderr(predicate::str::contains("Repeated Hits"))
.stderr(predicate::str::contains("rsigma backend convert"));
}
#[test]
fn non_postgres_targets_are_rejected_with_a_pointer() {
let rule = temp_file(".yml", RULE);
rsigma()
.args([
"hunt",
"run",
"-r",
rule.path().to_str().unwrap(),
"-t",
"splunk",
"--emit",
"sql",
])
.assert()
.code(3)
.stderr(predicate::str::contains(
"hunt run supports --target postgres only; 'splunk' is convert-only",
))
.stderr(predicate::str::contains("rsigma backend convert -t splunk"));
}
#[test]
fn empty_window_is_rejected() {
let rule = temp_file(".yml", RULE);
rsigma()
.args([
"hunt",
"run",
"-r",
rule.path().to_str().unwrap(),
"-t",
"postgres",
"--since",
"2026-07-02T00:00:00Z",
"--until",
"2026-07-01T00:00:00Z",
"--emit",
"sql",
])
.assert()
.code(3)
.stderr(predicate::str::contains("empty hunt window"));
}
#[test]
fn invalid_time_bound_is_rejected() {
let rule = temp_file(".yml", RULE);
rsigma()
.args([
"hunt",
"run",
"-r",
rule.path().to_str().unwrap(),
"-t",
"postgres",
"--since",
"next tuesday",
"--emit",
"sql",
])
.assert()
.code(3)
.stderr(predicate::str::contains(
"invalid time bound 'next tuesday'",
));
}
#[test]
fn timeout_is_validated_even_for_emit_sql() {
let rule = temp_file(".yml", RULE);
let base = |timeout: &str| {
let mut cmd = rsigma();
cmd.args([
"hunt",
"run",
"-r",
rule.path().to_str().unwrap(),
"-t",
"postgres",
"--emit",
"sql",
"--timeout",
timeout,
]);
cmd
};
base("never")
.assert()
.code(3)
.stderr(predicate::str::contains(
"invalid --timeout 'never': expected a duration",
));
base("500us")
.assert()
.code(3)
.stderr(predicate::str::contains(
"invalid --timeout '500us': must be between 1ms",
));
base("0s").assert().code(3).stderr(predicate::str::contains(
"invalid --timeout '0s': must be between 1ms",
));
}
#[cfg(not(feature = "hunt-postgres"))]
#[test]
fn events_mode_without_the_feature_is_a_pointed_error() {
let rule = temp_file(".yml", RULE);
rsigma()
.args([
"hunt",
"run",
"-r",
rule.path().to_str().unwrap(),
"-t",
"postgres",
"--dsn",
"postgres://hunter@archive/siem",
])
.assert()
.code(3)
.stderr(predicate::str::contains(
"built without the 'hunt-postgres' feature",
));
}