/* 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 { 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>> { 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, /// The version expression. /// /// - All of the inner most must evaluate to true. /// - To pass, any of the outer must be true. pub version: Vec>, } 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::(), )(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 { 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 { 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, features: HashSet, /// 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(""); 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(|| "".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::>() .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, } },], ] ); } }