diff --git a/doc/userguide/rules/integer-keywords.rst b/doc/userguide/rules/integer-keywords.rst index cd59a44ff630..81492ca4cd7d 100644 --- a/doc/userguide/rules/integer-keywords.rst +++ b/doc/userguide/rules/integer-keywords.rst @@ -142,4 +142,17 @@ For the array [1,2,3,4,5,6], here are some examples: * 3:-1 will have subslice [4,5] * -4:4 will have subslice [3,4] -If one index is out of bounds, an empty subslice is used. \ No newline at end of file +If one index is out of bounds, an empty subslice is used. + +Count +----- + +Multi-integer can also just count the number of occurences +without matching to a specific value. + +The syntax is:: + keyword: count [mode] value; + +Examples:: + + http2.window:count >5; diff --git a/etc/schema.json b/etc/schema.json index 5fbf3245bab5..4c1af452d4ee 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -1593,7 +1593,7 @@ "type": "string", "suricata": { "keywords": [ - "enip.command" + "enip_command" ] } }, @@ -1692,7 +1692,7 @@ "type": "string", "suricata": { "keywords": [ - "enip.command" + "enip_command" ] } }, diff --git a/rust/cbindgen.toml b/rust/cbindgen.toml index 78c3fd73305f..ef8e3d299e9e 100644 --- a/rust/cbindgen.toml +++ b/rust/cbindgen.toml @@ -90,6 +90,7 @@ include = [ "FtpDataStateValues", "HTTP2TransactionState", "DataRepType", + "DETECT_COUNT_INDEX", ] # A list of items to not include in the generated bindings diff --git a/rust/src/detect/mod.rs b/rust/src/detect/mod.rs index e42217d25eee..727480798dcc 100644 --- a/rust/src/detect/mod.rs +++ b/rust/src/detect/mod.rs @@ -139,6 +139,8 @@ pub const SIGMATCH_INFO_ENUM_UINT: u32 = 0x100000; // BIT_U32(20) pub const SIGMATCH_INFO_BITFLAGS_UINT: u32 = 0x200000; // BIT_U32(21) +pub const DETECT_COUNT_INDEX: u32 = 0xFFFFFFFF; + #[repr(u8)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] // endian diff --git a/rust/src/detect/uint.rs b/rust/src/detect/uint.rs index e3f847c0bb78..745646a1e8e0 100644 --- a/rust/src/detect/uint.rs +++ b/rust/src/detect/uint.rs @@ -58,6 +58,7 @@ pub enum DetectUintIndex { OrAbsent, Index((bool, i32)), NumberMatches(DetectUintData), + Count(DetectUintData), } #[derive(Debug, PartialEq)] @@ -109,6 +110,14 @@ fn parse_uint_subslice(parts: &[&str]) -> Option<(i32, i32)> { return Some((start, end)); } +fn parse_multi_count(s: &str) -> IResult<&str, DetectUintData> { + let (s, _) = opt(is_a(" "))(s)?; + let (s, _) = tag("count")(s)?; + let (s, _) = opt(is_a(" "))(s)?; + let (s, du32) = detect_parse_uint::(s)?; + Ok((s, du32)) +} + fn parse_uint_index(parts: &[&str]) -> Option { let index = if parts.len() >= 2 { match parts[1] { @@ -119,6 +128,8 @@ fn parse_uint_index(parts: &[&str]) -> Option { // not only a literal, but some numeric value _ => return parse_uint_index_val(parts[1]), } + } else if let Ok((_, du)) = parse_multi_count(parts[0]) { + DetectUintIndex::Count(du) } else { DetectUintIndex::Any }; @@ -132,6 +143,19 @@ pub(crate) fn detect_parse_array_uint(s: &str) -> Option { + arg1: T::min_value(), + arg2: T::min_value(), + mode: DetectUintMode::DetectUintModeEqual, + }, + index, + start: 0, + end: 0, + }); + } + let (_, du) = detect_parse_uint::(parts[0]).ok()?; let (start, end) = parse_uint_subslice(&parts)?; @@ -152,6 +176,19 @@ pub(crate) fn detect_parse_array_uint_enum } let index = parse_uint_index(&parts)?; + if let DetectUintIndex::Count(_) = &index { + return Some(DetectUintArrayData { + du: DetectUintData:: { + arg1: T1::min_value(), + arg2: T1::min_value(), + mode: DetectUintMode::DetectUintModeEqual, + }, + index, + start: 0, + end: 0, + }); + } + let du = detect_parse_uint_enum::(parts[0])?; let (start, end) = parse_uint_subslice(&parts)?; @@ -229,6 +266,26 @@ pub(crate) fn detect_uint_match_at_index( } return 0; } + DetectUintIndex::Count(du32) => { + if !eof { + match du32.mode { + DetectUintMode::DetectUintModeGt | DetectUintMode::DetectUintModeGte => {} + _ => { + return 0; + } + } + } + let mut nb = 0u32; + for response in subslice { + if get_value(response).is_some() { + nb += 1; + } + } + if detect_match_uint(du32, nb) { + return 1; + } + return 0; + } DetectUintIndex::All => { if !eof { return 0; @@ -764,6 +821,83 @@ pub unsafe extern "C" fn SCDetectU16Free(ctx: &mut DetectUintData) { std::mem::drop(Box::from_raw(ctx)); } +#[no_mangle] +pub unsafe extern "C" fn SCDetectMultiCountParse(ustr: *const std::os::raw::c_char) -> *mut c_void { + let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe + if let Ok(s) = ft_name.to_str() { + if let Ok((_, ctx)) = parse_multi_count(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut c_void; + } + } + return std::ptr::null_mut(); +} + +/// just a u8 for FFI +#[derive(PartialEq, Eq, Clone, Debug)] +#[repr(u8)] +pub enum DetectMultiIndex { + DetectMultiIndexAny, + DetectMultiIndexAbsentOr, + DetectMultiIndexAll, + DetectMultiIndexAll1, + DetectMultiIndexNb, + DetectMultiIndexPrecise, + DetectMultiIndexError, +} + +impl DetectUintIndex { + // just borrow + fn index(&self) -> DetectMultiIndex { + match self { + DetectUintIndex::All => DetectMultiIndex::DetectMultiIndexAll, + DetectUintIndex::All1 => DetectMultiIndex::DetectMultiIndexAll1, + DetectUintIndex::Any => DetectMultiIndex::DetectMultiIndexAny, + DetectUintIndex::OrAbsent => DetectMultiIndex::DetectMultiIndexAbsentOr, + DetectUintIndex::NumberMatches(_) => DetectMultiIndex::DetectMultiIndexNb, + DetectUintIndex::Index(_) => DetectMultiIndex::DetectMultiIndexPrecise, + DetectUintIndex::Count(_) => DetectMultiIndex::DetectMultiIndexError, + } + } + + // take ownership and move + fn into_box(self) -> *mut c_void { + match self { + DetectUintIndex::NumberMatches(du32) => { + let boxed = Box::new(du32); + Box::into_raw(boxed) as *mut c_void + } + // TODO DetectUintIndex::Index(_) + _ => std::ptr::null_mut(), + } + } +} + +fn parse_multi_index(s: &str) -> Option { + match s { + "all" => Some(DetectUintIndex::All), + "all1" => Some(DetectUintIndex::All1), + "any" => Some(DetectUintIndex::Any), + "absent_or" => Some(DetectUintIndex::OrAbsent), + // not only a literal, but some numeric value + _ => return parse_uint_index_val(s), + } +} + +#[no_mangle] +pub unsafe extern "C" fn SCDetectMultiIndexParse( + ustr: *const std::os::raw::c_char, it: *mut DetectMultiIndex, +) -> *mut c_void { + let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe + if let Ok(s) = ft_name.to_str() { + if let Some(ctx) = parse_multi_index(s) { + *it = ctx.index(); + return ctx.into_box(); + } + } + return std::ptr::null_mut(); +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/src/mime/detect.rs b/rust/src/mime/detect.rs index b1a36ce6a025..a777d341623d 100644 --- a/rust/src/mime/detect.rs +++ b/rust/src/mime/detect.rs @@ -63,6 +63,26 @@ pub unsafe extern "C" fn SCDetectMimeEmailGetUrl( return 0; } +/// Intermediary function used in detect-email.c to access data from the MimeStateSMTP structure +/// for array header fields. +/// The hname parameter determines which data will be returned. +#[no_mangle] +pub unsafe extern "C" fn SCDetectMimeEmailGetCount( + ctx: &MimeStateSMTP, hname: *const std::os::raw::c_char, +) -> u32 { + let c_str = CStr::from_ptr(hname); //unsafe + let str = c_str.to_str().unwrap_or(""); + + let mut r = 0u32; + for h in &ctx.headers[..ctx.main_headers_nb] { + if mime::slice_equals_lowercase(&h.name, str.as_bytes()) { + r += 1; + } + } + + return r; +} + /// Intermediary function used in detect-email.c to access data from the MimeStateSMTP structure /// for array header fields. /// The hname parameter determines which data will be returned. diff --git a/src/Makefile.am b/src/Makefile.am index 66da2d1df0a7..48449612a029 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -253,6 +253,7 @@ noinst_HEADERS = \ detect-metadata.h \ detect-modbus.h \ detect-msg.h \ + detect-multi.h \ detect-nfs-version.h \ detect-noalert.h \ detect-nocase.h \ @@ -852,6 +853,7 @@ libsuricata_c_a_SOURCES = \ detect-metadata.c \ detect-modbus.c \ detect-msg.c \ + detect-multi.c \ detect-nfs-version.c \ detect-noalert.c \ detect-nocase.c \ diff --git a/src/detect-email.c b/src/detect-email.c index be5e9793f949..4d3000e201c9 100644 --- a/src/detect-email.c +++ b/src/detect-email.c @@ -21,6 +21,7 @@ #include "detect-parse.h" #include "app-layer-smtp.h" #include "detect-email.h" +#include "detect-multi.h" #include "rust.h" #include "detect-engine-content-inspection.h" @@ -208,6 +209,10 @@ static int DetectMimeEmailReceivedSetup(DetectEngineCtx *de_ctx, Signature *s, c if (SCDetectSignatureSetAppProto(s, ALPROTO_SMTP) < 0) return -1; + if (arg) { + return DetectMultiSetup(de_ctx, s, arg); + } + return 0; } @@ -220,6 +225,11 @@ static bool GetMimeEmailReceivedData(DetectEngineThreadCtx *det_ctx, const void return false; } + if (idx == DETECT_COUNT_INDEX) { + *buf_len = SCDetectMimeEmailGetCount(tx->mime_state, "received"); + return true; + } + if (SCDetectMimeEmailGetDataArray(tx->mime_state, buf, buf_len, "received", idx) != 1) { return false; } @@ -336,7 +346,7 @@ void DetectEmailRegister(void) kw.desc = "'Received' field from an email"; kw.url = "/rules/email-keywords.html#email.received"; kw.Setup = DetectMimeEmailReceivedSetup; - kw.flags = SIGMATCH_NOOPT | SIGMATCH_INFO_STICKY_BUFFER | SIGMATCH_INFO_MULTI_BUFFER; + kw.flags = SIGMATCH_OPTIONAL_OPT | SIGMATCH_INFO_STICKY_BUFFER | SIGMATCH_INFO_MULTI_BUFFER; SCDetectHelperKeywordRegister(&kw); g_mime_email_received_buffer_id = SCDetectHelperMultiBufferMpmRegister("email.received", "MIME EMAIL RECEIVED", ALPROTO_SMTP, STREAM_TOSERVER, GetMimeEmailReceivedData); diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 8a490d75b429..7d92d97a9e49 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -210,6 +210,7 @@ #include "detect-ja4-hash.h" #include "detect-ftp-command.h" #include "detect-entropy.h" +#include "detect-multi.h" #include "detect-ftp-command-data.h" #include "detect-ftp-completion-code.h" #include "detect-ftp-reply.h" @@ -641,6 +642,7 @@ void SigTableSetup(void) DetectBytejumpRegister(); DetectBytemathRegister(); DetectEntropyRegister(); + DetectCountRegister(); DetectSameipRegister(); DetectGeoipRegister(); DetectL3ProtoRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index cdfe9e22f189..d07f899e67b3 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -94,6 +94,10 @@ enum DetectKeywordId { DETECT_URILEN, DETECT_ABSENT, DETECT_ENTROPY, + DETECT_COUNT, + DETECT_MULTI_ALL, + DETECT_MULTI_ALL1, + DETECT_MULTI_NB, /* end of content inspection */ DETECT_METADATA, diff --git a/src/detect-engine.c b/src/detect-engine.c index c894cf595556..c8c0400b788e 100644 --- a/src/detect-engine.c +++ b/src/detect-engine.c @@ -56,6 +56,8 @@ #include "detect-engine-payload.h" #include "detect-fast-pattern.h" #include "detect-byte-extract.h" +#include "detect-multi.h" +#include "detect-engine-uint.h" #include "detect-content.h" #include "detect-uricontent.h" #include "detect-tcphdr.h" @@ -2156,6 +2158,41 @@ uint8_t DetectEngineInspectMultiBufferGeneric(DetectEngineCtx *de_ctx, transforms = engine->v2.transforms; } + bool stop_on_first_match = true; + const bool eof = + (AppLayerParserGetStateProgress(f->proto, f->alproto, txv, flags) > engine->progress); + SigMatchData *smd = engine->smd; + switch (smd->type) { + case DETECT_COUNT: + // count should always be first and only + if (!DetectCountDoMatch( + det_ctx, f, flags, txv, engine->v2.GetMultiData, smd->ctx, eof)) { + return DETECT_ENGINE_INSPECT_SIG_NO_MATCH; + } else { + // only count + return DETECT_ENGINE_INSPECT_SIG_MATCH; + } + case DETECT_MULTI_ALL: + // fallthrough + case DETECT_MULTI_ALL1: + if (!eof) { + return DETECT_ENGINE_INSPECT_SIG_NO_MATCH; + } + stop_on_first_match = false; + smd++; + break; + case DETECT_MULTI_NB: + if (!eof) { + DetectU32Data *du32 = (DetectU32Data *)smd->ctx; + if (du32->mode != DETECT_UINT_GTE && du32->mode != DETECT_UINT_GT) { + return DETECT_ENGINE_INSPECT_SIG_NO_MATCH; + } + } + stop_on_first_match = false; + smd++; + } + + uint32_t nb_matches = 0; do { InspectionBuffer *buffer = DetectGetMultiData(det_ctx, transforms, f, flags, txv, engine->sm_list, local_id, engine->v2.GetMultiData); @@ -2165,17 +2202,33 @@ uint8_t DetectEngineInspectMultiBufferGeneric(DetectEngineCtx *de_ctx, // The GetData functions set buffer->flags to DETECT_CI_FLAGS_SINGLE // This is not meant for streaming buffers - const bool match = DetectEngineContentInspectionBuffer(de_ctx, det_ctx, s, engine->smd, - NULL, f, buffer, DETECT_ENGINE_CONTENT_INSPECTION_MODE_STATE); + const bool match = DetectEngineContentInspectionBuffer(de_ctx, det_ctx, s, smd, NULL, f, + buffer, DETECT_ENGINE_CONTENT_INSPECTION_MODE_STATE); if (match) { - return DETECT_ENGINE_INSPECT_SIG_MATCH; + if (stop_on_first_match) + return DETECT_ENGINE_INSPECT_SIG_MATCH; + nb_matches++; } local_id++; } while (1); + if (!stop_on_first_match) { + switch (engine->smd->type) { + case DETECT_MULTI_ALL: + if (nb_matches == local_id) + return DETECT_ENGINE_INSPECT_SIG_MATCH; + break; + case DETECT_MULTI_ALL1: + if (nb_matches == local_id && nb_matches > 0) + return DETECT_ENGINE_INSPECT_SIG_MATCH; + break; + case DETECT_MULTI_NB: + if (DetectU32Match(nb_matches, (DetectU32Data *)engine->smd->ctx)) + return DETECT_ENGINE_INSPECT_SIG_MATCH; + } + return DETECT_ENGINE_INSPECT_SIG_NO_MATCH; + } if (local_id == 0) { // That means we did not get even one buffer value from the multi-buffer - const bool eof = (AppLayerParserGetStateProgress(f->proto, f->alproto, txv, flags) > - engine->progress); if (eof && engine->match_on_null) { return DETECT_ENGINE_INSPECT_SIG_MATCH; } diff --git a/src/detect-multi.c b/src/detect-multi.c new file mode 100644 index 000000000000..e17b9bfc9c45 --- /dev/null +++ b/src/detect-multi.c @@ -0,0 +1,118 @@ +/* Copyright (C) 2025 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 "suricata-common.h" +#include "rust.h" + +#include "detect-multi.h" +#include "detect-engine-buffer.h" +#include "detect-engine-uint.h" +#include "detect-parse.h" +// DetectAbsentData +#include "detect-isdataat.h" + +#include "util-validate.h" + +static void DetectDu32Free(DetectEngineCtx *de_ctx, void *ptr) +{ + SCDetectU32Free(ptr); +} + +int DetectMultiSetup(DetectEngineCtx *de_ctx, Signature *s, const char *arg) +{ + DetectU32Data *du32 = SCDetectMultiCountParse(arg); + if (du32 != NULL) { + if (SCSigMatchAppendSMToList( + de_ctx, s, DETECT_COUNT, (SigMatchCtx *)du32, s->init_data->list) != NULL) { + return 0; + } + SCLogError("error during count setup"); + DetectDu32Free(de_ctx, du32); + return -1; + } // else not count + DetectMultiIndex index_type; + void *sm_ctx = SCDetectMultiIndexParse(arg, &index_type); + DetectAbsentData *dad; + switch (index_type) { + case DetectMultiIndexAny: + // default case, nothing to do + return 0; + case DetectMultiIndexAbsentOr: + dad = SCMalloc(sizeof(DetectAbsentData)); + if (unlikely(dad == NULL)) + return -1; + + dad->or_else = true; + if (SCSigMatchAppendSMToList( + de_ctx, s, DETECT_ABSENT, (SigMatchCtx *)dad, s->init_data->list) == NULL) { + return 0; + } + // DetectAbsentFree + SCFree(dad); + return -1; + case DetectMultiIndexAll: + if (SCSigMatchAppendSMToList(de_ctx, s, DETECT_MULTI_ALL, NULL, s->init_data->list) != + NULL) { + return 0; + } + return -1; + case DetectMultiIndexAll1: + if (SCSigMatchAppendSMToList(de_ctx, s, DETECT_MULTI_ALL1, NULL, s->init_data->list) != + NULL) { + return 0; + } + return -1; + // TODO precise index + case DetectMultiIndexNb: + if (SCSigMatchAppendSMToList(de_ctx, s, DETECT_MULTI_NB, sm_ctx, s->init_data->list) != + NULL) { + return 0; + } + return -1; + default: + SCLogError("invalid argument for multi-buffer"); + return -1; + } +} + +bool DetectCountDoMatch(DetectEngineThreadCtx *det_ctx, Flow *f, const uint8_t flow_flags, + void *txv, InspectionMultiBufferGetDataPtr GetBuf, const SigMatchCtx *ctx, bool eof) +{ + uint32_t count = 0; + DetectU32Data *du32 = (DetectU32Data *)ctx; + + if (!eof && du32->mode != DETECT_UINT_GTE && du32->mode != DETECT_UINT_GT) { + return false; + } + if (!GetBuf(det_ctx, txv, flow_flags, DETECT_COUNT_INDEX, NULL, &count)) { + DEBUG_VALIDATE_BUG_ON(1); + return false; + } + + return DetectU32Match(count, du32); +} + +void DetectCountRegister(void) +{ + // This is not used as a regular keyword + // But an option that can be set on multi-buffers + sigmatch_table[DETECT_COUNT].name = "count"; + sigmatch_table[DETECT_COUNT].desc = "count number of buffers in a multi-buffer"; + sigmatch_table[DETECT_COUNT].Free = DetectDu32Free; + + sigmatch_table[DETECT_MULTI_NB].Free = DetectDu32Free; +} diff --git a/src/detect-multi.h b/src/detect-multi.h new file mode 100644 index 000000000000..48858610c206 --- /dev/null +++ b/src/detect-multi.h @@ -0,0 +1,26 @@ +/* Copyright (C) 2025 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 SURICATA_DETECT_MULTI_H +#define SURICATA_DETECT_MULTI_H + +void DetectCountRegister(void); +bool DetectCountDoMatch(DetectEngineThreadCtx *det_ctx, Flow *f, const uint8_t flow_flags, + void *txv, InspectionMultiBufferGetDataPtr GetBuf, const SigMatchCtx *ctx, bool eof); +int DetectMultiSetup(DetectEngineCtx *de_ctx, Signature *s, const char *arg); + +#endif