diff --git a/doc/userguide/rules/transforms.rst b/doc/userguide/rules/transforms.rst index 4bb7915170a3..818fdec04993 100644 --- a/doc/userguide/rules/transforms.rst +++ b/doc/userguide/rules/transforms.rst @@ -436,3 +436,165 @@ Example:: from_base64: offset 12 ; zlib_deflate; content:"This is compressed then base64-encoded"; startswith; endswith; sid:2; rev:1;) + +subslice +-------- + +This transform creates a slice of the input buffer. + +The subslice transform requires parameters: + + * `offset` Specifies the starting offset at which to create the + subslice. When negative, expresses how far from the end of the + input buffer to begin. If the absolute value of a negative offset + exceeds the buffer length and ``truncate`` is not specified, the + transform will produce an empty buffer. When ``truncate`` is + specified, the starting position will be clamped to the beginning + of the buffer. [REQUIRED] + * `nbytes` Specifies the size of the subslice. When negative, + specifies that the subslice will end that many bytes from + the end of the input buffer. If the absolute value of a negative + ``nbytes`` exceeds the buffer length and ``truncate`` is not specified, + the transform will produce an empty buffer. When ``truncate`` is + specified, the endpoint will be clamped to the beginning of the + buffer. The default value is the size of the input buffer minus + the value of ``offset``. [OPTIONAL] + * `truncate` Specifies behavior when ``offset + nbytes`` is larger + than the input buffer size, or when the absolute value of a negative + offset or negative ``nbytes`` exceeds the buffer length. When specified, + the result will be trimmed as though ``offset + nbytes == buffer_length`` + and excessive negative values will be clamped to the buffer boundaries. + When not specified [DEFAULT], an empty buffer will be produced on + which ``bsize:0`` will match. [OPTIONAL] + +Specify the subslice desired -- `nbytes` and `truncate` are optional: + +Format:: + + subslice: offset <, nbytes>, <, truncate>; + +When `nbytes` is not specified, the size of the subslice will be the size +of the input buffer minus the `offset` value. + +When ``truncate`` is not specified and the value of ``offset + nbytes`` exceeds +the buffer length, and empty buffer will be produced such that ``bsize: 0`` will +match. + +The following examples use an input buffer of ``This is Suricata``. + +Examples + +The subslice will be a copy of the input buffer but omit the input buffer's first byte. +The subslice is ``his is Suricata``:: + + subslice: 1; + +This example creates the subslice ``This is Suric``:: + + subslice: 0, 13; + +This example starts at offset ``10`` and ends at 5 bytes from the end +of the buffer which creates a subslice from offset ``10`` to offset ``12``. +The length of the input buffer is ``17`` bytes; ``5`` bytes from the end +is ``12``:: + + subslice: 10, -5; + +This example will create a subslice from the last 3 bytes of the input +buffer and create ``ata``:: + + subslice: -3; + +Negative Offset Handling +~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a negative offset's absolute value exceeds the buffer length, the behavior +depends on whether ``truncate`` is specified: + +Without ``truncate``, the transform produces an empty buffer. For example, +with input buffer ``This is Suricata`` (16 bytes), using ``subslice: -17;`` +produces an empty string and ``bsize:0`` would match:: + + subslice: -17; + +With ``truncate`` specified, excessive negative offsets are clamped to the +buffer length, effectively starting at offset 0. Using the same input buffer +``This is Suricata`` (16 bytes), ``subslice: -17, truncate;`` is treated as +``subslice: -16, truncate;`` and produces the full buffer ``This is Suricata``:: + + subslice: -17, truncate; + +This also works with ``nbytes``. For example, ``subslice: -20, 5, truncate;`` +with input buffer ``This is Suricata`` starts at offset 0 and takes 5 bytes, +producing ``This`` (with a trailing space):: + + subslice: -20, 5, truncate; + +Similarly, when ``truncate`` is specified, negative ``nbytes`` values that would +place the endpoint before the beginning of the buffer are clamped to the +beginning of the buffer. For example, ``subslice: 0, -30, truncate;`` with +input buffer ``This is Suricata`` (16 bytes) clamps the endpoint to the +beginning of the buffer, producing an empty buffer:: + + subslice: 0, -30, truncate; + +However, a moderate negative ``nbytes`` works normally. For example, +``subslice: 0, -8, truncate;`` ends 8 bytes from the end (position 8), +producing ``This is`` (with a trailing space):: + + subslice: 0, -8, truncate; + +Truncation Behavior +~~~~~~~~~~~~~~~~~~~ + +When the buffer has less bytes than ``offset + nbytes``, the transform +will either trim the resulting buffer as though ``offset + nbytes == buffer_length`` +or produce an empty buffer on which `bsize:0` would match. The behavior +is determined by the inclusion of ``truncate`` with the keyword. + +This example receives an input buffer with the value ``curl/7.64.1`` and +produces ``curl/7.64.1``:: + + subslice: 0, 30; + +With truncation off, the default, the buffer produced by the transform +with the same input buffer would be the empty string: ``""`` and +``bsize:0`` would match:: + + subslice: 0, 30; + +When ``truncate`` is specified, ``nbytes + offset`` is reduced +such that they equal the input buffer length. In the following example, +the transform produces ``curl/7.64.1``:: + + subslice: 0, 30, truncate; + +Specifying ``truncate`` does not require ``nbytes`` to be specified: +such that they equal the input buffer length. In the following example, +the transform produces ``curl/7.64.1``:: + + subslice: 0, truncate; + +Summary of Truncate Behavior with Negative Values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following table summarizes how ``truncate`` handles edge cases with +the input buffer ``curl/7.64.1`` (11 bytes): + ++-------------------------------+---------------------+---------------------------+ +| Transform | Without truncate | With truncate | ++===============================+=====================+===========================+ +| ``subslice: 5;`` | ``7.64.1`` (6 bytes)| ``7.64.1`` (6 bytes) | ++-------------------------------+---------------------+---------------------------+ +| ``subslice: -20;`` | Empty buffer | Full buffer (start at 0) | ++-------------------------------+---------------------+---------------------------+ +| ``subslice: -20, 5;`` | Empty buffer | ``curl/`` (5 bytes) | ++-------------------------------+---------------------+---------------------------+ +| ``subslice: 0, -30;`` | Empty buffer | Empty buffer (end at 0) | ++-------------------------------+---------------------+---------------------------+ +| ``subslice: 0, -8;`` | ``cur`` (3 bytes) | ``cur`` (3 bytes) | ++-------------------------------+---------------------+---------------------------+ +| ``subslice: -20, -30;`` | Empty buffer | Empty buffer | ++-------------------------------+---------------------+---------------------------+ +| ``subslice: 0, 30;`` | Empty buffer | Full buffer (11 bytes) | ++-------------------------------+---------------------+---------------------------+ diff --git a/rust/src/detect/transforms/mod.rs b/rust/src/detect/transforms/mod.rs index 677b1ba2ef56..da42ecf38e77 100644 --- a/rust/src/detect/transforms/mod.rs +++ b/rust/src/detect/transforms/mod.rs @@ -28,3 +28,4 @@ pub mod http_headers; pub mod strip_whitespace; pub mod urldecode; pub mod xor; +pub mod subslice; diff --git a/rust/src/detect/transforms/subslice.rs b/rust/src/detect/transforms/subslice.rs new file mode 100644 index 000000000000..4c169e0f6b97 --- /dev/null +++ b/rust/src/detect/transforms/subslice.rs @@ -0,0 +1,850 @@ +/* Copyright (C) 2026 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 suricata_sys::sys::{ + DetectEngineCtx, DetectEngineThreadCtx, InspectionBuffer, SCDetectHelperTransformRegister, + SCDetectSignatureAddTransform, SCInspectionBufferCheckAndExpand, SCInspectionBufferTruncate, + SCTransformTableElmt, Signature, +}; + +use std::os::raw::{c_int, c_void}; + +use std::ffi::CStr; + +static mut G_TRANSFORM_SUBSLICE_ID: c_int = 0; + +#[derive(Debug, PartialEq)] +#[repr(C)] +struct DetectTransformSubsliceData { + pub offset: isize, + pub nbytes: Option, + pub truncate: bool, +} + +impl Default for DetectTransformSubsliceData { + fn default() -> Self { + DetectTransformSubsliceData { + offset: 0, + nbytes: None, + truncate: false, + } + } +} + +fn subslice_do_parse(i: &str) -> Option { + let parts: Vec<_> = i.trim().split(',').map(str::trim).collect(); + + match parts.as_slice() { + [offset] => { + let offset = offset.parse::().ok()?; + Some(DetectTransformSubsliceData { + offset, + nbytes: None, + truncate: false, + }) + } + // offset, truncate OR offset, nbytes + [first, second] => { + let offset = first.parse::().ok()?; + + if second.eq_ignore_ascii_case("truncate") { + // offset, truncate + Some(DetectTransformSubsliceData { + offset, + nbytes: None, + truncate: true, + }) + } else { + // offset, nbytes + let nbytes = second.parse::().ok()?; + if nbytes == 0 { + return None; + } + Some(DetectTransformSubsliceData { + offset, + nbytes: Some(nbytes), + truncate: false, + }) + } + } + // offset, nbytes, truncate + [first, second, third] => { + let offset = first.parse::().ok()?; + let nbytes = second.parse::().ok()?; + + if !third.eq_ignore_ascii_case("truncate") { + return None; + } + + if nbytes == 0 { + return None; + } + + Some(DetectTransformSubsliceData { + offset, + nbytes: Some(nbytes), + truncate: true, + }) + } + _ => { + SCLogError!("Invalid subslice options; use: 'offset' or 'offset, nbytes' or 'offset, truncate' or 'offset, nbytes, truncate'"); + None + } + } +} + +unsafe fn subslice_parse(raw: *const std::os::raw::c_char) -> *mut c_void { + let raw: &CStr = CStr::from_ptr(raw); + if let Ok(s) = raw.to_str() { + if let Some(ctx) = subslice_do_parse(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} + +unsafe extern "C" fn subslice_free(_de: *mut DetectEngineCtx, ctx: *mut c_void) { + std::mem::drop(Box::from_raw(ctx as *mut DetectTransformSubsliceData)); +} + +unsafe extern "C" fn subslice_setup( + de: *mut DetectEngineCtx, s: *mut Signature, opt_str: *const std::os::raw::c_char, +) -> c_int { + if opt_str.is_null() { + return -1; + } + + let ctx = subslice_parse(opt_str); + if ctx.is_null() { + return -1; + } + + let r = SCDetectSignatureAddTransform(s, G_TRANSFORM_SUBSLICE_ID, ctx); + if r != 0 { + subslice_free(de, ctx); + } + + return r; +} + +fn subslice_apply<'a>(data: &'a [u8], ctx: &DetectTransformSubsliceData) -> Option<&'a [u8]> { + let len = data.len() as isize; + + // Handle offset - clamp if truncate, reject if not + let offset = ctx.offset; + if offset.abs() > len && !ctx.truncate { + return None; + } + + // Compute start index from offset (clamped if needed) + let start = if offset >= 0 { + offset.min(len) + } else { + len + offset.max(-len) + }; + + // Compute end index from nbytes + let end = match ctx.nbytes { + None => len, + Some(n) if n < 0 => { + let candidate = len + n; + if candidate < 0 { + if !ctx.truncate { + return None; + } + 0 + } else { + candidate + } + } + Some(n) => { + let candidate = start + n; + if candidate > len && !ctx.truncate { + return None; + } + candidate.min(len) + } + }; + + // Normalize if indices reversed + let (start, end) = if end < start { + (end, start) + } else { + (start, end) + }; + + // Convert to usize - both are guaranteed to be in [0, len] + let (start, end) = (start as usize, end as usize); + + Some(&data[start..end]) +} + +fn subslice_transform_do( + input: &[u8], output: &mut [u8], ctx: &DetectTransformSubsliceData, +) -> u32 { + let Some(slice) = subslice_apply(input, ctx) else { + return 0; + }; + + let len = slice.len(); + + // Use ptr::copy which handles both overlapping and non-overlapping memory correctly. + unsafe { + std::ptr::copy(slice.as_ptr(), output.as_mut_ptr(), len); + } + + len as u32 +} + +unsafe extern "C" fn subslice_transform( + _det: *mut DetectEngineThreadCtx, buffer: *mut InspectionBuffer, ctx: *mut c_void, +) { + let input = (*buffer).inspect; + let input_len = (*buffer).inspect_len; + if input.is_null() || input_len == 0 { + return; + } + + let output = SCInspectionBufferCheckAndExpand(buffer, input_len); + if output.is_null() { + return; + } + let output = std::slice::from_raw_parts_mut(output, input_len as usize); + let ctx = cast_pointer!(ctx, DetectTransformSubsliceData); + let input = build_slice!(input, input_len as usize); + let out_length = subslice_transform_do(input, output, ctx); + SCInspectionBufferTruncate(buffer, out_length); +} + +unsafe extern "C" fn subslice_id(data: *mut *const u8, length: *mut u32, ctx: *mut c_void) { + if data.is_null() || length.is_null() || ctx.is_null() { + return; + } + + // This works because the structure is flat + // Once variables are really implemented, we should investigate if the structure should own + // its serialization or just borrow it to a caller + *data = ctx as *const u8; + *length = std::mem::size_of::() as u32; +} + +#[no_mangle] +pub unsafe extern "C" fn DetectTransformSubsliceRegister() { + let kw = SCTransformTableElmt { + name: b"subslice\0".as_ptr() as *const libc::c_char, + desc: b"create a subslice from the current buffer\0".as_ptr() as *const libc::c_char, + url: b"/rules/transforms.html#subslice\0".as_ptr() as *const libc::c_char, + Setup: Some(subslice_setup), + flags: 0, + Transform: Some(subslice_transform), + Free: Some(subslice_free), + TransformValidate: None, + TransformId: Some(subslice_id), + }; + unsafe { + G_TRANSFORM_SUBSLICE_ID = SCDetectHelperTransformRegister(&kw); + if G_TRANSFORM_SUBSLICE_ID < 0 { + SCLogWarning!("Failed registering transform subslice"); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + static TEST_STRING: &[u8] = b"this is suricata"; + + #[test] + fn test_subslice_parse_invalid() { + assert!(subslice_do_parse("f, y").is_none()); + assert!(subslice_do_parse("0:-10").is_none()); + assert!(subslice_do_parse("1 1").is_none()); + assert!(subslice_do_parse("1,0").is_none()); + assert!(subslice_do_parse("").is_none()); + assert!(subslice_do_parse("1, 2, nottruncate").is_none()); + // Test with too many arguments + assert!(subslice_do_parse("1, 2, 3, 4").is_none()); + assert!(subslice_do_parse("1, 2, truncate, extra").is_none()); + } + + #[test] + fn test_subslice_parse_valid() { + assert_eq!( + subslice_do_parse(" 0 , 9 ").unwrap(), + DetectTransformSubsliceData { + offset: 0, + nbytes: Some(9), + truncate: false + } + ); + assert_eq!( + subslice_do_parse("0, 9").unwrap(), + DetectTransformSubsliceData { + offset: 0, + nbytes: Some(9), + truncate: false + } + ); + assert_eq!( + subslice_do_parse("1, 1").unwrap(), + DetectTransformSubsliceData { + offset: 1, + nbytes: Some(1), + truncate: false + } + ); + assert_eq!( + subslice_do_parse("1").unwrap(), + DetectTransformSubsliceData { + offset: 1, + nbytes: None, + truncate: false + } + ); + assert_eq!( + subslice_do_parse("-1").unwrap(), + DetectTransformSubsliceData { + offset: -1, + nbytes: None, + truncate: false + } + ); + assert_eq!( + subslice_do_parse("0, -3").unwrap(), + DetectTransformSubsliceData { + offset: 0, + nbytes: Some(-3), + truncate: false + } + ); + assert_eq!( + subslice_do_parse("-10, -2").unwrap(), + DetectTransformSubsliceData { + offset: -10, + nbytes: Some(-2), + truncate: false + } + ); + assert_eq!( + subslice_do_parse("1, truncate").unwrap(), + DetectTransformSubsliceData { + offset: 1, + nbytes: None, + truncate: true + } + ); + assert_eq!( + subslice_do_parse("1, TRUNCATE").unwrap(), + DetectTransformSubsliceData { + offset: 1, + nbytes: None, + truncate: true + } + ); + assert_eq!( + subslice_do_parse("2, 10, truncate").unwrap(), + DetectTransformSubsliceData { + offset: 2, + nbytes: Some(10), + truncate: true + } + ); + } + + #[test] + fn test_subslice_transform_offset() { + let expected_output = b"his is suricata"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + + let mut out = vec![0u8; expected_output_len]; + let ctx = subslice_do_parse("1").unwrap(); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + #[test] + fn test_subslice_transform_nbytes() { + let expected_output = b"this is s"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("00,9").unwrap(); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + #[test] + fn test_subslice_transform_nbytes_2() { + let mut buf = Vec::new(); + buf.extend_from_slice(b"OISF provides architecture and infrastructure to open source security software communities and to projects like Suricata, the world-class threat-detection engine. As the need for robust and relevant security technologies grows, OISF serves to protect and maintain the authenticity of open source space. We welcome participation from diverse groups to generate networks and build active communities. We strengthen our communal space by offering user and developer training sessions around the world. OISF hosts SuriCon, the dynamic annual OISF/Suricata User Conference which gives our entire community a unique opportunity to collaborate together. + +OISF is funded by donations from world-class security organizations committed to our mission. A list of these organizations is available on our Consortium Members page."); + let mut out = vec![0u8; 24]; + if let Some(ctx) = subslice_do_parse("15, 24") { + subslice_transform_do(&buf, &mut out, &ctx); + } + assert_eq!(&out[..24], b"rchitecture and infrastr"); + } + + #[test] + fn test_subslice_transform_nbytes_3() { + let expected_output = b"s is suricata"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("3").unwrap(); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + #[test] + fn test_subslice_transform_offset_nbytes() { + let expected_output = b"his is suric"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("1,12").unwrap(); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + #[test] + fn test_subslice_transform_offset_neg_nbytes() { + let expected_output = b" is suric"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("13,-12").unwrap(); + assert!(!ctx.truncate); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + // offset + nbytes > data_len() [NO TRUNCATE] + #[test] + fn test_subslice_transform_offset_and_nbytes_exceeds_len_00() { + let expected_output = b""; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("2, 30").unwrap(); + assert!(!ctx.truncate); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + // offset + nbytes > data_len() [TRUNCATE] + #[test] + fn test_subslice_transform_offset_and_nbytes_exceeds_len_01() { + let expected_output = b"is is suricata"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("2, 30, truncate").unwrap(); + assert!(ctx.truncate); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + // Positive offset with nbytes out of range + #[test] + fn test_subslice_transform_offset_neg_nbytes_2() { + let expected_output = b"his is suric"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("13,-15").unwrap(); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + // Positive offset with nbytes out of range + #[test] + fn test_subslice_transform_offset_neg_nbytes_3() { + let expected_output = b"r"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("10,-5").unwrap(); + assert!(!ctx.truncate); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + #[test] + fn test_subslice_transform_offset_neg_offset() { + let expected_output = b"ata"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("-3").unwrap(); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + // abs(offset) exceeds length + #[test] + fn test_subslice_transform_offset_neg_offset_2() { + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + let mut out = vec![0u8; 3]; + + let ctx = subslice_do_parse("-17").unwrap(); + assert!(!ctx.truncate); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, 0); + } + + #[test] + fn test_subslice_with_truncate_literal() { + let expected_output = b"is is suricata"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("2, truncate").unwrap(); + assert!(ctx.truncate); + assert_eq!(ctx.offset, 2); + assert_eq!(ctx.nbytes, None); + + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + #[test] + fn test_subslice_with_nbytes_and_truncate_01() { + let expected_output = b"is is suricata"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("2, 20, truncate").unwrap(); + assert!(ctx.truncate); + assert_eq!(ctx.offset, 2); + assert_eq!(ctx.nbytes, Some(20)); + + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + #[test] + fn test_subslice_with_nbytes_and_truncate_02() { + let expected_output = b"curl/7.64.1"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + let test_string: &[u8] = b"curl/7.64.1"; + buf.extend_from_slice(test_string); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("0, 30, truncate").unwrap(); + assert!(ctx.truncate); + assert_eq!(ctx.offset, 0); + assert_eq!(ctx.nbytes, Some(30)); + assert!(ctx.truncate); + + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + #[test] + fn test_subslice_with_offset_and_truncate_01() { + let expected_output = b"url/7.64.1"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + let test_string: &[u8] = b"curl/7.64.1"; + buf.extend_from_slice(test_string); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("1, truncate").unwrap(); + assert!(ctx.truncate); + assert_eq!(ctx.offset, 1); + assert_eq!(ctx.nbytes, None); + assert!(ctx.truncate); + + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + #[test] + fn test_subslice_with_offset_and_truncate_02() { + let expected_output = b"curl/7.64.1"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + let test_string: &[u8] = b"curl/7.64.1"; + buf.extend_from_slice(test_string); + let mut out = vec![0u8; expected_output_len]; + + let ctx = subslice_do_parse("0, truncate").unwrap(); + assert!(ctx.truncate); + assert_eq!(ctx.offset, 0); + assert_eq!(ctx.nbytes, None); + assert!(ctx.truncate); + + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + // Test that when truncate is enabled, excessive negative offset is clamped + #[test] + fn test_subslice_transform_truncate_excessive_negative_offset() { + let expected_output = b"This is Suricata"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(b"This is Suricata"); + let mut out = vec![0u8; expected_output_len]; + + // -17 should be clamped to -16 (or 0) when truncate is enabled + let ctx = subslice_do_parse("-17, truncate").unwrap(); + assert!(ctx.truncate); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + // Test that when truncate is NOT enabled, excessive negative offset returns empty + #[test] + fn test_subslice_transform_no_truncate_excessive_negative_offset() { + let mut buf = Vec::new(); + buf.extend_from_slice(b"This is Suricata"); + let mut out = vec![0u8; 16]; + + // -17 without truncate should fail + let ctx = subslice_do_parse("-17").unwrap(); + assert!(!ctx.truncate); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, 0); + } + + // Test that when truncate is enabled, excessive positive offset is clamped + #[test] + fn test_subslice_transform_truncate_excessive_positive_offset() { + let expected_output = b""; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(b"This is Suricata"); + let mut out = vec![0u8; 16]; + + // 17 should be clamped to 16 when truncate is enabled (results in empty slice) + let ctx = subslice_do_parse("17, truncate").unwrap(); + assert!(ctx.truncate); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + // Test with nbytes and excessive negative offset with truncate + #[test] + fn test_subslice_transform_truncate_excessive_negative_offset_with_nbytes() { + let expected_output = b"This is Su"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(b"This is Suricata"); + let mut out = vec![0u8; expected_output_len]; + + // -17 should be clamped to -16 (start at 0), then take 10 bytes + let ctx = subslice_do_parse("-17, 10, truncate").unwrap(); + assert!(ctx.truncate); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + // Test edge case: offset exactly at -length with truncate + #[test] + fn test_subslice_transform_truncate_exact_negative_offset() { + let expected_output = b"This is Suricata"; + let expected_output_len = expected_output.len(); + + let mut buf = Vec::new(); + buf.extend_from_slice(b"This is Suricata"); + let mut out = vec![0u8; expected_output_len]; + + // -16 should work normally (start at position 0) + let ctx = subslice_do_parse("-16, truncate").unwrap(); + assert!(ctx.truncate); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output_len as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } + + // Test that overlapping buffers work correctly (input == output) + // This simulates the case where C code may pass the same buffer + #[test] + fn test_subslice_transform_overlapping_buffer() { + let expected_output = b"is is suricata"; + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + + // Simulate overlapping buffers using unsafe code + let ctx = subslice_do_parse("2").unwrap(); + unsafe { + let ptr = buf.as_mut_ptr(); + let len = buf.len(); + let input_slice = std::slice::from_raw_parts(ptr, len); + let output_slice = std::slice::from_raw_parts_mut(ptr, len); + let cnt = subslice_transform_do(input_slice, output_slice, &ctx); + assert_eq!(cnt, expected_output.len() as u32); + assert_eq!(&buf[..cnt as usize], expected_output); + } + } + + // Test overlapping buffer with negative offset + #[test] + fn test_subslice_transform_overlapping_buffer_negative_offset() { + let expected_output = b"ata"; + + let mut buf = Vec::new(); + buf.extend_from_slice(TEST_STRING); + + // Simulate overlapping buffers using unsafe code + let ctx = subslice_do_parse("-3").unwrap(); + unsafe { + let ptr = buf.as_mut_ptr(); + let len = buf.len(); + let input_slice = std::slice::from_raw_parts(ptr, len); + let output_slice = std::slice::from_raw_parts_mut(ptr, len); + let cnt = subslice_transform_do(input_slice, output_slice, &ctx); + assert_eq!(cnt, expected_output.len() as u32); + assert_eq!(&buf[..cnt as usize], expected_output); + } + } + + // Test excessive negative nbytes with truncate + #[test] + fn test_excessive_negative_nbytes_with_truncate() { + let mut buf = Vec::new(); + buf.extend_from_slice(b"This is Suricata"); + let mut out = vec![0u8; 16]; + + // offset: -20 (clamped to -16, start at 0) + // nbytes: -30 (would end at -14, clamped to 0) + // Result: empty buffer (start=0, end=0) + let ctx = subslice_do_parse("-20, -30, truncate").unwrap(); + assert!(ctx.truncate); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + // With truncate, this should produce empty buffer (start == end) + assert_eq!(cnt, 0); + } + + // Test excessive negative nbytes without truncate + #[test] + fn test_excessive_negative_nbytes_no_truncate() { + let mut buf = Vec::new(); + buf.extend_from_slice(b"This is Suricata"); + let mut out = vec![0u8; 16]; + + // nbytes: -30 (would end at -14, which is invalid) + let ctx = subslice_do_parse("0, -30").unwrap(); + assert!(!ctx.truncate); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + // Without truncate, this should fail + assert_eq!(cnt, 0); + } + + // Test moderate negative nbytes with truncate + #[test] + fn test_moderate_negative_nbytes_with_truncate() { + let expected_output = b"This is "; + + let mut buf = Vec::new(); + buf.extend_from_slice(b"This is Suricata"); + let mut out = vec![0u8; 16]; + + // offset: 0, nbytes: -8 (end at position 8) + let ctx = subslice_do_parse("0, -8, truncate").unwrap(); + let cnt = subslice_transform_do(&buf, &mut out, &ctx); + + assert_eq!(cnt, expected_output.len() as u32); + assert_eq!(&out[..cnt as usize], expected_output); + } +} diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 477247642b04..ee3e67b4243b 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -751,6 +751,7 @@ void SigTableSetup(void) DetectTransformLuaxformRegister(); DetectTransformGunzipRegister(); DetectTransformZlibDeflateRegister(); + DetectTransformSubsliceRegister(); DetectFileHandlerRegister();