use std::collections::{HashSet, VecDeque};
use std::{cmp::Ordering, ffi::CStr};
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 {
VersionGt,
VersionLt(SuricataVersion),
MissingFeature(String),
BadSuricataVersion,
BadRequires,
MultipleVersions,
Utf8Error,
UnknownRequirement(String),
MissingKeyword(String),
}
impl RequiresError {
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",
Self::UnknownRequirement(_) => "Unknown requirements\0",
Self::MissingKeyword(_) => "Suricata missing a required keyword\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,
}
}
}
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>,
pub keywords: Vec<String>,
pub version: Vec<Vec<RuleRequireVersion>>,
pub unknown: Vec<String>,
}
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)
}
fn parse_next_version_part(input: &str) -> IResult<&str, u8> {
map_res(
take_till(|c| c == '.' || c == '-' || c == ' '),
|s: &str| s.parse::<u8>(),
)(input)
}
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)> {
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;
}
"keyword" => {
requires.keywords.push(value.trim().to_string());
}
_ => {
SCLogWarning!("Unknown requires keyword: {}", keyword);
requires.unknown.push(format!("{} {}", keyword, value));
}
}
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.unknown.is_empty() {
return Err(RequiresError::UnknownRequirement(
requires.unknown.join(","),
));
}
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()));
}
}
for keyword in &requires.keywords {
if !crate::feature::has_keyword(keyword) {
return Err(RequiresError::MissingKeyword(keyword.to_string()));
}
}
Ok(())
}
#[derive(Debug, Default)]
pub struct SCDetectRequiresStatus {
min_version: Option<SuricataVersion>,
features: HashSet<String>,
feature_count: u64,
lt_count: u64,
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);
}
}
#[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 {
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() {
assert!(SuricataVersion::new(7, 1, 1) < SuricataVersion::new(7, 1, 2));
assert!(SuricataVersion::new(7, 1, 1) <= SuricataVersion::new(7, 1, 2));
assert!(SuricataVersion::new(7, 1, 1) <= SuricataVersion::new(7, 1, 1));
assert!(SuricataVersion::new(7, 1, 1) >= SuricataVersion::new(7, 1, 1));
assert!(SuricataVersion::new(7, 3, 1) < SuricataVersion::new(7, 22, 1));
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,
}
);
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![],
keywords: vec![],
version: vec![vec![RuleRequireVersion {
op: VersionCompareOp::Gte,
version: SuricataVersion {
major: 7,
minor: 0,
patch: 0,
}
}]],
unknown: vec![],
}
);
let requires = parse_requires("version >= 7.1").unwrap();
assert_eq!(
requires,
Requires {
features: vec![],
keywords: vec![],
version: vec![vec![RuleRequireVersion {
op: VersionCompareOp::Gte,
version: SuricataVersion {
major: 7,
minor: 1,
patch: 0,
}
}]],
unknown: vec![],
}
);
let requires = parse_requires("feature output::file-store, version >= 7.1.2").unwrap();
assert_eq!(
requires,
Requires {
features: vec!["output::file-store".to_string()],
keywords: vec![],
version: vec![vec![RuleRequireVersion {
op: VersionCompareOp::Gte,
version: SuricataVersion {
major: 7,
minor: 1,
patch: 2,
}
}]],
unknown: vec![],
}
);
let requires = parse_requires("feature geoip, version >= 7.1.2 < 8").unwrap();
assert_eq!(
requires,
Requires {
features: vec!["geoip".to_string()],
keywords: vec![],
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,
}
}
]],
unknown: vec![],
}
);
}
#[test]
fn test_check_requires() {
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,
})),
);
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(()));
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)
);
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(()));
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()))
);
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());
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());
let requires = parse_requires("feature true_lua, foo bar, version >= 7.0.3").unwrap();
assert_eq!(
requires,
Requires {
features: vec!["true_lua".to_string()],
keywords: vec![],
version: vec![vec![RuleRequireVersion {
op: VersionCompareOp::Gte,
version: SuricataVersion {
major: 7,
minor: 0,
patch: 3,
}
}]],
unknown: vec!["foo bar".to_string()],
}
);
assert!(check_requires(&requires, &SuricataVersion::new(8, 0, 0)).is_err());
}
#[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,
}
},],
]
);
}
#[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());
}
}