Skip to content

Commit

Permalink
requires: support requires check for keyword
Browse files Browse the repository at this point in the history
For example:

    requires: keyword foo;

Will require that Suricata supports the "foo" keyword.
  • Loading branch information
jasonish committed Nov 20, 2024
1 parent 677b081 commit 37545f6
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 5 deletions.
11 changes: 6 additions & 5 deletions doc/userguide/rules/meta.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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::
Expand All @@ -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.
Expand Down
25 changes: 25 additions & 0 deletions rust/src/detect/requires.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -600,6 +617,7 @@ mod test {
requires,
Requires {
features: vec![],
keywords: vec![],
version: vec![vec![RuleRequireVersion {
op: VersionCompareOp::Gte,
version: SuricataVersion {
Expand All @@ -617,6 +635,7 @@ mod test {
requires,
Requires {
features: vec![],
keywords: vec![],
version: vec![vec![RuleRequireVersion {
op: VersionCompareOp::Gte,
version: SuricataVersion {
Expand All @@ -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 {
Expand All @@ -651,6 +671,7 @@ mod test {
requires,
Requires {
features: vec!["geoip".to_string()],
keywords: vec![],
version: vec![vec![
RuleRequireVersion {
op: VersionCompareOp::Gte,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
}
}
18 changes: 18 additions & 0 deletions rust/src/feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand All @@ -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.
Expand All @@ -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))]
Expand Down

0 comments on commit 37545f6

Please sign in to comment.