diff --git a/crates/diskern-cli/src/main.rs b/crates/diskern-cli/src/main.rs index 86a9339..b2474c2 100644 --- a/crates/diskern-cli/src/main.rs +++ b/crates/diskern-cli/src/main.rs @@ -29,7 +29,7 @@ enum Command { /// Only show findings with this verdict #[arg(long, value_enum)] verdict: Option, - /// Load rules from a JSON file instead of the embedded database + /// Load rules from a JSON file; embedded protected rules still apply #[arg(long, value_name = "FILE")] rules: Option, }, diff --git a/crates/diskern-core/src/rules.rs b/crates/diskern-core/src/rules.rs index ef4cd93..25a0b2e 100644 --- a/crates/diskern-core/src/rules.rs +++ b/crates/diskern-core/src/rules.rs @@ -81,6 +81,15 @@ impl RulesDb { /// for a rule that can never match. pub fn validate(&self) -> Result<()> { for rule in &self.rules { + // An empty pattern list compiles cleanly into a GlobSet that + // matches nothing, so it fails the same way a bad glob does — + // silently, and only for the rule the author cared about. + if rule.patterns.is_empty() { + return Err(GenomeError::Rules(format!( + "rule '{}' has no patterns, so it can never match", + rule.id + ))); + } for pattern in &rule.patterns { build_glob(pattern).map_err(|error| { GenomeError::Rules(format!( @@ -274,6 +283,26 @@ mod tests { assert!(error.to_string().contains("invalid glob pattern")); } + #[test] + fn rules_without_patterns_are_rejected() { + let db = RulesDb::new( + 1, + vec![Rule { + id: "empty".into(), + patterns: vec![], + category: Category::Unknown, + verdict: Verdict::Review, + description: "rule with no patterns".into(), + }], + ); + + let error = db + .validate() + .expect_err("a rule with no patterns must fail validation"); + assert!(error.to_string().contains("empty")); + assert!(error.to_string().contains("can never match")); + } + #[test] fn embedded_protected_rules_precede_external_rules() { let external = RulesDb::new(