santh-sigma 0.1.4

Sigma rule compiler for security research
Documentation
use super::*;
use secir::template::matchers::MatcherKind;

#[test]
fn title_to_id_basic() {
    assert_eq!(
        title_to_id("Multiple Failed Logins"),
        "multiple-failed-logins"
    );
}

#[test]
fn title_to_id_with_special_chars() {
    assert_eq!(title_to_id("Deep Nested OR"), "deep-nested-or");
}

#[test]
fn severity_mapping() {
    assert_eq!(map_severity("test", "critical").unwrap(), Severity::Critical);
    assert_eq!(map_severity("test", "HIGH").unwrap(), Severity::High);
    assert_eq!(map_severity("test", "medium").unwrap(), Severity::Medium);
    assert_eq!(map_severity("test", "low").unwrap(), Severity::Low);
    assert_eq!(map_severity("test", "informational").unwrap(), Severity::Info);
    assert_eq!(map_severity("test", "info").unwrap(), Severity::Info);
    assert!(
        map_severity("test", "unknown").is_err(),
        "unknown Sigma level must be rejected"
    );
}

#[test]
fn compile_basic_sigma_rule() {
    let yaml = r#"
title: Sigma IR Rule
logsource:
  product: windows
detection:
  selection:
    CommandLine|contains: powershell.exe
  condition: selection
level: high
"#;
    let template = compile_rule(serde_yaml::from_str::<SigmaRule>(yaml).unwrap()).unwrap();
    assert_eq!(template.id, "sigma-ir-rule");
    assert_eq!(template.info.severity, Severity::High);
    assert_eq!(template.protocol, Protocol::File);
    assert!(!template.requests.is_empty());
    assert!(!template.requests[0].matchers.is_empty());
    assert_eq!(
        template.requests[0].matchers[0].values,
        vec!["powershell.exe"]
    );
}

#[test]
fn compile_multiple_values() {
    let yaml = r#"
title: Multi Value Rule
detection:
  selection:
    CommandLine|contains:
      - "powershell.exe"
      - "cmd.exe"
  condition: selection
level: medium
"#;
    let template = compile_rule(serde_yaml::from_str::<SigmaRule>(yaml).unwrap()).unwrap();
    assert_eq!(template.requests[0].matchers[0].values.len(), 2);
}

#[test]
fn compile_endswith_modifier() {
    let yaml = r#"
title: Endswith Rule
detection:
  selection:
    ParentImage|endswith: "\\winword.exe"
  condition: selection
level: low
"#;
    let template = compile_rule(serde_yaml::from_str::<SigmaRule>(yaml).unwrap()).unwrap();
    assert_eq!(template.requests[0].matchers[0].kind, MatcherKind::Regex);
    assert!(template.requests[0].matchers[0].values[0].ends_with('$'));
}

#[test]
fn compile_cased_modifier_drops_case_insensitive_flag() {
    let yaml = r#"
title: Cased Rule
detection:
  selection:
    cmd|cased: "Get-Process"
  condition: selection
level: medium
"#;
    let template = compile_rule(serde_yaml::from_str::<SigmaRule>(yaml).unwrap()).unwrap();
    let value = &template.requests[0].matchers[0].values[0];
    // The default case-insensitive matcher would carry `(?i)`
    // or `(?im)`: cased must drop the `i` (so case-sensitive).
    assert!(
        value.starts_with("(?m)"),
        "expected case-sensitive flags, got: {value}"
    );
    assert!(
        !value.starts_with("(?im)"),
        "cased must not produce (?im): {value}"
    );
    // The literal must still be there.
    assert!(value.contains("Get\\-Process"), "missing literal: {value}");
}

#[test]
fn compile_cased_modifier_on_contains_falls_through_to_regex() {
    // `contains` without `|cased` uses the faster Word matcher
    // (Aho-Corasick); with `|cased` the engine must respect
    // case, which forces the regex path.
    let yaml = r#"
title: Cased Contains
detection:
  selection:
    cmd|contains|cased: "PowerShell"
  condition: selection
level: medium
"#;
    let template = compile_rule(serde_yaml::from_str::<SigmaRule>(yaml).unwrap()).unwrap();
    assert_eq!(template.requests[0].matchers[0].kind, MatcherKind::Regex);
    let value = &template.requests[0].matchers[0].values[0];
    assert!(
        value.starts_with("(?m)"),
        "expected cased flags, got: {value}"
    );
}

#[test]
fn compile_base64_modifier_encodes_value() {
    let yaml = r#"
title: Base64 Rule
detection:
  selection:
    payload|base64: "rm -rf /"
  condition: selection
level: critical
"#;
    let template = compile_rule(serde_yaml::from_str::<SigmaRule>(yaml).unwrap()).unwrap();
    let value = &template.requests[0].matchers[0].values[0];
    // base64("rm -rf /") = "cm0gLXJmIC8=", but the default op
    // is `Exact`, which wraps the encoded form as an anchored
    // case-insensitive regex. Both forms are fine for matching;
    // assert the base64 payload is INSIDE the matcher value.
    assert!(
        value.contains("cm0gLXJmIC8="),
        "expected base64 encoding in matcher value, got: {value}"
    );
}

#[test]
fn compile_base64_and_base64offset_are_mutually_exclusive() {
    let yaml = r#"
title: Conflicting Modifiers
detection:
  selection:
    payload|base64|base64offset: "foo"
  condition: selection
level: low
"#;
    let result = compile_rule(serde_yaml::from_str::<SigmaRule>(yaml).unwrap());
    assert!(result.is_err());
    let err = result.unwrap_err().to_string();
    assert!(err.contains("mutually exclusive"), "wrong error: {err}");
}

#[test]
fn compile_unknown_modifier_is_rejected_with_actionable_error() {
    // Regression: a typo or unsupported modifier (e.g. someone
    // copies a Sigma v3 rule using `|wide` which we don't
    // support yet) must NOT silently fall through, it must
    // surface as a hard error naming the bad modifier.
    let yaml = r#"
title: Bad Modifier
detection:
  selection:
    cmd|wibble: "foo"
  condition: selection
level: low
"#;
    let result = compile_rule(serde_yaml::from_str::<SigmaRule>(yaml).unwrap());
    let err = result.unwrap_err().to_string();
    assert!(err.contains("wibble"), "must name bad modifier: {err}");
}

#[test]
fn reserved_detection_keys_are_excluded_from_selections() {
    // `condition`, `timeframe`, and `keywords` are reserved by the
    // Sigma spec and must not be treated as selection definitions.
    let yaml = r#"
title: Reserved Keys
detection:
  condition: selection
  timeframe: last_24h
  keywords:
    - foo
  selection:
    cmd|contains: evil
"#;
    let template = compile_rule(serde_yaml::from_str::<SigmaRule>(yaml).unwrap()).unwrap();
    // Only the real selection should compile.
    assert_eq!(template.id, "reserved-keys");
    assert!(!template.requests.is_empty());
}