use super::super::{FixPatch, LintRule, LintWarning, err, key, safe_fix, warning};
use super::detection::lint_logsource;
pub(crate) fn lint_filter_rule(m: &yaml_serde::Mapping, warnings: &mut Vec<LintWarning>) {
let Some(filter_val) = m.get(key("filter")) else {
warnings.push(err(
LintRule::MissingFilter,
"missing required field 'filter'",
"/filter",
));
return;
};
let Some(filter) = filter_val.as_mapping() else {
warnings.push(err(
LintRule::MissingFilter,
"'filter' must be a mapping",
"/filter",
));
return;
};
if let Some(rules_val) = filter.get(key("rules")) {
match rules_val {
yaml_serde::Value::Sequence(_) => {}
yaml_serde::Value::String(s) if s.eq_ignore_ascii_case("any") => {}
yaml_serde::Value::String(_) => {}
_ => {
warnings.push(err(
LintRule::MissingFilterRules,
"filter.rules must be a sequence of rule IDs, a single rule ID string, or 'any'",
"/filter/rules",
));
}
}
}
if !filter.contains_key(key("selection")) {
warnings.push(err(
LintRule::MissingFilterSelection,
"missing required field 'filter.selection'",
"/filter/selection",
));
}
if !filter.contains_key(key("condition")) {
warnings.push(err(
LintRule::MissingFilterCondition,
"missing required field 'filter.condition'",
"/filter/condition",
));
}
if !m.contains_key(key("logsource")) {
warnings.push(err(
LintRule::MissingFilterLogsource,
"missing required field 'logsource' for filter rule",
"/logsource",
));
} else {
lint_logsource(m, warnings);
}
if m.contains_key(key("level")) {
let mut w = warning(
LintRule::FilterHasLevel,
"filter rules should not have a 'level' field",
"/level",
);
w.fix = safe_fix(
"remove 'level' from filter rule",
vec![FixPatch::Remove {
path: "/level".into(),
}],
);
warnings.push(w);
}
if m.contains_key(key("status")) {
let mut w = warning(
LintRule::FilterHasStatus,
"filter rules should not have a 'status' field",
"/status",
);
w.fix = safe_fix(
"remove 'status' from filter rule",
vec![FixPatch::Remove {
path: "/status".into(),
}],
);
warnings.push(w);
}
}
#[cfg(test)]
mod tests {
use super::super::super::{Fix, FixPatch, LintRule, LintWarning, Severity, lint_yaml_value};
fn yaml_value(yaml: &str) -> yaml_serde::Value {
yaml_serde::from_str(yaml).unwrap()
}
fn lint(yaml: &str) -> Vec<LintWarning> {
lint_yaml_value(&yaml_value(yaml))
}
fn has_rule(warnings: &[LintWarning], rule: LintRule) -> bool {
warnings.iter().any(|w| w.rule == rule)
}
fn find_fix(warnings: &[LintWarning], rule: LintRule) -> Option<&Fix> {
warnings
.iter()
.find(|w| w.rule == rule)
.and_then(|w| w.fix.as_ref())
}
fn fix_summary(fix: &Fix) -> String {
use std::fmt::Write;
let mut s = String::new();
writeln!(s, "title: {}", fix.title).unwrap();
writeln!(s, "disposition: {:?}", fix.disposition).unwrap();
for (i, p) in fix.patches.iter().enumerate() {
match p {
FixPatch::ReplaceValue { path, new_value } => {
writeln!(s, "patch[{i}]: ReplaceValue {path} -> {new_value}").unwrap();
}
FixPatch::ReplaceKey { path, new_key } => {
writeln!(s, "patch[{i}]: ReplaceKey {path} -> {new_key}").unwrap();
}
FixPatch::Remove { path } => {
writeln!(s, "patch[{i}]: Remove {path}").unwrap();
}
}
}
s
}
#[test]
fn valid_filter_no_errors() {
let w = lint(
r#"
title: Filter Admin
logsource:
category: process_creation
product: windows
filter:
rules:
- 929a690e-bef0-4204-a928-ef5e620d6fcc
selection:
User|startswith: 'adm_'
condition: selection
"#,
);
let errors: Vec<_> = w.iter().filter(|w| w.severity == Severity::Error).collect();
assert!(errors.is_empty(), "unexpected errors: {errors:?}");
}
#[test]
fn filter_without_rules_is_valid() {
let w = lint(
r#"
title: Test
logsource:
category: test
filter:
selection:
User: admin
condition: selection
"#,
);
assert!(!has_rule(&w, LintRule::MissingFilterRules));
}
#[test]
fn filter_rules_invalid_type() {
let w = lint(
r#"
title: Test
logsource:
category: test
filter:
rules: 123
selection:
User: admin
condition: selection
"#,
);
assert!(has_rule(&w, LintRule::MissingFilterRules));
}
#[test]
fn filter_rules_any_string_is_valid() {
let w = lint(
r#"
title: Test
logsource:
category: test
filter:
rules: any
selection:
User: admin
condition: selection
"#,
);
assert!(!has_rule(&w, LintRule::MissingFilterRules));
}
#[test]
fn filter_rules_empty_sequence_is_valid() {
let w = lint(
r#"
title: Test
logsource:
category: test
filter:
rules: []
selection:
User: admin
condition: selection
"#,
);
assert!(!has_rule(&w, LintRule::EmptyFilterRules));
}
#[test]
fn missing_filter_selection() {
let w = lint(
r#"
title: Test
logsource:
category: test
filter:
rules:
- some-rule
condition: selection
"#,
);
assert!(has_rule(&w, LintRule::MissingFilterSelection));
}
#[test]
fn missing_filter_condition() {
let w = lint(
r#"
title: Test
logsource:
category: test
filter:
rules:
- some-rule
selection:
User: admin
"#,
);
assert!(has_rule(&w, LintRule::MissingFilterCondition));
}
#[test]
fn filter_has_level_warning() {
let w = lint(
r#"
title: Test
logsource:
category: test
level: high
filter:
rules:
- some-rule
selection:
User: admin
condition: selection
"#,
);
assert!(has_rule(&w, LintRule::FilterHasLevel));
}
#[test]
fn filter_has_status_warning() {
let w = lint(
r#"
title: Test
logsource:
category: test
status: test
filter:
rules:
- some-rule
selection:
User: admin
condition: selection
"#,
);
assert!(has_rule(&w, LintRule::FilterHasStatus));
}
#[test]
fn missing_filter_logsource() {
let w = lint(
r#"
title: Test
filter:
rules:
- some-rule
selection:
User: admin
condition: selection
"#,
);
assert!(has_rule(&w, LintRule::MissingFilterLogsource));
}
#[test]
fn fix_filter_has_level() {
let w = lint(
r#"
title: Test
logsource:
category: test
level: high
filter:
rules:
- rule1
selection:
User: admin
condition: selection
"#,
);
let fix = find_fix(&w, LintRule::FilterHasLevel).expect("should have fix");
insta::assert_snapshot!(fix_summary(fix), @r"
title: remove 'level' from filter rule
disposition: Safe
patch[0]: Remove /level
");
}
#[test]
fn fix_filter_has_status() {
let w = lint(
r#"
title: Test
logsource:
category: test
status: test
filter:
rules:
- rule1
selection:
User: admin
condition: selection
"#,
);
let fix = find_fix(&w, LintRule::FilterHasStatus).expect("should have fix");
insta::assert_snapshot!(fix_summary(fix), @r"
title: remove 'status' from filter rule
disposition: Safe
patch[0]: Remove /status
");
}
}