Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Requires keyword/v1 #12138

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
59 changes: 57 additions & 2 deletions rust/src/detect/requires.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ enum RequiresError {

/// Passed in requirements not a valid UTF-8 string.
Utf8Error,

/// An unknown requirement was provided.
UnknownRequirement(String),

/// Suricata does not have support for a required keyword.
MissingKeyword(String),
}

impl RequiresError {
Expand All @@ -70,6 +76,8 @@ impl RequiresError {
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",
Self::UnknownRequirement(_) => "Unknown requirements\0",
Self::MissingKeyword(_) => "Suricata missing a required keyword\0",
};
msg.as_ptr() as *const c_char
}
Expand Down Expand Up @@ -162,13 +170,20 @@ 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.
/// - To pass, any of the outer must be true.
pub version: Vec<Vec<RuleRequireVersion>>,

/// Unknown parameters to requires.
pub unknown: Vec<String>,
}

fn parse_op(input: &str) -> IResult<&str, VersionCompareOp> {
Expand Down Expand Up @@ -238,10 +253,14 @@ 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.
SCLogWarning!("Unknown requires keyword: {}", keyword);
requires.unknown.push(format!("{} {}", keyword, value));
}
}

Expand Down Expand Up @@ -291,6 +310,12 @@ fn check_version(
fn check_requires(
requires: &Requires, suricata_version: &SuricataVersion,
) -> Result<(), RequiresError> {
if !requires.unknown.is_empty() {
return Err(RequiresError::UnknownRequirement(
requires.unknown.join(","),
));
}

if !requires.version.is_empty() {
let mut errs = VecDeque::new();
let mut ok = 0;
Expand Down Expand Up @@ -319,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 @@ -586,6 +617,7 @@ mod test {
requires,
Requires {
features: vec![],
keywords: vec![],
version: vec![vec![RuleRequireVersion {
op: VersionCompareOp::Gte,
version: SuricataVersion {
Expand All @@ -594,6 +626,7 @@ mod test {
patch: 0,
}
}]],
unknown: vec![],
}
);

Expand All @@ -602,6 +635,7 @@ mod test {
requires,
Requires {
features: vec![],
keywords: vec![],
version: vec![vec![RuleRequireVersion {
op: VersionCompareOp::Gte,
version: SuricataVersion {
Expand All @@ -610,6 +644,7 @@ mod test {
patch: 0,
}
}]],
unknown: vec![],
}
);

Expand All @@ -618,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 @@ -626,6 +662,7 @@ mod test {
patch: 2,
}
}]],
unknown: vec![],
}
);

Expand All @@ -634,6 +671,7 @@ mod test {
requires,
Requires {
features: vec!["geoip".to_string()],
keywords: vec![],
version: vec![vec![
RuleRequireVersion {
op: VersionCompareOp::Gte,
Expand All @@ -652,6 +690,7 @@ mod test {
}
}
]],
unknown: vec![],
}
);
}
Expand Down Expand Up @@ -748,11 +787,12 @@ mod test {
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();
let requires = parse_requires("feature true_lua, foo bar, version >= 7.0.3").unwrap();
assert_eq!(
requires,
Requires {
features: vec!["lua".to_string()],
features: vec!["true_lua".to_string()],
keywords: vec![],
version: vec![vec![RuleRequireVersion {
op: VersionCompareOp::Gte,
version: SuricataVersion {
Expand All @@ -761,8 +801,14 @@ mod test {
patch: 3,
}
}]],
unknown: vec!["foo bar".to_string()],
}
);

// This should not pass the requires check as it contains an
// unknown requires keyword.
//check_requires(&requires, &SuricataVersion::new(8, 0, 0)).unwrap();
assert!(check_requires(&requires, &SuricataVersion::new(8, 0, 0)).is_err());
}

#[test]
Expand Down Expand Up @@ -802,4 +848,13 @@ 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
22 changes: 22 additions & 0 deletions src/detect-engine-register.c
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,28 @@ static void SigMultilinePrint(int i, const char *prefix)
printf("\n");
}

/** \brief Check if a keyword exists. */
bool SigTableHasKeyword(const char *keyword)
{
for (int i = 0; i < DETECT_TBLSIZE; i++) {
if (sigmatch_table[i].flags & SIGMATCH_NOT_BUILT) {
continue;
}

const char *name = sigmatch_table[i].name;

if (name == NULL || strlen(name) == 0) {
continue;
}

if (strcmp(keyword, name) == 0) {
return true;
}
}

return false;
}

int SigTableList(const char *keyword)
{
size_t size = DETECT_TBLSIZE;
Expand Down
3 changes: 3 additions & 0 deletions src/detect-engine-register.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
#ifndef SURICATA_DETECT_ENGINE_REGISTER_H
#define SURICATA_DETECT_ENGINE_REGISTER_H

#include "suricata-common.h"

enum DetectKeywordId {
DETECT_SID,
DETECT_PRIORITY,
Expand Down Expand Up @@ -341,5 +343,6 @@ void SigTableCleanup(void);
void SigTableInit(void);
void SigTableSetup(void);
void SigTableRegisterTests(void);
bool SigTableHasKeyword(const char *keyword);

#endif /* SURICATA_DETECT_ENGINE_REGISTER_H */
Loading