Coverage Report

Created: 2026-11-12 06:59

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/suricata8/rust/src/detect/entropy.rs
Line
Count
Source
1
/* Copyright (C) 2025 Open Information Security Foundation
2
 *
3
 * You can copy, redistribute or modify this Program under the terms of
4
 * the GNU General Public License version 2 as published by the Free
5
 * Software Foundation.
6
 *
7
 * This program is distributed in the hope that it will be useful,
8
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10
 * GNU General Public License for more details.
11
 *
12
 * You should have received a copy of the GNU General Public License
13
 * version 2 along with this program; if not, write to the Free Software
14
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
15
 * 02110-1301, USA.
16
 */
17
18
// Author: Jeff Lucovsky <jlucovsky@oisf.net>
19
//
20
use crate::detect::error::RuleParseError;
21
use crate::detect::float::{detect_match_float, detect_parse_float, DetectFloatData};
22
use crate::detect::parser::take_until_whitespace;
23
24
use nom7::bytes::complete::tag;
25
use nom7::character::complete::multispace0;
26
use nom7::sequence::preceded;
27
use nom7::{Err, IResult};
28
29
use std::ffi::CStr;
30
use std::os::raw::{c_double, c_char, c_void};
31
use std::slice;
32
33
#[repr(C)]
34
#[derive(Debug)]
35
pub struct DetectEntropyData {
36
    offset: i32,
37
    nbytes: i32,
38
    fv_idx: i32,
39
    value: DetectFloatData<f64>,
40
}
41
42
impl Default for DetectEntropyData {
43
2.84k
    fn default() -> Self {
44
2.84k
        DetectEntropyData {
45
2.84k
            offset: 0,
46
2.84k
            nbytes: 0,
47
2.84k
            fv_idx: 0,
48
2.84k
            value: DetectFloatData::<f64>::default(),
49
2.84k
        }
50
2.84k
    }
51
}
52
impl DetectEntropyData {
53
2.84k
    pub fn new() -> Self {
54
2.84k
        Self {
55
2.84k
            ..Default::default()
56
2.84k
        }
57
2.84k
    }
58
}
59
60
// All options have default values except for the entropy value
61
const DETECT_ENTROPY_FIXED_PARAM_COUNT: usize = 1;
62
const DETECT_ENTROPY_MAX_PARAM_COUNT: usize = 4;
63
pub const DETECT_ENTROPY_FLAG_BYTES: u8 = 0x01;
64
pub const DETECT_ENTROPY_FLAG_OFFSET: u8 = 0x02;
65
pub const DETECT_ENTROPY_FLAG_VALUE: u8 = 0x04;
66
67
2.84k
fn parse_entropy<'a>(
68
2.84k
    input: &'a str, flags: &'a mut u8,
69
2.84k
) -> IResult<&'a str, DetectEntropyData, RuleParseError<&'a str>> {
70
    // Inner utility function for easy error creation.
71
381
    fn make_error(reason: String) -> nom7::Err<RuleParseError<&'static str>> {
72
381
        Err::Error(RuleParseError::InvalidEntropy(reason))
73
381
    }
74
2.84k
    let (_, values) = nom7::multi::separated_list1(
75
2.84k
        tag(","),
76
2.84k
        preceded(multispace0, nom7::bytes::complete::is_not(",")),
77
2.84k
    )(input)?;
78
79
2.84k
    if values.len() < DETECT_ENTROPY_FIXED_PARAM_COUNT
80
2.84k
        || values.len() > DETECT_ENTROPY_MAX_PARAM_COUNT
81
    {
82
7
        return Err(make_error(format!("Incorrect argument string; at least {} values must be specified but no more than {}: {:?}",
83
7
            DETECT_ENTROPY_FIXED_PARAM_COUNT, DETECT_ENTROPY_MAX_PARAM_COUNT, input)));
84
2.84k
    }
85
86
2.84k
    let mut entropy = DetectEntropyData::new();
87
5.31k
    for value in values {
88
2.85k
        let (mut val, mut name) = take_until_whitespace(value)?;
89
2.85k
        val = val.trim();
90
2.85k
        name = name.trim();
91
2.85k
        match name {
92
2.85k
            "bytes" => {
93
3
                if 0 != (*flags & DETECT_ENTROPY_FLAG_BYTES) {
94
0
                    return Err(make_error("bytes already set".to_string()));
95
3
                }
96
3
                entropy.nbytes = val
97
3
                    .parse::<i32>()
98
3
                    .map_err(|_| make_error(format!("invalid bytes value: {}", val)))?;
99
1
                *flags |= DETECT_ENTROPY_FLAG_BYTES;
100
            }
101
2.84k
            "offset" => {
102
5
                if 0 != (*flags & DETECT_ENTROPY_FLAG_OFFSET) {
103
0
                    return Err(make_error("offset already set".to_string()));
104
5
                }
105
5
                entropy.offset = val
106
5
                    .parse::<i32>()
107
5
                    .map_err(|_| make_error(format!("invalid offset value: {}", val)))?;
108
4
                if entropy.offset > 65535 || entropy.offset < -65535 {
109
1
                    return Err(make_error(format!(
110
1
                        "invalid offset value: must be between -65535 and 65535: {}",
111
1
                        val
112
1
                    )));
113
3
                }
114
3
                *flags |= DETECT_ENTROPY_FLAG_OFFSET;
115
            }
116
2.84k
            "value" => {
117
2.78k
                if 0 != (*flags & DETECT_ENTROPY_FLAG_VALUE) {
118
1
                    return Err(make_error("value already set".to_string()));
119
2.78k
                }
120
2.78k
                if let Ok((_, ctx)) = detect_parse_float::<f64>(val) {
121
2.47k
                    entropy.value = ctx;
122
2.47k
                    *flags |= DETECT_ENTROPY_FLAG_VALUE;
123
2.47k
                } else {
124
312
                    return Err(make_error(format!("invalid entropy value: {}", val)));
125
                }
126
            }
127
            _ => {
128
56
                return Err(make_error(format!("unknown entropy option: {}", name)));
129
            }
130
        };
131
    }
132
133
    // an entropy value is required
134
2.46k
    if (*flags & DETECT_ENTROPY_FLAG_VALUE) != DETECT_ENTROPY_FLAG_VALUE {
135
1
        return Err(make_error(format!(
136
1
            "required entropy parameter missing: \"{:?}\"",
137
1
            input
138
1
        )));
139
2.46k
    }
140
141
2.46k
    Ok((input, entropy))
142
2.84k
}
143
144
77
fn calculate_entropy(data: &[u8]) -> f64 {
145
77
    if data.is_empty() {
146
0
        return 0.0;
147
77
    }
148
149
    // Use a 256-element array to store byte frequencies
150
77
    let mut frequency = [0u32; 256];
151
152
    // Calculate the frequency of each byte
153
89.8k
    for &byte in data.iter() {
154
89.8k
        frequency[byte as usize] += 1;
155
89.8k
    }
156
157
    // Calculate entropy using byte frequencies
158
77
    let length_f64 = data.len() as f64;
159
19.7k
    frequency.iter().fold(0.0, |entropy, &count| {
160
19.7k
        if count > 0 {
161
4.87k
            let probability = count as f64 / length_f64;
162
4.87k
            entropy - probability * probability.log2()
163
        } else {
164
14.8k
            entropy
165
        }
166
19.7k
    })
167
77
}
168
169
#[no_mangle]
170
77
pub unsafe extern "C" fn SCDetectEntropyMatch(
171
77
    c_data: *const c_void, length: i32, ctx: &DetectEntropyData,
172
77
    calculated_entropy: *mut c_double,
173
77
) -> bool {
174
77
    if c_data.is_null() {
175
0
        return false;
176
77
    }
177
178
77
    let buffer = std::slice::from_raw_parts(c_data as *const u8, length as usize);
179
77
    let mut start = buffer;
180
77
    let mut count = length;
181
182
    // Adjust start and count based on offset and nbytes from context
183
77
    if ctx.offset > 0 {
184
0
        let offset = ctx.offset;
185
0
        if offset > count {
186
            SCLogDebug!("offset {} exceeds buffer length {}", offset, count);
187
0
            return false;
188
0
        }
189
0
        start = &start[offset as usize..];
190
0
        count -= offset;
191
77
    }
192
193
77
    if ctx.nbytes > 0 {
194
0
        let nbytes = ctx.nbytes;
195
0
        if nbytes > count {
196
            SCLogDebug!("byte count {} exceeds buffer length {}", nbytes, count);
197
0
            return false;
198
0
        }
199
0
        count = nbytes;
200
77
    }
201
202
    // Calculate entropy based on the adjusted buffer slice
203
77
    let data_slice = slice::from_raw_parts(start.as_ptr(), count as usize);
204
77
    let entropy = calculate_entropy(data_slice);
205
    SCLogDebug!("entropy is {}", entropy);
206
207
    // Return entropy on request
208
77
    if !calculated_entropy.is_null() {
209
77
        *calculated_entropy = entropy;
210
77
    }
211
212
77
    detect_match_float::<f64>(&ctx.value, entropy)
213
77
}
214
215
#[no_mangle]
216
2.84k
pub unsafe extern "C" fn SCDetectEntropyParse(c_arg: *const c_char) -> *mut DetectEntropyData {
217
2.84k
    if c_arg.is_null() {
218
0
        return std::ptr::null_mut();
219
2.84k
    }
220
221
2.84k
    if let Ok(arg) = CStr::from_ptr(c_arg).to_str() {
222
2.84k
        let mut flags = 0;
223
2.84k
        match parse_entropy(arg, &mut flags) {
224
2.46k
            Ok((_, detect)) => return Box::into_raw(Box::new(detect)),
225
382
            Err(_) => return std::ptr::null_mut(),
226
        }
227
0
    }
228
0
    return std::ptr::null_mut();
229
2.84k
}
230
231
#[no_mangle]
232
2.46k
pub unsafe extern "C" fn SCDetectEntropyFree(ptr: *mut c_void) {
233
2.46k
    if !ptr.is_null() {
234
2.46k
        let _ = Box::from_raw(ptr as *mut DetectEntropyData);
235
2.46k
    }
236
2.46k
}
237
238
#[cfg(test)]
239
mod tests {
240
    use super::*;
241
    use crate::detect::float::DetectFloatMode;
242
    use num::traits::float::FloatCore;
243
    // structure equality only used by test cases
244
    impl PartialEq for DetectEntropyData {
245
        fn eq(&self, other: &Self) -> bool {
246
            self.value == other.value && self.offset == other.offset && self.nbytes == other.nbytes
247
        }
248
    }
249
250
    fn valid_test(
251
        args: &str, nbytes: i32, offset: i32, value: f64, mode: DetectFloatMode, flags: u8,
252
    ) {
253
        let ctx = DetectFloatData {
254
            arg1: value,
255
            arg2: FloatCore::min_value(),
256
            mode,
257
        };
258
        let ded = DetectEntropyData {
259
            offset,
260
            nbytes,
261
            fv_idx: 0,
262
            value: ctx,
263
        };
264
265
        let mut parsed_flags = 0;
266
        let (_, val) = parse_entropy(args, &mut parsed_flags).unwrap();
267
        assert_eq!(flags, parsed_flags);
268
        assert_eq!(val, ded);
269
    }
270
271
    #[test]
272
    fn test_parse_entropy_valid() {
273
        valid_test(
274
            "value 7",
275
            0,
276
            0,
277
            7.0,
278
            DetectFloatMode::DetectFloatModeEqual,
279
            DETECT_ENTROPY_FLAG_VALUE,
280
        );
281
        valid_test(
282
            "bytes 4, value >= 7",
283
            4,
284
            0,
285
            7.0,
286
            DetectFloatMode::DetectFloatModeGte,
287
            DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES,
288
        );
289
        valid_test(
290
            "bytes 4, value != 7",
291
            4,
292
            0,
293
            7.0,
294
            DetectFloatMode::DetectFloatModeNe,
295
            DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES,
296
        );
297
        valid_test(
298
            "bytes 4, value <7",
299
            4,
300
            0,
301
            7.0,
302
            DetectFloatMode::DetectFloatModeLt,
303
            DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES,
304
        );
305
        valid_test(
306
            "bytes 4, value <= 7",
307
            4,
308
            0,
309
            7.0,
310
            DetectFloatMode::DetectFloatModeLte,
311
            DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES,
312
        );
313
        valid_test(
314
            "bytes 4, value = 7",
315
            4,
316
            0,
317
            7.0,
318
            DetectFloatMode::DetectFloatModeEqual,
319
            DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES,
320
        );
321
        valid_test(
322
            "bytes 4, value > 7",
323
            4,
324
            0,
325
            7.0,
326
            DetectFloatMode::DetectFloatModeGt,
327
            DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES,
328
        );
329
        valid_test(
330
            "bytes 4, offset 30, value > 7",
331
            4,
332
            30,
333
            7.0,
334
            DetectFloatMode::DetectFloatModeGt,
335
            DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES | DETECT_ENTROPY_FLAG_OFFSET,
336
        );
337
        valid_test(
338
            "bytes 4, offset 30, value 7",
339
            4,
340
            30,
341
            7.0,
342
            DetectFloatMode::DetectFloatModeEqual,
343
            DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES | DETECT_ENTROPY_FLAG_OFFSET,
344
        );
345
        valid_test(
346
            "bytes 4, offset 30,  value < 7",
347
            4,
348
            30,
349
            7.0,
350
            DetectFloatMode::DetectFloatModeLt,
351
            DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES | DETECT_ENTROPY_FLAG_OFFSET,
352
        );
353
        valid_test(
354
            "bytes 4, offset 30,value <= 7",
355
            4,
356
            30,
357
            7.0,
358
            DetectFloatMode::DetectFloatModeLte,
359
            DETECT_ENTROPY_FLAG_VALUE | DETECT_ENTROPY_FLAG_BYTES | DETECT_ENTROPY_FLAG_OFFSET,
360
        );
361
    }
362
363
    #[test]
364
    fn test_parse_entropy_invalid() {
365
        let mut parsed_flags = 0;
366
        assert!(parse_entropy("", &mut parsed_flags,).is_err());
367
        assert!(parse_entropy("value ? 7.0", &mut parsed_flags,).is_err());
368
        assert!(parse_entropy("bytes 100", &mut parsed_flags,).is_err());
369
        assert!(parse_entropy("offset 100", &mut parsed_flags,).is_err());
370
        assert!(parse_entropy("bytes 100, offset 100", &mut parsed_flags,).is_err());
371
        assert!(parse_entropy("bytes 1, offset 10, value 7.0, extra", &mut parsed_flags,).is_err());
372
    }
373
374
    #[test]
375
    fn test_entropy_calculation() {
376
        // Test data
377
        let data = b"aaaaaaa"; // All the same byte
378
379
        // Calculate entropy
380
        let data_slice = unsafe { slice::from_raw_parts(data.as_ptr(), data.len()) };
381
        let entropy = calculate_entropy(data_slice);
382
383
        // Expected entropy is 0 (no randomness)
384
        assert!(
385
            (entropy - 0.0).abs() < 1e-6,
386
            "Entropy should be 0.0 for identical bytes"
387
        );
388
389
        // Test data with more randomness
390
        let data = b"abcdabcd"; // Equal distribution
391
392
        // Calculate entropy
393
        let data_slice = unsafe { slice::from_raw_parts(data.as_ptr(), data.len()) };
394
        let entropy = calculate_entropy(data_slice);
395
396
        // Expected entropy is 2 (each byte has 1/4 probability)
397
        assert!(
398
            (entropy - 2.0).abs() < 1e-6,
399
            "Entropy should be 2.0 for uniform distribution of 4 values"
400
        );
401
402
        // Test empty data
403
        let data: [u8; 0] = [];
404
405
        // Calculate entropy
406
        let data_slice = unsafe { slice::from_raw_parts(data.as_ptr(), data.len()) };
407
        let entropy = calculate_entropy(data_slice);
408
409
        // Expected entropy is 0 (no data)
410
        assert!(
411
            (entropy - 0.0).abs() < 1e-6,
412
            "Entropy should be 0.0 for empty data"
413
        );
414
415
        // Test mixed data
416
        let data = b"aaabbcc";
417
418
        // Calculate entropy
419
        let data_slice = unsafe { slice::from_raw_parts(data.as_ptr(), data.len()) };
420
        let entropy = calculate_entropy(data_slice);
421
422
        // Verify entropy is non-zero and less than maximum
423
        assert!(
424
            entropy > 0.0 && entropy <= 8.0,
425
            "Entropy should be between 0.0 and 8.0"
426
        );
427
    }
428
}