use detect::expr::Expr;
use detect::parser::error::DetectError;
use detect::parser::{RawParser, Typechecker};
use detect::predicate::{
parse_time_value, Bound, DetectFileType, EnumMatcher, MetadataPredicate, NamePredicate,
NumberMatcher, Predicate, StreamingCompiledContentPredicate, StringMatcher, TimeMatcher,
};
fn parse_and_typecheck(expr: &str) -> Result<Expr<Predicate>, DetectError> {
let raw_expr = RawParser::parse_raw_expr(expr).unwrap();
Typechecker::typecheck(raw_expr, expr, &detect::RuntimeConfig::default())
}
use std::collections::HashSet;
#[test]
fn test_selector_recognition() {
let typed = parse_and_typecheck("path == foo").unwrap();
let expected = Expr::Predicate(Predicate::name(NamePredicate::FullPath(
StringMatcher::Equals("foo".to_string()),
)));
assert_eq!(typed, expected);
let typed = parse_and_typecheck("name == test.rs").unwrap();
let expected = Expr::Predicate(Predicate::name(NamePredicate::FileName(
StringMatcher::Equals("test.rs".to_string()),
)));
assert_eq!(typed, expected);
let typed = parse_and_typecheck("ext == rs").unwrap();
let expected = Expr::Predicate(Predicate::name(NamePredicate::Extension(
StringMatcher::Equals("rs".to_string()),
)));
assert_eq!(typed, expected);
}
#[test]
fn test_unknown_selector() {
let error = parse_and_typecheck("unknown_selector == foo").unwrap_err();
assert!(
matches!(error, DetectError::UnknownSelector { selector, .. } if selector == "unknown_selector")
);
}
#[test]
fn test_operator_validation() {
assert!(parse_and_typecheck("name == foo").is_ok());
let error = parse_and_typecheck("name > foo").unwrap_err();
assert!(matches!(error, DetectError::IncompatibleOperator { .. }));
assert!(parse_and_typecheck("size > 1000").is_ok());
let error = parse_and_typecheck("size contains foo").unwrap_err();
assert!(matches!(error, DetectError::IncompatibleOperator { .. }));
}
#[test]
fn test_string_value_parsing() {
let typed = parse_and_typecheck("name == test.rs").unwrap();
let expected = Expr::Predicate(Predicate::name(NamePredicate::FileName(
StringMatcher::Equals("test.rs".to_string()),
)));
assert_eq!(typed, expected);
let typed = parse_and_typecheck("name != test.rs").unwrap();
let expected = Expr::Predicate(Predicate::name(NamePredicate::FileName(
StringMatcher::NotEquals("test.rs".to_string()),
)));
assert_eq!(typed, expected);
let typed = parse_and_typecheck("path contains src").unwrap();
let expected = Expr::Predicate(Predicate::name(NamePredicate::FullPath(
StringMatcher::Contains("src".to_string()),
)));
assert_eq!(typed, expected);
}
#[test]
fn test_set_value_parsing() {
let typed = parse_and_typecheck("ext in [rs, js, ts]").unwrap();
let expected_set: HashSet<String> = vec!["rs", "js", "ts"]
.into_iter()
.map(|s| s.to_string())
.collect();
let expected = Expr::Predicate(Predicate::name(NamePredicate::Extension(
StringMatcher::In(expected_set),
)));
assert_eq!(typed, expected);
}
#[test]
fn test_regex_parsing() {
let typed = parse_and_typecheck("content ~= TODO.*").unwrap();
let expected_content = StreamingCompiledContentPredicate::new("TODO.*".to_string()).unwrap();
let expected = Expr::Predicate(Predicate::contents(expected_content));
assert_eq!(typed, expected);
}
#[test]
fn test_simple_unquoted_quantifiers_typecheck() {
let test_cases = vec![
("content ~= [0-9]+", "[0-9]+"),
("content ~= [a-z]*", "[a-z]*"),
("content ~= [A-Z]?", "[A-Z]?"),
("content ~= [0-9]{2,4}", "[0-9]{2,4}"),
("content ~= (foo|bar)+", "(foo|bar)+"),
("content ~= (test)*", "(test)*"),
("content ~= (a|b){2,3}", "(a|b){2,3}"),
];
for (expr, expected_pattern) in test_cases {
let typed = parse_and_typecheck(expr).unwrap();
let expected = Expr::Predicate(Predicate::contents(
StreamingCompiledContentPredicate::new(expected_pattern.to_string()).unwrap(),
));
assert_eq!(typed, expected, "Mismatch for: {}", expr);
}
}
#[test]
fn test_complex_unquoted_patterns_typecheck() {
let test_cases = vec![
("content ~= [a-z]+[A-Z]+", "[a-z]+[A-Z]+"),
("content ~= [0-9]+\\.[0-9]+", "[0-9]+\\.[0-9]+"),
("content ~= [a-z]+[A-Z]+[0-9]+", "[a-z]+[A-Z]+[0-9]+"),
("content ~= [0-9]+@domain", "[0-9]+@domain"),
("content ~= [0-9]+-[0-9]+", "[0-9]+-[0-9]+"),
("content ~= [a-z]+_suffix", "[a-z]+_suffix"),
("content ~= [0-9]+:port", "[0-9]+:port"),
(
"content ~= [a-z]+@[a-z]+\\.[a-z]+",
"[a-z]+@[a-z]+\\.[a-z]+",
),
(
"content ~= \\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}",
"\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}",
),
];
for (expr, expected_pattern) in test_cases {
let typed = parse_and_typecheck(expr).unwrap();
let expected = Expr::Predicate(Predicate::contents(
StreamingCompiledContentPredicate::new(expected_pattern.to_string()).unwrap(),
));
assert_eq!(typed, expected, "Mismatch for: {}", expr);
}
}
#[test]
fn test_quoted_unquoted_equivalence() {
let test_cases = vec![
("content ~= [0-9]+", r#"content ~= "[0-9]+""#),
("content ~= [a-z]+[A-Z]+", r#"content ~= "[a-z]+[A-Z]+""#),
("content ~= (foo|bar)+", r#"content ~= "(foo|bar)+""#),
("content ~= [0-9]+@domain", r#"content ~= "[0-9]+@domain""#),
];
for (unquoted_expr, quoted_expr) in test_cases {
let unquoted = parse_and_typecheck(unquoted_expr).unwrap();
let quoted = parse_and_typecheck(quoted_expr).unwrap();
assert_eq!(
unquoted, quoted,
"Quoted/unquoted mismatch for: {}",
unquoted_expr
);
}
}
#[test]
fn test_unquoted_quantifiers_in_boolean_expressions() {
let typed = parse_and_typecheck("content ~= [0-9]+ AND size > 1kb").unwrap();
let left = Expr::Predicate(Predicate::contents(
StreamingCompiledContentPredicate::new("[0-9]+".to_string()).unwrap(),
));
let right = Expr::Predicate(Predicate::meta(MetadataPredicate::Filesize(
NumberMatcher::In(Bound::Left(1025..)), )));
let expected = Expr::and(left, right);
assert_eq!(typed, expected);
let typed = parse_and_typecheck("content ~= [a-z]+[A-Z]+ OR name == test").unwrap();
let left = Expr::Predicate(Predicate::contents(
StreamingCompiledContentPredicate::new("[a-z]+[A-Z]+".to_string()).unwrap(),
));
let right = Expr::Predicate(Predicate::name(NamePredicate::FileName(
StringMatcher::Equals("test".to_string()),
)));
let expected = Expr::or(left, right);
assert_eq!(typed, expected);
let typed = parse_and_typecheck("NOT content ~= [0-9]+").unwrap();
let inner = Expr::Predicate(Predicate::contents(
StreamingCompiledContentPredicate::new("[0-9]+".to_string()).unwrap(),
));
let expected = Expr::negate(inner);
assert_eq!(typed, expected);
let typed = parse_and_typecheck("(content ~= [0-9]+ OR content ~= [a-z]+) AND NOT type == dir")
.unwrap();
let lhs_left = Expr::Predicate(Predicate::contents(
StreamingCompiledContentPredicate::new("[0-9]+".to_string()).unwrap(),
));
let lhs_right = Expr::Predicate(Predicate::contents(
StreamingCompiledContentPredicate::new("[a-z]+".to_string()).unwrap(),
));
let lhs = Expr::or(lhs_left, lhs_right);
let rhs_inner = Expr::Predicate(Predicate::meta(MetadataPredicate::Type(
EnumMatcher::Equals(detect::predicate::DetectFileType::Directory),
)));
let rhs = Expr::negate(rhs_inner);
let expected = Expr::and(lhs, rhs);
assert_eq!(typed, expected);
}
#[test]
fn test_invalid_regex_patterns_fail_typecheck() {
let invalid_patterns = vec![
"content ~= [z-a]", "content ~= (?P<>test)", "content ~= (?#", ];
for expr in invalid_patterns {
let result = parse_and_typecheck(expr);
assert!(
matches!(result, Err(DetectError::InvalidValue { .. })),
"Should fail with InvalidValue error for: {}, got: {:?}",
expr,
result
);
}
}
#[test]
fn test_size_value_parsing() {
let typed = parse_and_typecheck("size > 1000").unwrap();
let expected = Expr::Predicate(Predicate::meta(MetadataPredicate::Filesize(
NumberMatcher::In(Bound::Left(1001..)),
)));
assert_eq!(typed, expected);
let typed = parse_and_typecheck("size > 1mb").unwrap();
let expected = Expr::Predicate(Predicate::meta(MetadataPredicate::Filesize(
NumberMatcher::In(Bound::Left(1048577..)),
)));
assert_eq!(typed, expected);
}
#[test]
fn test_temporal_value_parsing() {
let typed = parse_and_typecheck("modified > -7d").unwrap();
assert!(
matches!(typed, Expr::Predicate(ref p) if matches!(p, Predicate::Metadata(ref mp) if matches!(&**mp, MetadataPredicate::Modified(TimeMatcher::After(_)))))
);
let typed = parse_and_typecheck("created == 2024-01-01").unwrap();
let expected_time = parse_time_value("2024-01-01").unwrap();
let expected = Expr::Predicate(Predicate::meta(MetadataPredicate::Created(
TimeMatcher::Equals(expected_time),
)));
assert_eq!(typed, expected);
}
#[test]
fn test_boolean_logic_preservation() {
let typed = parse_and_typecheck("name == foo AND size > 1000").unwrap();
assert!(matches!(typed, Expr::And(_, _)));
let typed = parse_and_typecheck("name == foo OR ext == rs").unwrap();
assert!(matches!(typed, Expr::Or(_, _)));
let typed = parse_and_typecheck("NOT name == foo").unwrap();
assert!(matches!(typed, Expr::Not(_)));
}
#[test]
fn test_complex_expression() {
let typed =
parse_and_typecheck("(name == test.rs OR ext in [js, ts]) AND NOT size > 1mb").unwrap();
let lhs_left = Expr::Predicate(Predicate::name(NamePredicate::FileName(
StringMatcher::Equals("test.rs".to_string()),
)));
let lhs_right_set: HashSet<String> = vec!["js", "ts"]
.into_iter()
.map(|s| s.to_string())
.collect();
let lhs_right = Expr::Predicate(Predicate::name(NamePredicate::Extension(
StringMatcher::In(lhs_right_set),
)));
let lhs = Expr::or(lhs_left, lhs_right);
let rhs_inner = Expr::Predicate(Predicate::meta(MetadataPredicate::Filesize(
NumberMatcher::In(Bound::Left(1048577..)),
)));
let rhs = Expr::negate(rhs_inner);
let expected = Expr::and(lhs, rhs);
assert_eq!(typed, expected);
}
#[test]
fn test_selector_aliases() {
assert_eq!(
parse_and_typecheck("filename == test.rs").unwrap(),
parse_and_typecheck("name == test.rs").unwrap()
);
assert_eq!(
parse_and_typecheck("stem == test").unwrap(),
parse_and_typecheck("basename == test").unwrap()
);
assert_eq!(
parse_and_typecheck("extension == rs").unwrap(),
parse_and_typecheck("ext == rs").unwrap()
);
assert_eq!(
parse_and_typecheck("parent contains src").unwrap(),
parse_and_typecheck("dir contains src").unwrap()
);
assert_eq!(
parse_and_typecheck("directory contains src").unwrap(),
parse_and_typecheck("dir contains src").unwrap()
);
assert_eq!(
parse_and_typecheck("filesize > 1mb").unwrap(),
parse_and_typecheck("size > 1mb").unwrap()
);
assert_eq!(
parse_and_typecheck("bytes > 1024").unwrap(),
parse_and_typecheck("size > 1024").unwrap()
);
assert_eq!(
parse_and_typecheck("filetype == file").unwrap(),
parse_and_typecheck("type == file").unwrap()
);
assert_eq!(
parse_and_typecheck("mtime > 2024-01-01").unwrap(),
parse_and_typecheck("modified > 2024-01-01").unwrap()
);
assert_eq!(
parse_and_typecheck("ctime < 2024-12-31").unwrap(),
parse_and_typecheck("created < 2024-12-31").unwrap()
);
assert_eq!(
parse_and_typecheck("atime == 2024-06-15").unwrap(),
parse_and_typecheck("accessed == 2024-06-15").unwrap()
);
assert_eq!(
parse_and_typecheck("contents contains TODO").unwrap(),
parse_and_typecheck("content contains TODO").unwrap()
);
assert_eq!(
parse_and_typecheck("text ~= pattern").unwrap(),
parse_and_typecheck("content ~= pattern").unwrap()
);
}
#[test]
fn test_operator_aliases() {
let cases = vec![
("name = foo", "name == foo"),
("name eq foo", "name == foo"),
("path != bar", "path <> bar"),
("content matches pattern", "content ~= pattern"),
("content regex pattern", "content ~= pattern"),
("path has src", "path contains src"),
("path includes src", "path contains src"),
];
for (alias, canonical) in cases {
let result1 = RawParser::parse_raw_expr(alias).unwrap();
let typed1 =
Typechecker::typecheck(result1, alias, &detect::RuntimeConfig::default()).unwrap();
let result2 = RawParser::parse_raw_expr(canonical).unwrap();
let typed2 =
Typechecker::typecheck(result2, canonical, &detect::RuntimeConfig::default()).unwrap();
assert_eq!(typed1, typed2, "Failed for {} vs {}", alias, canonical);
}
let num_cases = vec![
("size = 100", "size == 100"),
("size gt 100", "size > 100"),
("size gte 100", "size >= 100"),
("size lt 100", "size < 100"),
("size lte 100", "size <= 100"),
("size => 100", "size >= 100"),
("size =< 100", "size <= 100"),
];
for (alias, canonical) in num_cases {
let result1 = RawParser::parse_raw_expr(alias).unwrap();
let typed1 =
Typechecker::typecheck(result1, alias, &detect::RuntimeConfig::default()).unwrap();
let result2 = RawParser::parse_raw_expr(canonical).unwrap();
let typed2 =
Typechecker::typecheck(result2, canonical, &detect::RuntimeConfig::default()).unwrap();
assert_eq!(typed1, typed2, "Failed for {} vs {}", alias, canonical);
}
let time_cases = vec![
("modified on 2024-01-01", "modified == 2024-01-01"),
("modified before 2024-01-01", "modified < 2024-01-01"),
("modified after 2024-01-01", "modified > 2024-01-01"),
];
for (alias, canonical) in time_cases {
let result1 = RawParser::parse_raw_expr(alias).unwrap();
let typed1 =
Typechecker::typecheck(result1, alias, &detect::RuntimeConfig::default()).unwrap();
let result2 = RawParser::parse_raw_expr(canonical).unwrap();
let typed2 =
Typechecker::typecheck(result2, canonical, &detect::RuntimeConfig::default()).unwrap();
assert_eq!(typed1, typed2, "Failed for {} vs {}", alias, canonical);
}
}
#[test]
fn test_invalid_values() {
let result = parse_and_typecheck("name == [foo, bar]");
assert!(
result.is_ok(),
"With == operator, [foo, bar] is a literal string value"
);
let error = parse_and_typecheck("size > foo").unwrap_err();
assert!(matches!(error, DetectError::InvalidValue { .. }));
let error = parse_and_typecheck("content in [foo, bar]").unwrap_err();
assert!(matches!(error, DetectError::IncompatibleOperator { .. }));
}
#[test]
fn test_content_operators() {
let valid_ops = vec!["==", "~=", "contains"];
for op in valid_ops {
let expr = format!("content {} pattern", op);
assert!(
parse_and_typecheck(&expr).is_ok(),
"Failed for operator: {}",
op
);
}
let error = parse_and_typecheck("content in [foo, bar]").unwrap_err();
assert!(matches!(error, DetectError::IncompatibleOperator { .. }));
let error = parse_and_typecheck("content != pattern").unwrap_err();
assert!(matches!(error, DetectError::IncompatibleOperator { .. }));
}
#[test]
fn test_type_safety_enforcement() {
let error = parse_and_typecheck("name > foo").unwrap_err();
assert!(matches!(error, DetectError::IncompatibleOperator { .. }));
let error = parse_and_typecheck("size contains foo").unwrap_err();
assert!(matches!(error, DetectError::IncompatibleOperator { .. }));
let error = parse_and_typecheck("modified contains 2024").unwrap_err();
assert!(matches!(error, DetectError::IncompatibleOperator { .. }));
let error = parse_and_typecheck("content in [foo, bar]").unwrap_err();
assert!(matches!(error, DetectError::IncompatibleOperator { .. }));
let error = parse_and_typecheck("content != pattern").unwrap_err();
assert!(matches!(error, DetectError::IncompatibleOperator { .. }));
}
#[test]
fn test_all_operator_aliases_work() {
let test_cases = vec![
"name = foo",
"name eq foo",
"path <> bar",
"content matches pattern",
"content regex pattern",
"path has src",
"path includes src",
"size = 100",
"size gt 100",
"size gte 100",
"size lt 100",
"size lte 100",
"size => 100",
"size =< 100",
"modified on 2024-01-01",
"modified before 2024-01-01",
"modified after 2024-01-01",
];
for expr in test_cases {
let parse_result = RawParser::parse_raw_expr(expr);
assert!(parse_result.is_ok(), "Failed to parse: {}", expr);
let typecheck_result = Typechecker::typecheck(
parse_result.unwrap(),
expr,
&detect::RuntimeConfig::default(),
);
assert!(typecheck_result.is_ok(), "Failed to typecheck: {}", expr);
}
}
#[test]
fn test_case_insensitive_operators() {
let test_cases = vec![
("name CONTAINS foo", "name contains foo"),
("size GT 100", "size gt 100"),
("modified BEFORE 2024-01-01", "modified before 2024-01-01"),
("content MATCHES pattern", "content matches pattern"),
("ext IN [rs, js]", "ext in [rs, js]"),
("name EQ test", "name eq test"),
];
for (upper_case, lower_case) in test_cases {
let result1 = RawParser::parse_raw_expr(upper_case).unwrap();
let typed1 =
Typechecker::typecheck(result1, upper_case, &detect::RuntimeConfig::default()).unwrap();
let result2 = RawParser::parse_raw_expr(lower_case).unwrap();
let typed2 =
Typechecker::typecheck(result2, lower_case, &detect::RuntimeConfig::default()).unwrap();
assert_eq!(
typed1, typed2,
"Case sensitivity failed for: {} vs {}",
upper_case, lower_case
);
}
}
#[test]
fn test_truly_unknown_operators() {
let test_cases = vec![
("name === foo", "==="),
("name ! foo", "!"),
("name <=> foo", "<=>"),
("name ~~ foo", "~~"),
("name >>> foo", ">>>"),
("name like foo", "like"), ("name between foo", "between"), ];
for (expr, op) in test_cases {
let error = parse_and_typecheck(expr).unwrap_err();
assert!(
matches!(error, DetectError::UnknownOperator { operator: ref o, .. } if o == op),
"Expected UnknownOperator({}) for expression: {}",
op,
expr
);
}
}
#[test]
fn test_operator_edge_cases() {
use detect::parser::test_utils::RawTestExpr;
let expr = "name = foo";
let result = RawParser::parse_raw_expr(expr).unwrap();
let expected = RawTestExpr::string_predicate("name", "=", "foo");
assert_eq!(result.to_test_expr(), expected);
let typecheck_result = Typechecker::typecheck(result, expr, &detect::RuntimeConfig::default());
assert!(
typecheck_result.is_ok(),
"Single = should typecheck as valid alias for =="
);
let expr = "name ! foo";
let result = RawParser::parse_raw_expr(expr).unwrap();
let expected = RawTestExpr::string_predicate("name", "!", "foo");
assert_eq!(result.to_test_expr(), expected);
let typecheck_result = Typechecker::typecheck(result, expr, &detect::RuntimeConfig::default());
assert!(
matches!(typecheck_result, Err(DetectError::UnknownOperator { operator: ref o, .. }) if o == "!"),
"Single ! should fail typecheck with UnknownOperator"
);
let expr = "name ~ foo";
let result = RawParser::parse_raw_expr(expr).unwrap();
let expected = RawTestExpr::string_predicate("name", "~", "foo");
assert_eq!(result.to_test_expr(), expected);
let typecheck_result = Typechecker::typecheck(result, expr, &detect::RuntimeConfig::default());
assert!(
typecheck_result.is_ok(),
"Single ~ should typecheck as valid alias"
);
let result = RawParser::parse_raw_expr("name < = foo");
assert!(
result.is_err(),
"Spaced <= should still fail due to grammar structure"
);
let expr = "name === foo";
let result = RawParser::parse_raw_expr(expr).unwrap();
let expected = RawTestExpr::string_predicate("name", "===", "foo");
assert_eq!(result.to_test_expr(), expected);
let typecheck_result = Typechecker::typecheck(result, expr, &detect::RuntimeConfig::default());
assert!(
matches!(typecheck_result, Err(DetectError::UnknownOperator { operator: ref o, .. }) if o == "==="),
"Triple equals should fail typecheck with UnknownOperator"
);
let expr = "name <> foo";
let result = RawParser::parse_raw_expr(expr).unwrap();
let expected = RawTestExpr::string_predicate("name", "<>", "foo");
assert_eq!(result.to_test_expr(), expected);
let typecheck_result = Typechecker::typecheck(result, expr, &detect::RuntimeConfig::default());
assert!(
typecheck_result.is_ok(),
"SQL-style <> should typecheck as valid alias for !="
);
}
#[test]
fn test_unknown_operators_parse_but_fail_typecheck() {
use detect::parser::test_utils::RawTestExpr;
let test_cases = vec![
("name ! foo", "!", "name", "foo"),
("size === 100", "===", "size", "100"),
("path <=> test", "<=>", "path", "test"),
("content ~~ pattern", "~~", "content", "pattern"),
("depth >>> 3", ">>>", "depth", "3"),
("type !! file", "!!", "type", "file"),
("ext !== rs", "!==", "ext", "rs"),
];
for (expr, expected_op, expected_selector, expected_value) in test_cases {
let result = RawParser::parse_raw_expr(expr).unwrap_or_else(|e| {
panic!("Failed to parse '{}': {:?}", expr, e);
});
let expected =
RawTestExpr::string_predicate(expected_selector, expected_op, expected_value);
assert_eq!(
result.to_test_expr(),
expected,
"Parsed structure mismatch for '{}'",
expr
);
let typecheck_result =
Typechecker::typecheck(result, expr, &detect::RuntimeConfig::default());
assert!(
matches!(typecheck_result, Err(DetectError::UnknownOperator { operator: ref o, .. }) if o == expected_op),
"Expected UnknownOperator({}) for '{}', got {:?}",
expected_op,
expr,
typecheck_result
);
}
}
#[test]
fn test_very_large_numeric_values() {
use detect::parser::typechecker::Typechecker;
let cases = vec![
"size == 18446744073709551615", "size > 9999999999999999999",
"filesize < 1000000000000000000",
];
for expr in cases {
let result = RawParser::parse_raw_expr(expr);
assert!(result.is_ok(), "Failed to parse: {}", expr);
}
let overflow = "size > 99999999999999999999999999999";
let parse_result = RawParser::parse_raw_expr(overflow);
assert!(parse_result.is_ok());
let typecheck_result = Typechecker::typecheck(
parse_result.unwrap(),
overflow,
&detect::RuntimeConfig::default(),
);
assert!(typecheck_result.is_err());
}
#[test]
fn test_special_regex_characters() {
use detect::parser::typechecker::Typechecker;
let cases = vec![
(r#"name ~= "test\\.rs""#, "Escaped dot in regex"),
(r#"path ~= "src/main\\.rs""#, "Path with escaped dot"),
(r#"content ~= "\\bword\\b""#, "Word boundary anchors"),
(
r#"path ~= "[a-z]+\\.rs$""#,
"Character class with quantifier",
),
(
r#"name ~= "^test_[0-9]{3}""#,
"Anchored pattern with repetition",
),
(
r#"text ~= "(?i)case.*insensitive""#,
"Case insensitive flag",
),
];
for (expr, description) in cases {
let parse_result = RawParser::parse_raw_expr(expr);
assert!(
parse_result.is_ok(),
"Failed to parse {}: {}",
description,
expr
);
let typecheck_result = Typechecker::typecheck(
parse_result.unwrap(),
expr,
&detect::RuntimeConfig::default(),
);
assert!(
typecheck_result.is_ok(),
"Failed to typecheck {}: {}",
description,
expr
);
}
}
#[test]
fn test_enum_valid_values_all_aliases() {
let test_cases = vec![
("type == file", DetectFileType::File),
("type == dir", DetectFileType::Directory),
("type == directory", DetectFileType::Directory),
("type == symlink", DetectFileType::Symlink),
("type == link", DetectFileType::Symlink),
("type == socket", DetectFileType::Socket),
("type == sock", DetectFileType::Socket),
("type == fifo", DetectFileType::Fifo),
("type == pipe", DetectFileType::Fifo),
("type == block", DetectFileType::BlockDevice),
("type == blockdev", DetectFileType::BlockDevice),
("type == char", DetectFileType::CharDevice),
("type == chardev", DetectFileType::CharDevice),
];
for (expr, expected_variant) in test_cases {
let typed = parse_and_typecheck(expr).unwrap();
let expected = Expr::Predicate(Predicate::meta(MetadataPredicate::Type(
EnumMatcher::Equals(expected_variant),
)));
assert_eq!(typed, expected, "Failed for: {}", expr);
}
}
#[test]
fn test_enum_invalid_values() {
let invalid_cases = vec![
"type == dirq", "type == folder", "type == executable", "type == reg", "type == unknown", ];
for expr in invalid_cases {
let error = parse_and_typecheck(expr).unwrap_err();
assert!(
matches!(error, DetectError::InvalidValue { ref expected, .. } if expected.contains("file") && expected.contains("dir")),
"Expected InvalidValue error with list of valid types for '{}', got: {:?}",
expr,
error
);
}
}
#[test]
fn test_enum_not_equals_operator() {
let typed = parse_and_typecheck("type != file").unwrap();
let expected = Expr::Predicate(Predicate::meta(MetadataPredicate::Type(
EnumMatcher::NotEquals(DetectFileType::File),
)));
assert_eq!(typed, expected);
}
#[test]
fn test_enum_in_operator() {
use std::collections::HashSet;
let typed = parse_and_typecheck("type in [file, dir, symlink]").unwrap();
match typed {
Expr::Predicate(Predicate::Metadata(ref mp)) => match &**mp {
MetadataPredicate::Type(EnumMatcher::In(ref set)) => {
let expected_set: HashSet<DetectFileType> = vec![
DetectFileType::File,
DetectFileType::Directory,
DetectFileType::Symlink,
]
.into_iter()
.collect();
assert_eq!(set, &expected_set);
}
_ => panic!("Expected Type predicate with In matcher"),
},
_ => panic!("Expected Metadata predicate"),
}
}
#[test]
fn test_enum_in_operator_with_aliases() {
use std::collections::HashSet;
let typed = parse_and_typecheck("type in [directory, link, sock]").unwrap();
match typed {
Expr::Predicate(Predicate::Metadata(ref mp)) => match &**mp {
MetadataPredicate::Type(EnumMatcher::In(ref set)) => {
let expected_set: HashSet<DetectFileType> = vec![
DetectFileType::Directory,
DetectFileType::Symlink,
DetectFileType::Socket,
]
.into_iter()
.collect();
assert_eq!(set, &expected_set);
}
_ => panic!("Expected Type predicate with In matcher"),
},
_ => panic!("Expected Metadata predicate"),
}
}
#[test]
fn test_enum_invalid_value_in_set() {
let error = parse_and_typecheck("type in [file, invalid, dir]").unwrap_err();
assert!(
matches!(error, DetectError::InvalidValue { ref found, .. } if found == "invalid"),
"Expected InvalidValue error for 'invalid', got: {:?}",
error
);
}
#[test]
fn test_enum_incompatible_operators() {
let invalid_operator_cases = vec![
"type ~= file", "type contains dir", "type > file", "type < dir", "type >= socket", "type before file", ];
for expr in invalid_operator_cases {
let error = parse_and_typecheck(expr).unwrap_err();
assert!(
matches!(
error,
DetectError::IncompatibleOperator { .. } | DetectError::UnknownOperator { .. }
),
"Expected IncompatibleOperator or UnknownOperator for '{}', got: {:?}",
expr,
error
);
}
}
#[test]
fn test_enum_case_insensitive() {
let test_cases = vec![
("type == FILE", DetectFileType::File),
("type == Dir", DetectFileType::Directory),
("type == SYMLINK", DetectFileType::Symlink),
("type == SoCkEt", DetectFileType::Socket),
];
for (expr, expected_variant) in test_cases {
let typed = parse_and_typecheck(expr).unwrap();
let expected = Expr::Predicate(Predicate::meta(MetadataPredicate::Type(
EnumMatcher::Equals(expected_variant),
)));
assert_eq!(typed, expected, "Case insensitivity failed for: {}", expr);
}
}
#[test]
fn test_enum_in_boolean_expressions() {
let typed = parse_and_typecheck("type == dir AND size > 1kb").unwrap();
assert!(matches!(typed, Expr::And(_, _)));
let typed = parse_and_typecheck("type != file OR name == test").unwrap();
assert!(matches!(typed, Expr::Or(_, _)));
let typed = parse_and_typecheck("NOT type == symlink").unwrap();
assert!(matches!(typed, Expr::Not(_)));
let typed =
parse_and_typecheck("(type in [file, dir] AND size > 0) OR name == README").unwrap();
match typed {
Expr::Or(ref lhs, _) => {
assert!(matches!(**lhs, Expr::And(_, _)));
}
_ => panic!("Expected Or expression"),
}
}
#[test]
fn test_fractional_size_parsing() {
let typed = parse_and_typecheck("size > 0.5kb").unwrap();
let expected = Expr::Predicate(Predicate::meta(MetadataPredicate::Filesize(
NumberMatcher::In(Bound::Left(513..)), )));
assert_eq!(typed, expected);
let typed = parse_and_typecheck("size >= 1.5kb").unwrap();
let expected = Expr::Predicate(Predicate::meta(MetadataPredicate::Filesize(
NumberMatcher::In(Bound::Left(1536..)), )));
assert_eq!(typed, expected);
let typed = parse_and_typecheck("size < 2.5mb").unwrap();
let expected = Expr::Predicate(Predicate::meta(MetadataPredicate::Filesize(
NumberMatcher::In(Bound::Right(..2621440)), )));
assert_eq!(typed, expected);
let typed = parse_and_typecheck("size > 0.5KB").unwrap();
let expected = Expr::Predicate(Predicate::meta(MetadataPredicate::Filesize(
NumberMatcher::In(Bound::Left(513..)),
)));
assert_eq!(typed, expected);
let typed = parse_and_typecheck("filesize >= 1.5mb").unwrap();
let expected = Expr::Predicate(Predicate::meta(MetadataPredicate::Filesize(
NumberMatcher::In(Bound::Left(1572864..)), )));
assert_eq!(typed, expected);
}