]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
requires: add requires keyword
authorJason Ish <jason.ish@oisf.net>
Tue, 28 Nov 2023 21:35:09 +0000 (15:35 -0600)
committerVictor Julien <victor@inliniac.net>
Tue, 19 Dec 2023 08:51:22 +0000 (09:51 +0100)
Add a new rule keyword "requires" that allows a rule to require specific
Suricata versions and/or Suricata features to be enabled.

Example:

  requires: feature geoip, version >= 7.0.0, version < 8;
  requires: version >= 7.0.3 < 8
  requires: version >= 7.0.3 < 8 | >= 8.0.3

Feature: #5972

Co-authored-by: Philippe Antoine <pantoine@oisf.net>
13 files changed:
doc/userguide/rules/meta.rst
rust/src/detect/mod.rs
rust/src/detect/requires.rs [new file with mode: 0644]
rust/src/log.rs
src/Makefile.am
src/detect-engine-loader.c
src/detect-engine-register.c
src/detect-engine-register.h
src/detect-engine.c
src/detect-parse.c
src/detect-requires.c [new file with mode: 0644]
src/detect-requires.h [new file with mode: 0644]
src/detect.h

index 06e5040e73a50124871baa0e9ea3b2c510d49f63..0e888add697ae035a6be7a5254b8d93e026e0a36 100644 (file)
@@ -211,3 +211,50 @@ The format is::
 If the value is src_ip then the source IP in the generated event (src_ip
 field in JSON) is the target of the attack. If target is set to dest_ip
 then the target is the destination IP in the generated event.
+
+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.
+
+When parsing rules, the parser attempts to process the ``requires``
+keywords before others. This allows it to occur after keywords that
+may only be present in specific versions of Suricata, as specified by
+the ``requires`` statement. However, the keywords preceding it must
+still adhere to the basic known formats of Suricata rules.
+
+The format is::
+
+   requires: feature geoip, version >= 7.0.0
+
+To require multiple features, the feature sub-keyword must be
+specified multiple times::
+
+   requires: feature geoip, feature lua
+
+Alternatively, *and* expressions may be expressed like::
+
+   requires: version >= 7.0.4 < 8
+
+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
+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.
+
+This can be extended to multiple release branches::
+
+   requires: version >= 7.0.10 < 8 | >= 8.0.5 < 9 | >= 9.0.3
+
+If no *minor* or *patch* version component is provided, it will
+default to 0.
+
+The ``version`` may only be specified once, if specified more than
+once the rule will log an error and not be loaded.
+
index 41c7ff2455bd9dab59e9df76a9022e745b5ec440..d33c9ae7fabf612a650e24953a095a4f56387fe4 100644 (file)
@@ -24,3 +24,4 @@ pub mod parser;
 pub mod stream_size;
 pub mod uint;
 pub mod uri;
+pub mod requires;
diff --git a/rust/src/detect/requires.rs b/rust/src/detect/requires.rs
new file mode 100644 (file)
index 0000000..e9e1aca
--- /dev/null
@@ -0,0 +1,805 @@
+/* Copyright (C) 2023 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+use std::collections::{HashSet, VecDeque};
+use std::{cmp::Ordering, ffi::CStr};
+
+// std::ffi::{c_char, c_int} is recommended these days, but requires
+// Rust 1.64.0.
+use std::os::raw::{c_char, c_int};
+
+use nom7::bytes::complete::take_while;
+use nom7::combinator::map;
+use nom7::multi::{many1, separated_list1};
+use nom7::sequence::tuple;
+use nom7::{
+    branch::alt,
+    bytes::complete::{tag, take_till},
+    character::complete::{char, multispace0},
+    combinator::map_res,
+    sequence::preceded,
+    IResult,
+};
+
+#[derive(Debug, Eq, PartialEq)]
+enum RequiresError {
+    /// Suricata is greater than the required version.
+    VersionGt,
+
+    /// Suricata is less than the required version.
+    VersionLt(SuricataVersion),
+
+    /// The running Suricata is missing a required feature.
+    MissingFeature(String),
+
+    /// The Suricata version, of Suricata itself is bad and failed to parse.
+    BadSuricataVersion,
+
+    /// The requires expression is bad and failed to parse.
+    BadRequires,
+
+    /// MultipleVersions
+    MultipleVersions,
+
+    /// Passed in requirements not a valid UTF-8 string.
+    Utf8Error,
+}
+
+impl RequiresError {
+    /// Return a pointer to a C compatible constant error message.
+    const fn c_errmsg(&self) -> *const c_char {
+        let msg = match self {
+            Self::VersionGt => "Suricata version greater than required\0",
+            Self::VersionLt(_) => "Suricata version less than required\0",
+            Self::MissingFeature(_) => "Suricata missing a required feature\0",
+            Self::BadSuricataVersion => "Failed to parse running Suricata version\0",
+            Self::BadRequires => "Failed to parse requires expression\0",
+            Self::MultipleVersions => "Version may only be specified once\0",
+            Self::Utf8Error => "Requires expression is not valid UTF-8\0",
+        };
+        msg.as_ptr() as *const c_char
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+enum VersionCompareOp {
+    Gt,
+    Gte,
+    Lt,
+    Lte,
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+struct SuricataVersion {
+    major: u8,
+    minor: u8,
+    patch: u8,
+}
+
+impl PartialOrd for SuricataVersion {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for SuricataVersion {
+    fn cmp(&self, other: &Self) -> Ordering {
+        match self.major.cmp(&other.major) {
+            Ordering::Equal => match self.minor.cmp(&other.minor) {
+                Ordering::Equal => self.patch.cmp(&other.patch),
+                other => other,
+            },
+            other => other,
+        }
+    }
+}
+
+impl std::fmt::Display for SuricataVersion {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
+    }
+}
+
+impl SuricataVersion {
+    fn new(major: u8, minor: u8, patch: u8) -> Self {
+        Self {
+            major,
+            minor,
+            patch,
+        }
+    }
+}
+
+/// Parse a version expression.
+///
+/// Parse into a version expression into a nested array, for example:
+///
+///    version: >= 7.0.3 < 8 | >= 8.0.3
+///
+/// would result in something like:
+///
+/// [
+///     [{op: gte, version: 7.0.3}, {op:lt, version: 8}],
+///     [{op: gte, version: 8.0.3}],
+/// ]
+fn parse_version_expression(input: &str) -> IResult<&str, Vec<Vec<RuleRequireVersion>>> {
+    let sep = preceded(multispace0, tag("|"));
+    let inner_parser = many1(tuple((parse_op, parse_version)));
+    let (input, versions) = separated_list1(sep, inner_parser)(input)?;
+
+    let versions = versions
+        .into_iter()
+        .map(|versions| {
+            versions
+                .into_iter()
+                .map(|(op, version)| RuleRequireVersion { op, version })
+                .collect()
+        })
+        .collect();
+
+    Ok((input, versions))
+}
+
+#[derive(Debug, Eq, PartialEq)]
+struct RuleRequireVersion {
+    pub op: VersionCompareOp,
+    pub version: SuricataVersion,
+}
+
+#[derive(Debug, Default, Eq, PartialEq)]
+struct Requires {
+    pub features: Vec<String>,
+
+    /// The version expression.
+    ///
+    /// - All of the inner most must evaluate to true.
+    /// - To pass, any of the outer must be true.
+    pub version: Vec<Vec<RuleRequireVersion>>,
+}
+
+fn parse_op(input: &str) -> IResult<&str, VersionCompareOp> {
+    preceded(
+        multispace0,
+        alt((
+            map(tag(">="), |_| VersionCompareOp::Gte),
+            map(tag(">"), |_| VersionCompareOp::Gt),
+            map(tag("<="), |_| VersionCompareOp::Lte),
+            map(tag("<"), |_| VersionCompareOp::Lt),
+        )),
+    )(input)
+}
+
+/// Parse the next part of the version.
+///
+/// That is all chars up to eof, or the next '.' or '-'.
+fn parse_next_version_part(input: &str) -> IResult<&str, u8> {
+    map_res(
+        take_till(|c| c == '.' || c == '-' || c == ' '),
+        |s: &str| s.parse::<u8>(),
+    )(input)
+}
+
+/// Parse a version string into a SuricataVersion.
+fn parse_version(input: &str) -> IResult<&str, SuricataVersion> {
+    let (input, major) = preceded(multispace0, parse_next_version_part)(input)?;
+    let (input, minor) = if input.is_empty() || input.starts_with(' ') {
+        (input, 0)
+    } else {
+        preceded(char('.'), parse_next_version_part)(input)?
+    };
+    let (input, patch) = if input.is_empty() || input.starts_with(' ') {
+        (input, 0)
+    } else {
+        preceded(char('.'), parse_next_version_part)(input)?
+    };
+
+    Ok((input, SuricataVersion::new(major, minor, patch)))
+}
+
+fn parse_key_value(input: &str) -> IResult<&str, (&str, &str)> {
+    // Parse the keyword, any sequence of characters, numbers or "-" or "_".
+    let (input, key) = preceded(
+        multispace0,
+        take_while(|c: char| c.is_alphanumeric() || c == '-' || c == '_'),
+    )(input)?;
+    let (input, value) = preceded(multispace0, take_till(|c: char| c == ','))(input)?;
+    Ok((input, (key, value)))
+}
+
+fn parse_requires(mut input: &str) -> Result<Requires, RequiresError> {
+    let mut requires = Requires::default();
+
+    while !input.is_empty() {
+        let (rest, (keyword, value)) =
+            parse_key_value(input).map_err(|_| RequiresError::BadRequires)?;
+        match keyword {
+            "feature" => {
+                requires.features.push(value.trim().to_string());
+            }
+            "version" => {
+                if !requires.version.is_empty() {
+                    return Err(RequiresError::MultipleVersions);
+                }
+                let (_, versions) =
+                    parse_version_expression(value).map_err(|_| RequiresError::BadRequires)?;
+                requires.version = versions;
+            }
+            _ => {
+                // Unknown keyword, allow by warn in case we extend
+                // this in the future.
+                SCLogWarning!("Unknown requires keyword: {}", keyword);
+            }
+        }
+
+        // No consume any remaining ',' or whitespace.
+        input = rest.trim_start_matches(|c: char| c == ',' || c.is_whitespace());
+    }
+    Ok(requires)
+}
+
+fn parse_suricata_version(version: &CStr) -> Result<SuricataVersion, *const c_char> {
+    let version = version
+        .to_str()
+        .map_err(|_| RequiresError::BadSuricataVersion.c_errmsg())?;
+    let (_, version) =
+        parse_version(version).map_err(|_| RequiresError::BadSuricataVersion.c_errmsg())?;
+    Ok(version)
+}
+
+fn check_version(
+    version: &RuleRequireVersion, suricata_version: &SuricataVersion,
+) -> Result<(), RequiresError> {
+    match version.op {
+        VersionCompareOp::Gt => {
+            if suricata_version <= &version.version {
+                return Err(RequiresError::VersionLt(version.version.clone()));
+            }
+        }
+        VersionCompareOp::Gte => {
+            if suricata_version < &version.version {
+                return Err(RequiresError::VersionLt(version.version.clone()));
+            }
+        }
+        VersionCompareOp::Lt => {
+            if suricata_version >= &version.version {
+                return Err(RequiresError::VersionGt);
+            }
+        }
+        VersionCompareOp::Lte => {
+            if suricata_version > &version.version {
+                return Err(RequiresError::VersionGt);
+            }
+        }
+    }
+    Ok(())
+}
+
+fn check_requires(
+    requires: &Requires, suricata_version: &SuricataVersion,
+) -> Result<(), RequiresError> {
+    if !requires.version.is_empty() {
+        let mut errs = VecDeque::new();
+        let mut ok = 0;
+        for or_versions in &requires.version {
+            let mut err = None;
+            for version in or_versions {
+                if let Err(_err) = check_version(version, suricata_version) {
+                    err = Some(_err);
+                    break;
+                }
+            }
+            if let Some(err) = err {
+                errs.push_back(err);
+            } else {
+                ok += 1;
+            }
+        }
+        if ok == 0 {
+            return Err(errs.pop_front().unwrap());
+        }
+    }
+
+    for feature in &requires.features {
+        if !crate::feature::requires(feature) {
+            return Err(RequiresError::MissingFeature(feature.to_string()));
+        }
+    }
+
+    Ok(())
+}
+
+/// Status object to hold required features and the latest version of
+/// Suricata required.
+///
+/// Full qualified name as it is exposed to C.
+#[derive(Debug, Default)]
+pub struct SCDetectRequiresStatus {
+    min_version: Option<SuricataVersion>,
+    features: HashSet<String>,
+
+    /// Number of rules that didn't meet a feature.
+    feature_count: u64,
+
+    /// Number of rules where the Suricata version wasn't new enough.
+    lt_count: u64,
+
+    /// Number of rules where the Suricata version was too new.
+    gt_count: u64,
+}
+
+#[no_mangle]
+pub extern "C" fn SCDetectRequiresStatusNew() -> *mut SCDetectRequiresStatus {
+    Box::into_raw(Box::default())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCDetectRequiresStatusFree(status: *mut SCDetectRequiresStatus) {
+    if !status.is_null() {
+        std::mem::drop(Box::from_raw(status));
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCDetectRequiresStatusLog(
+    status: &mut SCDetectRequiresStatus, suricata_version: *const c_char, tenant_id: u32,
+) {
+    let suricata_version = CStr::from_ptr(suricata_version)
+        .to_str()
+        .unwrap_or("<unknown>");
+
+    let mut parts = vec![];
+    if status.lt_count > 0 {
+        let min_version = status
+            .min_version
+            .as_ref()
+            .map(|v| v.to_string())
+            .unwrap_or_else(|| "<unknown>".to_string());
+        let msg = format!(
+            "{} {} skipped because the running Suricata version {} is less than {}",
+            status.lt_count,
+            if status.lt_count > 1 {
+                "rules were"
+            } else {
+                "rule was"
+            },
+            suricata_version,
+            &min_version
+        );
+        parts.push(msg);
+    }
+    if status.gt_count > 0 {
+        let msg = format!(
+            "{} {} for an older version Suricata",
+            status.gt_count,
+            if status.gt_count > 1 {
+                "rules were skipped as they are"
+            } else {
+                "rule was skipped as it is"
+            }
+        );
+        parts.push(msg);
+    }
+    if status.feature_count > 0 {
+        let features = status
+            .features
+            .iter()
+            .map(|f| f.to_string())
+            .collect::<Vec<String>>()
+            .join(", ");
+        let msg = format!(
+            "{}{} {} skipped because the running Suricata version does not have feature{}: [{}]",
+            if tenant_id > 0 {
+                format!("tenant id: {}  ", tenant_id)
+            } else {
+                String::new()
+            },
+            status.feature_count,
+            if status.feature_count > 1 {
+                "rules were"
+            } else {
+                "rule was"
+            },
+            if status.feature_count > 1 { "s" } else { "" },
+            &features
+        );
+        parts.push(msg);
+    }
+
+    let msg = parts.join("; ");
+
+    if status.lt_count > 0 {
+        SCLogNotice!("{}", &msg);
+    } else if status.gt_count > 0 || status.feature_count > 0 {
+        SCLogInfo!("{}", &msg);
+    }
+}
+
+/// Parse a "requires" rule option.
+///
+/// Return values:
+///   *  0 - OK, rule should continue loading
+///   * -1 - Error parsing the requires content
+///   * -4 - Requirements not met, don't continue loading the rule, this
+///          value is chosen so it can be passed back to the options parser
+///          as its treated as a non-fatal silent error.
+#[no_mangle]
+pub unsafe extern "C" fn SCDetectCheckRequires(
+    requires: *const c_char, suricata_version_string: *const c_char, errstr: *mut *const c_char,
+    status: &mut SCDetectRequiresStatus,
+) -> c_int {
+    // First parse the running Suricata version.
+    let suricata_version = match parse_suricata_version(CStr::from_ptr(suricata_version_string)) {
+        Ok(version) => version,
+        Err(err) => {
+            *errstr = err;
+            return -1;
+        }
+    };
+
+    let requires = match CStr::from_ptr(requires)
+        .to_str()
+        .map_err(|_| RequiresError::Utf8Error)
+        .and_then(parse_requires)
+    {
+        Ok(requires) => requires,
+        Err(err) => {
+            *errstr = err.c_errmsg();
+            return -1;
+        }
+    };
+
+    match check_requires(&requires, &suricata_version) {
+        Ok(()) => 0,
+        Err(err) => {
+            match &err {
+                RequiresError::VersionLt(version) => {
+                    if let Some(min_version) = &status.min_version {
+                        if version > min_version {
+                            status.min_version = Some(version.clone());
+                        }
+                    } else {
+                        status.min_version = Some(version.clone());
+                    }
+                    status.lt_count += 1;
+                }
+                RequiresError::MissingFeature(feature) => {
+                    status.features.insert(feature.to_string());
+                    status.feature_count += 1;
+                }
+                RequiresError::VersionGt => {
+                    status.gt_count += 1;
+                }
+                _ => {}
+            }
+            *errstr = err.c_errmsg();
+            return -4;
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_suricata_version() {
+        // 7.1.1 < 7.1.2
+        assert!(SuricataVersion::new(7, 1, 1) < SuricataVersion::new(7, 1, 2));
+
+        // 7.1.1 <= 7.1.2
+        assert!(SuricataVersion::new(7, 1, 1) <= SuricataVersion::new(7, 1, 2));
+
+        // 7.1.1 <= 7.1.1
+        assert!(SuricataVersion::new(7, 1, 1) <= SuricataVersion::new(7, 1, 1));
+
+        // NOT 7.1.1 < 7.1.1
+        assert!(SuricataVersion::new(7, 1, 1) >= SuricataVersion::new(7, 1, 1));
+
+        // 7.3.1 < 7.22.1
+        assert!(SuricataVersion::new(7, 3, 1) < SuricataVersion::new(7, 22, 1));
+
+        // 7.22.1 >= 7.3.4
+        assert!(SuricataVersion::new(7, 22, 1) >= SuricataVersion::new(7, 3, 4));
+    }
+
+    #[test]
+    fn test_parse_op() {
+        assert_eq!(parse_op(">").unwrap().1, VersionCompareOp::Gt);
+        assert_eq!(parse_op(">=").unwrap().1, VersionCompareOp::Gte);
+        assert_eq!(parse_op("<").unwrap().1, VersionCompareOp::Lt);
+        assert_eq!(parse_op("<=").unwrap().1, VersionCompareOp::Lte);
+
+        assert!(parse_op("=").is_err());
+    }
+
+    #[test]
+    fn test_parse_version() {
+        assert_eq!(
+            parse_version("7").unwrap().1,
+            SuricataVersion {
+                major: 7,
+                minor: 0,
+                patch: 0,
+            }
+        );
+
+        assert_eq!(
+            parse_version("7.1").unwrap().1,
+            SuricataVersion {
+                major: 7,
+                minor: 1,
+                patch: 0,
+            }
+        );
+
+        assert_eq!(
+            parse_version("7.1.2").unwrap().1,
+            SuricataVersion {
+                major: 7,
+                minor: 1,
+                patch: 2,
+            }
+        );
+
+        // Suricata pre-releases will have a suffix starting with a
+        // '-', so make sure we accept those versions as well.
+        assert_eq!(
+            parse_version("8.0.0-dev").unwrap().1,
+            SuricataVersion {
+                major: 8,
+                minor: 0,
+                patch: 0,
+            }
+        );
+
+        assert!(parse_version("7.1.2a").is_err());
+        assert!(parse_version("a").is_err());
+        assert!(parse_version("777").is_err());
+        assert!(parse_version("product-1").is_err());
+    }
+
+    #[test]
+    fn test_parse_requires() {
+        let requires = parse_requires("  feature geoip").unwrap();
+        assert_eq!(&requires.features[0], "geoip");
+
+        let requires = parse_requires("  feature geoip,    feature    lua  ").unwrap();
+        assert_eq!(&requires.features[0], "geoip");
+        assert_eq!(&requires.features[1], "lua");
+
+        let requires = parse_requires("version >=7").unwrap();
+        assert_eq!(
+            requires,
+            Requires {
+                features: vec![],
+                version: vec![vec![RuleRequireVersion {
+                    op: VersionCompareOp::Gte,
+                    version: SuricataVersion {
+                        major: 7,
+                        minor: 0,
+                        patch: 0,
+                    }
+                }]],
+            }
+        );
+
+        let requires = parse_requires("version >= 7.1").unwrap();
+        assert_eq!(
+            requires,
+            Requires {
+                features: vec![],
+                version: vec![vec![RuleRequireVersion {
+                    op: VersionCompareOp::Gte,
+                    version: SuricataVersion {
+                        major: 7,
+                        minor: 1,
+                        patch: 0,
+                    }
+                }]],
+            }
+        );
+
+        let requires = parse_requires("feature output::file-store, version >= 7.1.2").unwrap();
+        assert_eq!(
+            requires,
+            Requires {
+                features: vec!["output::file-store".to_string()],
+                version: vec![vec![RuleRequireVersion {
+                    op: VersionCompareOp::Gte,
+                    version: SuricataVersion {
+                        major: 7,
+                        minor: 1,
+                        patch: 2,
+                    }
+                }]],
+            }
+        );
+
+        let requires = parse_requires("feature geoip, version >= 7.1.2 < 8").unwrap();
+        assert_eq!(
+            requires,
+            Requires {
+                features: vec!["geoip".to_string()],
+                version: vec![vec![
+                    RuleRequireVersion {
+                        op: VersionCompareOp::Gte,
+                        version: SuricataVersion {
+                            major: 7,
+                            minor: 1,
+                            patch: 2,
+                        },
+                    },
+                    RuleRequireVersion {
+                        op: VersionCompareOp::Lt,
+                        version: SuricataVersion {
+                            major: 8,
+                            minor: 0,
+                            patch: 0,
+                        }
+                    }
+                ]],
+            }
+        );
+    }
+
+    #[test]
+    fn test_check_requires() {
+        // Have 7.0.4, require >= 8.
+        let suricata_version = SuricataVersion::new(7, 0, 4);
+        let requires = parse_requires("version >= 8").unwrap();
+        assert_eq!(
+            check_requires(&requires, &suricata_version),
+            Err(RequiresError::VersionLt(SuricataVersion {
+                major: 8,
+                minor: 0,
+                patch: 0,
+            })),
+        );
+
+        // Have 7.0.4, require 7.0.3.
+        let suricata_version = SuricataVersion::new(7, 0, 4);
+        let requires = parse_requires("version >= 7.0.3").unwrap();
+        assert_eq!(check_requires(&requires, &suricata_version), Ok(()));
+
+        // Have 8.0.0, require >= 7.0.0 and < 8.0
+        let suricata_version = SuricataVersion::new(8, 0, 0);
+        let requires = parse_requires("version >= 7.0.0 < 8").unwrap();
+        assert_eq!(
+            check_requires(&requires, &suricata_version),
+            Err(RequiresError::VersionGt)
+        );
+
+        // Have 8.0.0, require >= 7.0.0 and < 9.0
+        let suricata_version = SuricataVersion::new(8, 0, 0);
+        let requires = parse_requires("version >= 7.0.0 < 9").unwrap();
+        assert_eq!(check_requires(&requires, &suricata_version), Ok(()));
+
+        // Require feature foobar.
+        let suricata_version = SuricataVersion::new(8, 0, 0);
+        let requires = parse_requires("feature foobar").unwrap();
+        assert_eq!(
+            check_requires(&requires, &suricata_version),
+            Err(RequiresError::MissingFeature("foobar".to_string()))
+        );
+
+        // Require feature foobar, but this time we have the feature.
+        let suricata_version = SuricataVersion::new(8, 0, 0);
+        let requires = parse_requires("feature true_foobar").unwrap();
+        assert_eq!(check_requires(&requires, &suricata_version), Ok(()));
+
+        let suricata_version = SuricataVersion::new(8, 0, 1);
+        let requires = parse_requires("version >= 7.0.3 < 8").unwrap();
+        assert!(check_requires(&requires, &suricata_version).is_err());
+
+        let suricata_version = SuricataVersion::new(7, 0, 1);
+        let requires = parse_requires("version >= 7.0.3 < 8").unwrap();
+        assert!(check_requires(&requires, &suricata_version).is_err());
+
+        let suricata_version = SuricataVersion::new(7, 0, 3);
+        let requires = parse_requires("version >= 7.0.3 < 8").unwrap();
+        assert!(check_requires(&requires, &suricata_version).is_ok());
+
+        let suricata_version = SuricataVersion::new(8, 0, 3);
+        let requires = parse_requires("version >= 7.0.3 < 8 | >= 8.0.3").unwrap();
+        assert!(check_requires(&requires, &suricata_version).is_ok());
+
+        let suricata_version = SuricataVersion::new(8, 0, 2);
+        let requires = parse_requires("version >= 7.0.3 < 8 | >= 8.0.3").unwrap();
+        assert!(check_requires(&requires, &suricata_version).is_err());
+
+        let suricata_version = SuricataVersion::new(7, 0, 2);
+        let requires = parse_requires("version >= 7.0.3 < 8 | >= 8.0.3").unwrap();
+        assert!(check_requires(&requires, &suricata_version).is_err());
+
+        let suricata_version = SuricataVersion::new(7, 0, 3);
+        let requires = parse_requires("version >= 7.0.3 < 8 | >= 8.0.3").unwrap();
+        assert!(check_requires(&requires, &suricata_version).is_ok());
+
+        // Example of something that requires a fix/feature that was
+        // implemented in 7.0.5, 8.0.4, 9.0.3.
+        let requires = parse_requires("version >= 7.0.5 < 8 | >= 8.0.4 < 9 | >= 9.0.3").unwrap();
+        assert!(check_requires(&requires, &SuricataVersion::new(6, 0, 0)).is_err());
+        assert!(check_requires(&requires, &SuricataVersion::new(7, 0, 4)).is_err());
+        assert!(check_requires(&requires, &SuricataVersion::new(7, 0, 5)).is_ok());
+        assert!(check_requires(&requires, &SuricataVersion::new(8, 0, 3)).is_err());
+        assert!(check_requires(&requires, &SuricataVersion::new(8, 0, 4)).is_ok());
+        assert!(check_requires(&requires, &SuricataVersion::new(9, 0, 2)).is_err());
+        assert!(check_requires(&requires, &SuricataVersion::new(9, 0, 3)).is_ok());
+        assert!(check_requires(&requires, &SuricataVersion::new(10, 0, 0)).is_ok());
+
+        let requires = parse_requires("version >= 8 < 9").unwrap();
+        assert!(check_requires(&requires, &SuricataVersion::new(6, 0, 0)).is_err());
+        assert!(check_requires(&requires, &SuricataVersion::new(7, 0, 0)).is_err());
+        assert!(check_requires(&requires, &SuricataVersion::new(8, 0, 0)).is_ok());
+        assert!(check_requires(&requires, &SuricataVersion::new(9, 0, 0)).is_err());
+
+        // Unknown keyword.
+        let requires = parse_requires("feature lua, foo bar, version >= 7.0.3").unwrap();
+        assert_eq!(
+            requires,
+            Requires {
+                features: vec!["lua".to_string()],
+                version: vec![vec![RuleRequireVersion {
+                    op: VersionCompareOp::Gte,
+                    version: SuricataVersion {
+                        major: 7,
+                        minor: 0,
+                        patch: 3,
+                    }
+                }]],
+            }
+        );
+    }
+
+    #[test]
+    fn test_parse_version_expression() {
+        let version_str = ">= 7.0.3 < 8 | >= 8.0.3";
+        let (rest, versions) = parse_version_expression(version_str).unwrap();
+        assert!(rest.is_empty());
+        assert_eq!(
+            versions,
+            vec![
+                vec![
+                    RuleRequireVersion {
+                        op: VersionCompareOp::Gte,
+                        version: SuricataVersion {
+                            major: 7,
+                            minor: 0,
+                            patch: 3,
+                        }
+                    },
+                    RuleRequireVersion {
+                        op: VersionCompareOp::Lt,
+                        version: SuricataVersion {
+                            major: 8,
+                            minor: 0,
+                            patch: 0,
+                        }
+                    },
+                ],
+                vec![RuleRequireVersion {
+                    op: VersionCompareOp::Gte,
+                    version: SuricataVersion {
+                        major: 8,
+                        minor: 0,
+                        patch: 3,
+                    }
+                },],
+            ]
+        );
+    }
+}
index 744169a9703997c5c09bd8fa347b2c854794dd36..7bf0be8a97c6350eeda296fd74e43c30656fdd32 100644 (file)
@@ -29,7 +29,7 @@ pub enum Level {
     NotSet = -1,
     _None = 0,
     Error,
-    _Warning,
+    Warning,
     Notice,
     Info,
     _Perf,
@@ -115,6 +115,13 @@ macro_rules!SCLogError {
     };
 }
 
+#[macro_export]
+macro_rules!SCLogWarning {
+    ($($arg:tt)*) => {
+        $crate::do_log!($crate::log::Level::Warning, $($arg)*);
+    };
+}
+
 #[macro_export]
 macro_rules!SCLogNotice {
     ($($arg:tt)*) => {
index 6d115ac48ae5de6d7617fe36db7837b249199be3..9cb4f815531c6e2f1e5551b2ccbddc2338c67d3e 100755 (executable)
@@ -280,6 +280,7 @@ noinst_HEADERS = \
        detect-rawbytes.h \
        detect-reference.h \
        detect-replace.h \
+       detect-requires.h \
        detect-rev.h \
        detect-rfb-name.h \
        detect-rfb-secresult.h \
@@ -895,6 +896,7 @@ libsuricata_c_a_SOURCES = \
        detect-rawbytes.c \
        detect-reference.c \
        detect-replace.c \
+       detect-requires.c \
        detect-rev.c \
        detect-rfb-name.c \
        detect-rfb-secresult.c \
index 40919568503a1e35f231fbe9cb99b6ce8e88bc8f..9c3e3e8b533cd1c04a10636f098d5d445806b465 100644 (file)
@@ -44,6 +44,8 @@
 #include "util-threshold-config.h"
 #include "util-path.h"
 
+#include "rust.h"
+
 #ifdef HAVE_GLOB_H
 #include <glob.h>
 #endif
@@ -109,11 +111,11 @@ char *DetectLoadCompleteSigPath(const DetectEngineCtx *de_ctx, const char *sig_f
  *  \param badsigs_tot Will store number of invalid signatures in the file
  *  \retval 0 on success, -1 on error
  */
-static int DetectLoadSigFile(DetectEngineCtx *de_ctx, char *sig_file,
-        int *goodsigs, int *badsigs)
+static int DetectLoadSigFile(
+        DetectEngineCtx *de_ctx, char *sig_file, int *goodsigs, int *badsigs, int *skippedsigs)
 {
     Signature *sig = NULL;
-    int good = 0, bad = 0;
+    int good = 0, bad = 0, skipped = 0;
     char line[DETECT_MAX_RULE_SIZE] = "";
     size_t offset = 0;
     int lineno = 0, multiline = 0;
@@ -196,6 +198,12 @@ static int DetectLoadSigFile(DetectEngineCtx *de_ctx, char *sig_file,
             if (!de_ctx->sigerror_ok) {
                 bad++;
             }
+            if (de_ctx->sigerror_requires) {
+                SCLogInfo("Skipping signature due to missing requirements: %s from file %s at line "
+                          "%" PRId32,
+                        line, sig_file, lineno - multiline);
+                skipped++;
+            }
         }
         multiline = 0;
     }
@@ -203,6 +211,7 @@ static int DetectLoadSigFile(DetectEngineCtx *de_ctx, char *sig_file,
 
     *goodsigs = good;
     *badsigs = bad;
+    *skippedsigs = skipped;
     return 0;
 }
 
@@ -212,8 +221,8 @@ static int DetectLoadSigFile(DetectEngineCtx *de_ctx, char *sig_file,
  *  \param sig_file Filename (or pattern) holding signatures
  *  \retval -1 on error
  */
-static int ProcessSigFiles(DetectEngineCtx *de_ctx, char *pattern,
-        SigFileLoaderStat *st, int *good_sigs, int *bad_sigs)
+static int ProcessSigFiles(DetectEngineCtx *de_ctx, char *pattern, SigFileLoaderStat *st,
+        int *good_sigs, int *bad_sigs, int *skipped_sigs)
 {
     int r = 0;
 
@@ -250,7 +259,7 @@ static int ProcessSigFiles(DetectEngineCtx *de_ctx, char *pattern,
         } else {
             SCLogConfig("Loading rule file: %s", fname);
         }
-        r = DetectLoadSigFile(de_ctx, fname, good_sigs, bad_sigs);
+        r = DetectLoadSigFile(de_ctx, fname, good_sigs, bad_sigs, skipped_sigs);
         if (r < 0) {
             ++(st->bad_files);
         }
@@ -259,6 +268,7 @@ static int ProcessSigFiles(DetectEngineCtx *de_ctx, char *pattern,
 
         st->good_sigs_total += *good_sigs;
         st->bad_sigs_total += *bad_sigs;
+        st->skipped_sigs_total += *skipped_sigs;
 
 #ifdef HAVE_GLOB_H
     }
@@ -286,6 +296,7 @@ int SigLoadSignatures(DetectEngineCtx *de_ctx, char *sig_file, bool sig_file_exc
     char varname[128] = "rule-files";
     int good_sigs = 0;
     int bad_sigs = 0;
+    int skipped_sigs = 0;
 
     if (strlen(de_ctx->config_prefix) > 0) {
         snprintf(varname, sizeof(varname), "%s.rule-files",
@@ -307,8 +318,9 @@ int SigLoadSignatures(DetectEngineCtx *de_ctx, char *sig_file, bool sig_file_exc
             else {
                 TAILQ_FOREACH(file, &rule_files->head, next) {
                     sfile = DetectLoadCompleteSigPath(de_ctx, file->val);
-                    good_sigs = bad_sigs = 0;
-                    ret = ProcessSigFiles(de_ctx, sfile, sig_stat, &good_sigs, &bad_sigs);
+                    good_sigs = bad_sigs = skipped_sigs = 0;
+                    ret = ProcessSigFiles(
+                            de_ctx, sfile, sig_stat, &good_sigs, &bad_sigs, &skipped_sigs);
                     SCFree(sfile);
 
                     if (de_ctx->failure_fatal && ret != 0) {
@@ -327,7 +339,7 @@ int SigLoadSignatures(DetectEngineCtx *de_ctx, char *sig_file, bool sig_file_exc
 
     /* If a Signature file is specified from command-line, parse it too */
     if (sig_file != NULL) {
-        ret = ProcessSigFiles(de_ctx, sig_file, sig_stat, &good_sigs, &bad_sigs);
+        ret = ProcessSigFiles(de_ctx, sig_file, sig_stat, &good_sigs, &bad_sigs, &skipped_sigs);
 
         if (ret != 0) {
             if (de_ctx->failure_fatal) {
@@ -351,15 +363,23 @@ int SigLoadSignatures(DetectEngineCtx *de_ctx, char *sig_file, bool sig_file_exc
         }
     } else {
         /* we report the total of files and rules successfully loaded and failed */
-        if (strlen(de_ctx->config_prefix) > 0)
+        if (strlen(de_ctx->config_prefix) > 0) {
             SCLogInfo("tenant id %d:  %" PRId32 " rule files processed. %" PRId32
-                      " rules successfully loaded, %" PRId32 " rules failed",
+                      " rules successfully loaded, %" PRId32 " rules failed, %" PRId32
+                      " rules skipped",
                     de_ctx->tenant_id, sig_stat->total_files, sig_stat->good_sigs_total,
-                    sig_stat->bad_sigs_total);
-        else
+                    sig_stat->bad_sigs_total, sig_stat->skipped_sigs_total);
+        } else {
             SCLogInfo("%" PRId32 " rule files processed. %" PRId32
-                      " rules successfully loaded, %" PRId32 " rules failed",
-                    sig_stat->total_files, sig_stat->good_sigs_total, sig_stat->bad_sigs_total);
+                      " rules successfully loaded, %" PRId32 " rules failed, %" PRId32
+                      " rules skipped",
+                    sig_stat->total_files, sig_stat->good_sigs_total, sig_stat->bad_sigs_total,
+                    sig_stat->skipped_sigs_total);
+        }
+        if (de_ctx->requirements != NULL && sig_stat->skipped_sigs_total > 0) {
+            SCDetectRequiresStatusLog(de_ctx->requirements, PROG_VER,
+                    strlen(de_ctx->config_prefix) > 0 ? de_ctx->tenant_id : 0);
+        }
     }
 
     if ((sig_stat->bad_sigs_total || sig_stat->bad_files) && de_ctx->failure_fatal) {
index a97da4617197ca207cd1a1c5622216e3a39b27e3..9f37e09455446bdaf5ca27a227d09dae643f1ec0 100644 (file)
 #include "detect-flow.h"
 #include "detect-flow-age.h"
 #include "detect-flow-pkts.h"
+#include "detect-requires.h"
 #include "detect-tcp-window.h"
 #include "detect-ftpbounce.h"
 #include "detect-isdataat.h"
@@ -575,6 +576,7 @@ void SigTableSetup(void)
     DetectFlowPktsToServerRegister();
     DetectFlowBytesToClientRegister();
     DetectFlowBytesToServerRegister();
+    DetectRequiresRegister();
     DetectWindowRegister();
     DetectRpcRegister();
     DetectFtpbounceRegister();
index 2e4a330788ed6aa2f02e4d39949749ee5855f1df..9dd01f5fd487118a1fbd6442cd41c69a5b3f6e6c 100644 (file)
@@ -115,6 +115,8 @@ enum DetectKeywordId {
     DETECT_FLOW_BYTES_TO_CLIENT,
     DETECT_FLOW_BYTES_TO_SERVER,
 
+    DETECT_REQUIRES,
+
     DETECT_AL_TLS_VERSION,
     DETECT_AL_TLS_SUBJECT,
     DETECT_AL_TLS_ISSUERDN,
index 3e1ce93cf671d2caedcf4e706995dce5a62401f7..13e09be71889fde722baf5e77ddf500e00b2841b 100644 (file)
@@ -2652,6 +2652,10 @@ void DetectEngineCtxFree(DetectEngineCtx *de_ctx)
         SCFree(de_ctx->tenant_path);
     }
 
+    if (de_ctx->requirements) {
+        SCDetectRequiresStatusFree(de_ctx->requirements);
+    }
+
     SCFree(de_ctx);
     //DetectAddressGroupPrintMemory();
     //DetectSigGroupPrintMemory();
index 83c22aa8adace0888c74a49f24124131b2021248..b94d54dd6cd04fea60fc4ad4c220580b793e1b6c 100644 (file)
@@ -2146,12 +2146,17 @@ static Signature *SigInitHelper(DetectEngineCtx *de_ctx, const char *sigstr,
     sig->gid = 1;
 
     int ret = SigParse(de_ctx, sig, sigstr, dir, &parser);
-    if (ret == -3) {
+    if (ret == -4) {
+        /* Rule requirements not met. */
         de_ctx->sigerror_silent = true;
         de_ctx->sigerror_ok = true;
+        de_ctx->sigerror_requires = true;
         goto error;
-    }
-    else if (ret == -2) {
+    } else if (ret == -3) {
+        de_ctx->sigerror_silent = true;
+        de_ctx->sigerror_ok = true;
+        goto error;
+    } else if (ret == -2) {
         de_ctx->sigerror_silent = true;
         goto error;
     } else if (ret < 0) {
diff --git a/src/detect-requires.c b/src/detect-requires.c
new file mode 100644 (file)
index 0000000..4d7f916
--- /dev/null
@@ -0,0 +1,50 @@
+/* Copyright (C) 2023 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+#include "detect-requires.h"
+#include "suricata-common.h"
+#include "detect-engine.h"
+#include "rust.h"
+
+static int DetectRequiresSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr)
+{
+    if (de_ctx->requirements == NULL) {
+        de_ctx->requirements = (void *)SCDetectRequiresStatusNew();
+        BUG_ON(de_ctx->requirements == NULL);
+    }
+
+    const char *errmsg = NULL;
+    int res = SCDetectCheckRequires(rawstr, PROG_VER, &errmsg, de_ctx->requirements);
+    if (res == -1) {
+        // The requires expression is bad, log an error.
+        SCLogError("%s: %s", errmsg, rawstr);
+        de_ctx->sigerror = errmsg;
+    } else if (res < -1) {
+        // This Suricata instance didn't meet the requirements.
+        SCLogInfo("Suricata did not meet the rule requirements: %s: %s", errmsg, rawstr);
+        return -4;
+    }
+    return res;
+}
+
+void DetectRequiresRegister(void)
+{
+    sigmatch_table[DETECT_REQUIRES].name = "requires";
+    sigmatch_table[DETECT_REQUIRES].desc = "require Suricata version or features";
+    sigmatch_table[DETECT_REQUIRES].url = "/rules/meta-keywords.html#requires";
+    sigmatch_table[DETECT_REQUIRES].Setup = DetectRequiresSetup;
+}
diff --git a/src/detect-requires.h b/src/detect-requires.h
new file mode 100644 (file)
index 0000000..70f1dc4
--- /dev/null
@@ -0,0 +1,23 @@
+/* Copyright (C) 2023 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+#ifndef __DETECT_REQUIRES_H__
+#define __DETECT_REQUIRES_H__
+
+void DetectRequiresRegister(void);
+
+#endif /* __DETECT_REQUIRES_H__ */
index 8278290a2992001d4548a681a4764176b8f9e0fe..5d55720758eafd4e900afa3c1b6caac475bcc52e 100644 (file)
@@ -53,6 +53,9 @@
 struct SCSigOrderFunc_;
 struct SCSigSignatureWrapper_;
 
+/* Forward declarations for structures from Rust. */
+typedef struct SCDetectRequiresStatus SCDetectRequiresStatus;
+
 enum SignatureType {
     SIG_TYPE_NOT_SET = 0,
     SIG_TYPE_IPONLY,      // rule is handled by IPONLY engine
@@ -797,6 +800,7 @@ typedef struct SigFileLoaderStat_ {
     int total_files;
     int good_sigs_total;
     int bad_sigs_total;
+    int skipped_sigs_total;
 } SigFileLoaderStat;
 
 typedef struct DetectEngineThreadKeywordCtxItem_ {
@@ -925,6 +929,9 @@ typedef struct DetectEngineCtx_ {
     bool sigerror_silent;
     bool sigerror_ok;
 
+    /** The rule errored out due to missing requirements. */
+    bool sigerror_requires;
+
     bool filedata_config_initialized;
 
     /* specify the configuration for mpm context factory */
@@ -1032,6 +1039,9 @@ typedef struct DetectEngineCtx_ {
 
     /* path to the tenant yaml for this engine */
     char *tenant_path;
+
+    /* Track rule requirements for reporting after loading rules. */
+    SCDetectRequiresStatus *requirements;
 } DetectEngineCtx;
 
 /* Engine groups profiles (low, medium, high, custom) */