]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
requires: support requires check for keyword
authorJason Ish <jason.ish@oisf.net>
Wed, 20 Nov 2024 16:54:55 +0000 (10:54 -0600)
committerVictor Julien <victor@inliniac.net>
Mon, 2 Dec 2024 11:33:36 +0000 (12:33 +0100)
For example:

    requires: keyword foo;

Will require that Suricata supports the "foo" keyword.

Ticket: #7403

doc/userguide/rules/meta.rst
rust/src/detect/requires.rs
rust/src/feature.rs

index 89a98972e7ae062a64c1e27ca7639322685522dd..8312c91de091afb20896e7378f4ecf04d7d0fe60 100644 (file)
@@ -218,9 +218,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 be ignored, and Suricata will not treat them as
+errors.
 
 Requirements that follow the valid format of ``<keyword>
 <expression>`` but are not known to Suricata are allowed for future
@@ -235,7 +236,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::
@@ -250,7 +251,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.
index 2635605d265de76eada3d4665249aa9a11e0585e..5800c64be59463ca4f628536b2af0b246348bd46 100644 (file)
@@ -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<String>,
 
+    /// Rule keywords required to exist.
+    pub keywords: Vec<String>,
+
     /// The version expression.
     ///
     /// - All of the inner most must evaluate to true.
@@ -245,6 +253,9 @@ fn parse_requires(mut input: &str) -> Result<Requires, RequiresError> {
                     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());
     }
 }
index abd09669af11b678fa20c159fb815c99819c6ebe..ff4bb736d262b0f84437427f5cd288a955d77d8f 100644 (file)
@@ -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))]