From 37545f665099f7069a9ae63e1f3f07c3c238d15f Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Wed, 20 Nov 2024 10:54:55 -0600 Subject: [PATCH] requires: support requires check for keyword For example: requires: keyword foo; Will require that Suricata supports the "foo" keyword. --- doc/userguide/rules/meta.rst | 11 ++++++----- rust/src/detect/requires.rs | 25 +++++++++++++++++++++++++ rust/src/feature.rs | 18 ++++++++++++++++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/doc/userguide/rules/meta.rst b/doc/userguide/rules/meta.rst index 1ceb5fe834e0..939b281b6cdb 100644 --- a/doc/userguide/rules/meta.rst +++ b/doc/userguide/rules/meta.rst @@ -216,9 +216,10 @@ requires -------- The ``requires`` keyword allows a rule to require specific Suricata -features to be enabled, or the Suricata version to match an -expression. Rules that do not meet the requirements will by ignored, -and Suricata will not treat them as errors. +features to be enabled, specific keywords to be available, or the +Suricata version to match an expression. Rules that do not meet the +requirements will by ignored, and Suricata will not treat them as +errors. When parsing rules, the parser attempts to process the ``requires`` keywords before others. This allows it to occur after keywords that @@ -228,7 +229,7 @@ still adhere to the basic known formats of Suricata rules. The format is:: - requires: feature geoip, version >= 7.0.0 + requires: feature geoip, version >= 7.0.0, keyword foobar To require multiple features, the feature sub-keyword must be specified multiple times:: @@ -243,7 +244,7 @@ and *or* expressions may expressed with ``|`` like:: requires: version >= 7.0.4 < 8 | >= 8.0.3 -to express that a rules requires version 7.0.4 or greater, but less +to express that a rule requires version 7.0.4 or greater, but less than 8, **OR** greater than or equal to 8.0.3. Which could be useful if a keyword wasn't added until 7.0.4 and the 8.0.3 patch releases, as it would not exist in 8.0.1. diff --git a/rust/src/detect/requires.rs b/rust/src/detect/requires.rs index 2635605d265d..5800c64be594 100644 --- a/rust/src/detect/requires.rs +++ b/rust/src/detect/requires.rs @@ -60,6 +60,9 @@ enum RequiresError { /// An unknown requirement was provided. UnknownRequirement(String), + + /// Suricata does not have support for a required keyword. + MissingKeyword(String), } impl RequiresError { @@ -74,6 +77,7 @@ impl RequiresError { Self::MultipleVersions => "Version may only be specified once\0", Self::Utf8Error => "Requires expression is not valid UTF-8\0", Self::UnknownRequirement(_) => "Unknown requirements\0", + Self::MissingKeyword(_) => "Suricata missing a required keyword\0", }; msg.as_ptr() as *const c_char } @@ -166,8 +170,12 @@ struct RuleRequireVersion { #[derive(Debug, Default, Eq, PartialEq)] struct Requires { + /// Features required to be enabled. pub features: Vec, + /// Rule keywords required to exist. + pub keywords: Vec, + /// The version expression. /// /// - All of the inner most must evaluate to true. @@ -245,6 +253,9 @@ fn parse_requires(mut input: &str) -> Result { parse_version_expression(value).map_err(|_| RequiresError::BadRequires)?; requires.version = versions; } + "keyword" => { + requires.keywords.push(value.trim().to_string()); + } _ => { // Unknown keyword, allow by warn in case we extend // this in the future. @@ -333,6 +344,12 @@ fn check_requires( } } + for keyword in &requires.keywords { + if !crate::feature::has_keyword(keyword) { + return Err(RequiresError::MissingKeyword(keyword.to_string())); + } + } + Ok(()) } @@ -600,6 +617,7 @@ mod test { requires, Requires { features: vec![], + keywords: vec![], version: vec![vec![RuleRequireVersion { op: VersionCompareOp::Gte, version: SuricataVersion { @@ -617,6 +635,7 @@ mod test { requires, Requires { features: vec![], + keywords: vec![], version: vec![vec![RuleRequireVersion { op: VersionCompareOp::Gte, version: SuricataVersion { @@ -634,6 +653,7 @@ mod test { requires, Requires { features: vec!["output::file-store".to_string()], + keywords: vec![], version: vec![vec![RuleRequireVersion { op: VersionCompareOp::Gte, version: SuricataVersion { @@ -651,6 +671,7 @@ mod test { requires, Requires { features: vec!["geoip".to_string()], + keywords: vec![], version: vec![vec![ RuleRequireVersion { op: VersionCompareOp::Gte, @@ -771,6 +792,7 @@ mod test { requires, Requires { features: vec!["true_lua".to_string()], + keywords: vec![], version: vec![vec![RuleRequireVersion { op: VersionCompareOp::Gte, version: SuricataVersion { @@ -830,6 +852,9 @@ mod test { #[test] fn test_requires_keyword() { let requires = parse_requires("keyword true_bar").unwrap(); + assert!(check_requires(&requires, &SuricataVersion::new(8, 0, 0)).is_ok()); + + let requires = parse_requires("keyword bar").unwrap(); assert!(check_requires(&requires, &SuricataVersion::new(8, 0, 0)).is_err()); } } diff --git a/rust/src/feature.rs b/rust/src/feature.rs index abd09669af11..ff4bb736d262 100644 --- a/rust/src/feature.rs +++ b/rust/src/feature.rs @@ -32,6 +32,15 @@ mod mock { pub fn requires(feature: &str) -> bool { return feature.starts_with("true"); } + + /// Check for a keyword returning true if found. + /// + /// This a "mock" variant of `has_keyword` that will return true + /// for any keyword starting with string `true`, and false for + /// anything else. + pub fn has_keyword(keyword: &str) -> bool { + return keyword.starts_with("true"); + } } #[cfg(not(test))] @@ -41,6 +50,7 @@ mod real { extern "C" { fn RequiresFeature(feature: *const c_char) -> bool; + fn SigTableHasKeyword(keyword: *const c_char) -> bool; } /// Check for a feature returning true if found. @@ -51,6 +61,14 @@ mod real { false } } + + pub fn has_keyword(keyword: &str) -> bool { + if let Ok(keyword) = CString::new(keyword) { + unsafe { SigTableHasKeyword(keyword.as_ptr()) } + } else { + false + } + } } #[cfg(not(test))]