mirror of https://github.com/OISF/suricata
requires: add requires keyword
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>pull/10076/head
parent
15ed51f9b8
commit
5d5b0509a5
@ -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,
|
||||
}
|
||||
},],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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__ */
|
||||
Loading…
Reference in New Issue