vrl/stdlib/
parse_cef.rs

1use crate::compiler::prelude::*;
2use nom::{
3    self, IResult, Parser,
4    branch::alt,
5    bytes::complete::{escaped_transform, tag, take_till1, take_until},
6    character::complete::{char, one_of, satisfy},
7    combinator::{map, opt, peek, success, value},
8    error::{ErrorKind, ParseError},
9    multi::{count, many1},
10    sequence::{delimited, pair, preceded},
11};
12use nom_language::error::VerboseError;
13use std::collections::{BTreeMap, HashMap};
14use std::sync::LazyLock;
15
16static DEFAULT_TRANSLATE_CUSTOM_FIELDS: LazyLock<Value> = LazyLock::new(|| Value::Boolean(false));
17
18static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
19    vec![
20        Parameter::required("value", kind::BYTES, "The string to parse."),
21        Parameter::optional(
22            "translate_custom_fields",
23            kind::BOOLEAN,
24            "Toggles translation of custom field pairs to `key:value`.",
25        )
26        .default(&DEFAULT_TRANSLATE_CUSTOM_FIELDS),
27    ]
28});
29
30fn build_map() -> HashMap<&'static str, (usize, CustomField)> {
31    [
32        ("c6a1Label", "c6a1"),
33        ("c6a2Label", "c6a2"),
34        ("c6a3Label", "c6a3"),
35        ("c6a4Label", "c6a4"),
36        ("cfp1Label", "cfp1"),
37        ("cfp2Label", "cfp2"),
38        ("cfp3Label", "cfp3"),
39        ("cfp4Label", "cfp4"),
40        ("cn1Label", "cn1"),
41        ("cn2Label", "cn2"),
42        ("cn3Label", "cn3"),
43        ("cs1Label", "cs1"),
44        ("cs2Label", "cs2"),
45        ("cs3Label", "cs3"),
46        ("cs4Label", "cs4"),
47        ("cs5Label", "cs5"),
48        ("cs6Label", "cs6"),
49        ("deviceCustomDate1Label", "deviceCustomDate1"),
50        ("deviceCustomDate2Label", "deviceCustomDate2"),
51        ("flexDate1Label", "flexDate1"),
52        ("flexString1Label", "flexString1"),
53        ("flexString2Label", "flexString2"),
54    ]
55    .iter()
56    .enumerate()
57    .flat_map(|(i, (k, v))| [(*k, (i, CustomField::Label)), (*v, (i, CustomField::Value))])
58    .collect()
59}
60
61#[derive(Clone, Copy, Debug)]
62pub struct ParseCef;
63
64impl Function for ParseCef {
65    fn identifier(&self) -> &'static str {
66        "parse_cef"
67    }
68
69    fn usage(&self) -> &'static str {
70        "Parses the `value` in CEF (Common Event Format) format. Ignores everything up to CEF header. Empty values are returned as empty strings. Surrounding quotes are removed from values."
71    }
72
73    fn category(&self) -> &'static str {
74        Category::Parse.as_ref()
75    }
76
77    fn internal_failure_reasons(&self) -> &'static [&'static str] {
78        &["`value` is not a properly formatted CEF string."]
79    }
80
81    fn return_kind(&self) -> u16 {
82        kind::OBJECT
83    }
84
85    fn notices(&self) -> &'static [&'static str] {
86        &[indoc! {"
87            All values are returned as strings. We recommend manually coercing values to desired
88            types as you see fit.
89        "}]
90    }
91
92    fn parameters(&self) -> &'static [Parameter] {
93        PARAMETERS.as_slice()
94    }
95
96    fn examples(&self) -> &'static [Example] {
97        &[
98            example! {
99                title: "Parse output generated by PTA",
100                source: indoc! {r#"
101                    parse_cef!(
102                        "CEF:0|CyberArk|PTA|12.6|1|Suspected credentials theft|8|suser=mike2@prod1.domain.com shost=prod1.domain.com src=1.1.1.1 duser=andy@dev1.domain.com dhost=dev1.domain.com dst=2.2.2.2 cs1Label=ExtraData cs1=None cs2Label=EventID cs2=52b06812ec3500ed864c461e deviceCustomDate1Label=detectionDate deviceCustomDate1=1388577900000 cs3Label=PTAlink cs3=https://1.1.1.1/incidents/52b06812ec3500ed864c461e cs4Label=ExternalLink cs4=None"
103                    )
104                "#},
105                result: Ok(
106                    r#"{"cefVersion":"0","deviceVendor":"CyberArk","deviceProduct":"PTA","deviceVersion":"12.6","deviceEventClassId":"1","name":"Suspected credentials theft","severity":"8","suser":"mike2@prod1.domain.com","shost":"prod1.domain.com","src":"1.1.1.1","duser":"andy@dev1.domain.com","dhost":"dev1.domain.com","dst":"2.2.2.2","cs1Label":"ExtraData","cs1":"None","cs2Label":"EventID","cs2":"52b06812ec3500ed864c461e","deviceCustomDate1Label":"detectionDate","deviceCustomDate1":"1388577900000","cs3Label":"PTAlink","cs3":"https://1.1.1.1/incidents/52b06812ec3500ed864c461e","cs4Label":"ExternalLink","cs4":"None"}"#,
107                ),
108            },
109            example! {
110                title: "Ignore syslog header",
111                source: indoc! {r#"
112                    parse_cef!(
113                        "Sep 29 08:26:10 host CEF:1|Security|threatmanager|1.0|100|worm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2 spt=1232"
114                    )
115                "#},
116                result: Ok(
117                    r#"{"cefVersion":"1","deviceVendor":"Security","deviceProduct":"threatmanager","deviceVersion":"1.0","deviceEventClassId":"100","name":"worm successfully stopped","severity":"10","src":"10.0.0.1","dst":"2.1.2.2","spt":"1232"}"#,
118                ),
119            },
120            example! {
121                title: "Translate custom fields",
122                source: indoc! {r#"
123                    parse_cef!(
124                        "CEF:0|Dev|firewall|2.2|1|Connection denied|5|c6a1=2345:0425:2CA1:0000:0000:0567:5673:23b5 c6a1Label=Device IPv6 Address",
125                        translate_custom_fields: true
126                    )
127                "#},
128                result: Ok(
129                    r#"{"cefVersion":"0","deviceVendor":"Dev","deviceProduct":"firewall","deviceVersion":"2.2","deviceEventClassId":"1","name":"Connection denied","severity":"5","Device IPv6 Address":"2345:0425:2CA1:0000:0000:0567:5673:23b5"}"#,
130                ),
131            },
132            example! {
133                title: "Parse CEF with only header",
134                source: r#"parse_cef!("CEF:1|Security|threatmanager|1.0|100|worm successfully stopped|10|")"#,
135                result: Ok(
136                    r#"{"cefVersion":"1","deviceVendor":"Security","deviceProduct":"threatmanager","deviceVersion":"1.0","deviceEventClassId":"100","name":"worm successfully stopped","severity":"10"}"#,
137                ),
138            },
139            example! {
140                title: "Parse CEF with empty value",
141                source: r#"parse_cef!("CEF:0|CyberArk|PTA|12.6|1|Suspected credentials theft||suser=mike2@prod1.domain.com shost= src=1.1.1.1")"#,
142                result: Ok(
143                    r#"{"cefVersion":"0","deviceVendor":"CyberArk","deviceProduct":"PTA","deviceVersion":"12.6","deviceEventClassId":"1","name":"Suspected credentials theft","severity":"","suser":"mike2@prod1.domain.com","shost":"","src":"1.1.1.1"}"#,
144                ),
145            },
146            example! {
147                title: "Parse CEF with escapes",
148                source: r"parse_cef!(s'CEF:0|security|threatmanager|1.0|100|Detected a \| in message. No action needed.|10|src=10.0.0.1 msg=Detected a threat.\n No action needed act=blocked a \= dst=1.1.1.1')",
149                result: Ok(
150                    r#"{"cefVersion":"0","deviceVendor":"security","deviceProduct":"threatmanager","deviceVersion":"1.0","deviceEventClassId":"100","name":"Detected a | in message. No action needed.","severity":"10","src":"10.0.0.1","msg":"Detected a threat.\n No action needed","act":"blocked a =", "dst":"1.1.1.1"}"#,
151                ),
152            },
153        ]
154    }
155
156    fn compile(
157        &self,
158        _state: &state::TypeState,
159        _ctx: &mut FunctionCompileContext,
160        arguments: ArgumentList,
161    ) -> Compiled {
162        let value = arguments.required("value");
163        let translate_custom_fields = arguments.optional("translate_custom_fields");
164        let custom_field_map = build_map();
165
166        Ok(ParseCefFn {
167            value,
168            translate_custom_fields,
169            custom_field_map,
170        }
171        .as_expr())
172    }
173}
174
175#[derive(Clone, Debug)]
176pub(crate) struct ParseCefFn {
177    pub(crate) value: Box<dyn Expression>,
178    pub(crate) translate_custom_fields: Option<Box<dyn Expression>>,
179    custom_field_map: HashMap<&'static str, (usize, CustomField)>,
180}
181
182impl FunctionExpression for ParseCefFn {
183    fn resolve(&self, ctx: &mut Context) -> Resolved {
184        let bytes = self.value.resolve(ctx)?;
185        let bytes = bytes.try_bytes_utf8_lossy()?;
186        let translate_custom_fields = self
187            .translate_custom_fields
188            .map_resolve_with_default(ctx, || DEFAULT_TRANSLATE_CUSTOM_FIELDS.clone())?
189            .try_boolean()?;
190
191        let result = parse(&bytes)?;
192
193        if translate_custom_fields {
194            let mut custom_fields = HashMap::<_, [Option<String>; 2]>::new();
195
196            let mut result = result
197                .filter_map(|(k, v)| {
198                    if let Some(&(i, custom_field)) = self.custom_field_map.get(&k[..]) {
199                        let previous =
200                            custom_fields.entry(i).or_default()[custom_field as usize].replace(v);
201                        if previous.is_some() {
202                            return Some(Err(format!(
203                                "Custom field with duplicate {}",
204                                match custom_field {
205                                    CustomField::Label => "label",
206                                    CustomField::Value => "value",
207                                }
208                            )
209                            .into()));
210                        }
211                        None
212                    } else {
213                        Some(Ok((k.into(), v.into())))
214                    }
215                })
216                .collect::<ExpressionResult<ObjectMap>>()?;
217
218            for (_, fields) in custom_fields {
219                match fields {
220                    [Some(label), value] => {
221                        result.insert(label.into(), value.into());
222                    }
223                    _ => return Err("Custom field with missing label or value".into()),
224                }
225            }
226
227            Ok(Value::Object(result))
228        } else {
229            Ok(result.map(|(k, v)| (k, v.into())).collect())
230        }
231    }
232
233    fn type_def(&self, _: &state::TypeState) -> TypeDef {
234        type_def()
235    }
236}
237
238#[derive(Debug, Clone, Copy)]
239enum CustomField {
240    Label = 0,
241    Value = 1,
242}
243
244fn parse(input: &str) -> ExpressionResult<impl Iterator<Item = (String, String)> + '_> {
245    let (rest, (header, mut extension)) = pair(parse_header, parse_extension)
246        .parse(input)
247        .map_err(|e| match e {
248            nom::Err::Error(e) | nom::Err::Failure(e) => {
249                // Create a descriptive error message if possible.
250                nom_language::error::convert_error(input, e)
251            }
252            nom::Err::Incomplete(_) => e.to_string(),
253        })?;
254
255    // Trim trailing whitespace on last extension value
256    if let Some((_, value)) = extension.last_mut() {
257        let suffix = value.trim_end_matches(' ');
258        value.truncate(suffix.len());
259    }
260
261    if rest.trim().is_empty() {
262        let headers = [
263            "cefVersion",
264            "deviceVendor",
265            "deviceProduct",
266            "deviceVersion",
267            "deviceEventClassId",
268            "name",
269            "severity",
270        ]
271        .into_iter()
272        .zip(header);
273        let result = extension
274            .into_iter()
275            .chain(headers)
276            .map(|(key, mut value)| {
277                // Strip quotes from value
278                if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
279                    value = value[1..value.len() - 1].to_string();
280                }
281                (key, value)
282            })
283            .map(|(key, value)| (key.to_string(), value));
284
285        Ok(result)
286    } else {
287        Err("Could not parse whole line successfully".into())
288    }
289}
290
291fn parse_header(input: &str) -> IResult<&str, Vec<String>, VerboseError<&str>> {
292    preceded(
293        pair(take_until("CEF:"), tag("CEF:")),
294        count(parse_header_value, 7),
295    )
296    .parse(input)
297}
298
299fn parse_header_value(input: &str) -> IResult<&str, String, VerboseError<&str>> {
300    preceded(
301        opt(char('|')),
302        alt((
303            map(peek(char('|')), |_| String::new()),
304            map(
305                escaped_transform(
306                    take_till1(|c: char| c == '\\' || c == '|'),
307                    '\\',
308                    satisfy(|c| c == '\\' || c == '|'),
309                ),
310                |value: String| value.trim().to_string(),
311            ),
312        )),
313    )
314    .parse(input)
315}
316
317fn parse_extension(input: &str) -> IResult<&str, Vec<(&str, String)>, VerboseError<&str>> {
318    alt((many1(parse_key_value), map(tag("|"), |_| vec![]))).parse(input)
319}
320
321fn parse_key_value(input: &str) -> IResult<&str, (&str, String), VerboseError<&str>> {
322    pair(parse_key, parse_value).parse(input)
323}
324
325fn parse_value(input: &str) -> IResult<&str, String, VerboseError<&str>> {
326    alt((
327        map(peek(parse_key), |_| String::new()),
328        escaped_transform(
329            take_till1_input(|input| alt((tag("\\"), tag("="), parse_key)).parse(input).is_ok()),
330            '\\',
331            alt((
332                value('=', char('=')),
333                value('\\', char('\\')),
334                value('\n', one_of("nr")),
335                success('\\'),
336            )),
337        ),
338    ))
339    .parse(input)
340}
341
342/// As take `take_till1` but can have condition on input instead of `Input::Item`.
343fn take_till1_input<'a, F: Fn(&'a str) -> bool, Error: ParseError<&'a str>>(
344    cond: F,
345) -> impl Fn(&'a str) -> IResult<&'a str, &'a str, Error> {
346    move |input: &'a str| {
347        for (i, _) in input.char_indices() {
348            if cond(&input[i..]) {
349                return if i == 0 {
350                    Err(nom::Err::Error(Error::from_error_kind(
351                        input,
352                        ErrorKind::TakeTill1,
353                    )))
354                } else {
355                    Ok((&input[i..], &input[..i]))
356                };
357            }
358        }
359        Ok(("", input))
360    }
361}
362
363fn parse_key(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
364    delimited(
365        many1(alt((char(' '), char('|')))),
366        take_till1(|c| c == ' ' || c == '=' || c == '\\'),
367        char('='),
368    )
369    .parse(input)
370}
371
372fn type_def() -> TypeDef {
373    TypeDef::object(Collection::from_parts(
374        BTreeMap::from([
375            (Field::from("cefVersion"), Kind::bytes()),
376            (Field::from("deviceVendor"), Kind::bytes()),
377            (Field::from("deviceProduct"), Kind::bytes()),
378            (Field::from("deviceVersion"), Kind::bytes()),
379            (Field::from("deviceEventClassId"), Kind::bytes()),
380            (Field::from("name"), Kind::bytes()),
381            (Field::from("severity"), Kind::bytes()),
382        ]),
383        Kind::bytes(),
384    ))
385    .fallible()
386}
387
388#[cfg(test)]
389mod test {
390    use super::*;
391    use crate::value;
392
393    #[test]
394    fn test_parse_header() {
395        assert_eq!(
396            Ok(vec![
397                ("cefVersion".to_string(), "1".into()),
398                ("deviceVendor".to_string(), "Security".into()),
399                ("deviceProduct".to_string(), "threatmanager".into()),
400                ("deviceVersion".to_string(), "1.0".into()),
401                ("deviceEventClassId".to_string(), "100".into()),
402                ("name".to_string(), "worm successfully stopped".into()),
403                ("severity".to_string(), "10".into()),
404            ]),
405            parse("CEF:1|Security|threatmanager|1.0|100|worm successfully stopped|10|")
406                .map(Iterator::collect)
407        );
408    }
409
410    #[test]
411    fn test_parse_extension() {
412        assert_eq!(
413            Ok(vec![
414                ("src".to_string(), "10.0.0.1".into()),
415                ("dst".to_string(), "2.1.2.2".into()),
416                ("spt".to_string(),"1232".into()),
417                ("cefVersion".to_string(), "1".into()),
418                ("deviceVendor".to_string(), "Security".into()),
419                ("deviceProduct".to_string(), "threatmanager".into()),
420                ("deviceVersion".to_string(), "1.0".into()),
421                ("deviceEventClassId".to_string(), "100".into()),
422                ("name".to_string(), "worm successfully stopped".into()),
423                ("severity".to_string(), "10".into()),
424            ]),
425            parse("CEF:1|Security|threatmanager|1.0|100|worm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2 spt=1232")
426                .map(Iterator::collect)
427        );
428    }
429
430    #[test]
431    fn test_parse_empty_value() {
432        assert_eq!(
433            Ok(vec![
434                ("src".to_string(), String::new()),
435                ("dst".to_string(), "2.1.2.2".into()),
436                ("cefVersion".to_string(), "1".into()),
437                ("deviceVendor".to_string(), "Security".into()),
438                ("deviceProduct".to_string(), "threatmanager".into()),
439                ("deviceVersion".to_string(), String::new()),
440                ("deviceEventClassId".to_string(), "100".into()),
441                ("name".to_string(), "worm successfully stopped".into()),
442                ("severity".to_string(), String::new()),
443            ]),
444            parse("CEF:1|Security|threatmanager||100|worm successfully stopped||src= dst=2.1.2.2")
445                .map(Iterator::collect)
446        );
447    }
448
449    #[test]
450    fn test_strip_quotes() {
451        assert_eq!(
452            Ok(vec![
453                ("src".to_string(), "10.0.0.1".into()),
454                ("dst".to_string(), "2.1.2.2".into()),
455                ("spt".to_string(),"1232".into()),
456                ("cefVersion".to_string(), "1".into()),
457                ("deviceVendor".to_string(), "Security".into()),
458                ("deviceProduct".to_string(), "threatmanager".into()),
459                ("deviceVersion".to_string(), "1.0".into()),
460                ("deviceEventClassId".to_string(), "100".into()),
461                ("name".to_string(), "worm successfully stopped".into()),
462                ("severity".to_string(), "10".into()),
463            ]),
464            parse(r#"CEF:1|"Security"|threatmanager|1.0|100|"worm successfully stopped"|10|src="10.0.0.1" dst=2.1.2.2 spt="1232""#)
465                .map(Iterator::collect)
466        );
467    }
468
469    #[test]
470    fn test_ignore_syslog_prefix() {
471        assert_eq!(
472            Ok(vec![
473                ("src".to_string(), "10.0.0.1".into()),
474                ("dst".to_string(), "2.1.2.2".into()),
475                ("spt".to_string(),"1232".into()),
476                ("cefVersion".to_string(), "1".into()),
477                ("deviceVendor".to_string(), "Security".into()),
478                ("deviceProduct".to_string(), "threatmanager".into()),
479                ("deviceVersion".to_string(), "1.0".into()),
480                ("deviceEventClassId".to_string(), "100".into()),
481                ("name".to_string(), "worm successfully stopped".into()),
482                ("severity".to_string(), "10".into()),
483            ]),
484            parse("Sep 29 08:26:10 host CEF:1|Security|threatmanager|1.0|100|worm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2 spt=1232")
485                .map(Iterator::collect)
486        );
487    }
488
489    #[test]
490    fn test_escape_header_1() {
491        assert_eq!(
492            Ok(vec![
493                ("cefVersion".to_string(), "1".into()),
494                ("deviceVendor".to_string(), "Security".into()),
495                ("deviceProduct".to_string(), "threatmanager".into()),
496                ("deviceVersion".to_string(), "1.0".into()),
497                ("deviceEventClassId".to_string(), "100".into()),
498                ("name".to_string(), "worm | successfully | stopped".into()),
499                ("severity".to_string(), "10".into()),
500            ]),
501            parse(r"CEF:1|Security|threatmanager|1.0|100|worm \| successfully \| stopped|10|")
502                .map(Iterator::collect)
503        );
504    }
505
506    #[test]
507    fn test_escape_header_2() {
508        assert_eq!(
509            Ok(vec![
510                ("cefVersion".to_string(), "1".into()),
511                ("deviceVendor".to_string(), "Security".into()),
512                ("deviceProduct".to_string(), "threatmanager".into()),
513                ("deviceVersion".to_string(), "1.0".into()),
514                ("deviceEventClassId".to_string(), "100".into()),
515                ("name".to_string(), "worm \\ successfully \\ stopped".into()),
516                ("severity".to_string(), "10".into()),
517            ]),
518            parse(r"CEF:1|Security|threatmanager|1.0|100|worm \\ successfully \\ stopped|10|")
519                .map(Iterator::collect)
520        );
521    }
522
523    #[test]
524    fn test_escape_extension_1() {
525        assert_eq!(
526            Ok(vec![
527                ("src".to_string(), "ip=10.0.0.1".into()),
528                ("dst".to_string(), "2.1.2.2".into()),
529                ("spt".to_string(),"1232".into()),
530                ("cefVersion".to_string(), "1".into()),
531                ("deviceVendor".to_string(), "Security".into()),
532                ("deviceProduct".to_string(), "threatmanager".into()),
533                ("deviceVersion".to_string(), "1.0".into()),
534                ("deviceEventClassId".to_string(), "100".into()),
535                ("name".to_string(), "worm successfully stopped".into()),
536                ("severity".to_string(), "10".into()),
537            ]),
538            parse(r"CEF:1|Security|threatmanager|1.0|100|worm successfully stopped|10|src=ip\=10.0.0.1 dst=2.1.2.2 spt=1232")
539                .map(Iterator::collect)
540        );
541    }
542
543    #[test]
544    fn test_escape_extension_2() {
545        assert_eq!(
546            Ok(vec![
547                ("dst".to_string(), "2.1.2.2".into()),
548                ("path".to_string(), "\\home\\".into()),
549                ("spt".to_string(),"1232".into()),
550                ("cefVersion".to_string(), "1".into()),
551                ("deviceVendor".to_string(), "Security".into()),
552                ("deviceProduct".to_string(), "threatmanager".into()),
553                ("deviceVersion".to_string(), "1.0".into()),
554                ("deviceEventClassId".to_string(), "100".into()),
555                ("name".to_string(), "worm successfully stopped".into()),
556                ("severity".to_string(), "10".into()),
557            ]),
558            parse(r"CEF:1|Security|threatmanager|1.0|100|worm successfully stopped|10|dst=2.1.2.2 path=\\home\\ spt=1232")
559                .map(Iterator::collect)
560        );
561    }
562
563    #[test]
564    fn test_extension_newline() {
565        assert_eq!(
566            Ok(vec![
567                ("dst".to_string(), "2.1.2.2".into()),
568                ("msg".to_string(), "Detected a threat.\n No action needed".into()),
569                ("spt".to_string(),"1232".into()),
570                ("cefVersion".to_string(), "1".into()),
571                ("deviceVendor".to_string(), "Security".into()),
572                ("deviceProduct".to_string(), "threatmanager".into()),
573                ("deviceVersion".to_string(), "1.0".into()),
574                ("deviceEventClassId".to_string(), "100".into()),
575                ("name".to_string(), "worm successfully stopped".into()),
576                ("severity".to_string(), "10".into()),
577            ]),
578            parse(r"CEF:1|Security|threatmanager|1.0|100|worm successfully stopped|10|dst=2.1.2.2 msg=Detected a threat.\r No action needed spt=1232")
579                .map(Iterator::collect)
580        );
581    }
582
583    #[test]
584    fn test_extension_trailing_whitespace() {
585        assert_eq!(
586            Ok(vec![
587                ("dst".to_string(), "2.1.2.2".into()),
588                ("msg".to_string(), "Detected a threat. No action needed".into()),
589                ("spt".to_string(),"1232".into()),
590                ("cefVersion".to_string(), "1".into()),
591                ("deviceVendor".to_string(), "Security".into()),
592                ("deviceProduct".to_string(), "threatmanager".into()),
593                ("deviceVersion".to_string(), "1.0".into()),
594                ("deviceEventClassId".to_string(), "100".into()),
595                ("name".to_string(), "worm successfully stopped".into()),
596                ("severity".to_string(), "10".into()),
597            ]),
598            parse("CEF:1|Security|threatmanager|1.0|100|worm successfully stopped|10|dst=2.1.2.2 msg=Detected a threat. No action needed   spt=1232")
599                .map(Iterator::collect)
600        );
601    }
602
603    #[test]
604    fn test_extension_end_whitespace() {
605        assert_eq!(
606            Ok(vec![
607                ("dst".to_string(), "2.1.2.2".into()),
608                ("msg".to_string(), "Detected a threat. No action needed".into()),
609                ("cefVersion".to_string(), "1".into()),
610                ("deviceVendor".to_string(), "Security".into()),
611                ("deviceProduct".to_string(), "threatmanager".into()),
612                ("deviceVersion".to_string(), "1.0".into()),
613                ("deviceEventClassId".to_string(), "100".into()),
614                ("name".to_string(), "worm successfully stopped".into()),
615                ("severity".to_string(), "10".into()),
616            ]),
617            parse("CEF:1|Security|threatmanager|1.0|100|worm successfully stopped|10|dst=2.1.2.2 msg=Detected a threat. No action needed   ")
618                .map(Iterator::collect)
619        );
620    }
621
622    #[test]
623    fn test_extension_space_after_separator() {
624        assert_eq!(
625            Ok(vec![
626                ("src".to_string(), "192.168.1.100".into()),
627                ("dst".to_string(), "10.0.0.5".into()),
628                ("spt".to_string(), "12345".into()),
629                ("dpt".to_string(), "80".into()),
630                ("proto".to_string(), "TCP".into()),
631                ("msg".to_string(), "Blocked unauthorized access attempt".into()),
632                ("act".to_string(), "blocked".into()),
633                ("outcome".to_string(), "failure".into()),
634                ("rt".to_string(), "2025-06-29T23:35:00Z".into()),
635                ("cefVersion".to_string(), "0".into()),
636                ("deviceVendor".to_string(), "SecurityVendor".into()),
637                ("deviceProduct".to_string(), "SecurityProduct".into()),
638                ("deviceVersion".to_string(), "1.0".into()),
639                ("deviceEventClassId".to_string(), "100".into()),
640                ("name".to_string(), "Unauthorized Access Attempt".into()),
641                ("severity".to_string(), "10".into()),
642            ]),
643            parse("CEF:0|SecurityVendor|SecurityProduct|1.0|100|Unauthorized Access Attempt|10| src=192.168.1.100 dst=10.0.0.5 spt=12345 dpt=80 proto=TCP msg=Blocked unauthorized access attempt act=blocked outcome=failure rt=2025-06-29T23:35:00Z")
644                .map(Iterator::collect)
645        );
646    }
647
648    test_function![
649        parse_cef => ParseCef;
650
651        default {
652            args: func_args! [
653                value: "CEF:0|CyberArk|PTA|12.6|1|Suspected credentials theft|8|suser=mike2@prod1.domain.com shost=prod1.domain.com src=1.1.1.1",
654            ],
655            want: Ok(value!({
656                "cefVersion":"0",
657                "deviceVendor":"CyberArk",
658                "deviceProduct":"PTA",
659                "deviceVersion":"12.6",
660                "deviceEventClassId":"1",
661                "name":"Suspected credentials theft",
662                "severity":"8",
663                "suser":"mike2@prod1.domain.com",
664                "shost":"prod1.domain.com",
665                "src":"1.1.1.1"
666            })),
667            tdef: type_def(),
668        }
669
670        real_case {
671            args: func_args! [
672                value: r"CEF:0|Check Point|VPN-1 & FireWall-1|Check Point|Log|https|Unknown|act=Accept destinationTranslatedAddress=0.0.0.0 destinationTranslatedPort=0 deviceDirection=0 rt=1543270652000 sourceTranslatedAddress=192.168.103.254 sourceTranslatedPort=35398 spt=49363 dpt=443 cs2Label=Rule Name layer_name=Network layer_uuid=b406b732-2437-4848-9741-6eae1f5bf112 match_id=4 parent_rule=0 rule_action=Accept rule_uid=9e5e6e74-aa9a-4693-b9fe-53712dd27bea ifname=eth0 logid=0 loguid={0x5bfc70fc,0x1,0xfe65a8c0,0xc0000001} origin=192.168.101.254 originsicname=CN\=R80,O\=R80_M..6u6bdo sequencenum=1 version=5 dst=52.173.84.157 inzone=Internal nat_addtnl_rulenum=1 nat_rulenum=4 outzone=External product=VPN-1 & FireWall-1 proto=6 service_id=https src=192.168.101.100",
673            ],
674            want: Ok(value!({
675                "cefVersion":"0",
676                "deviceVendor":"Check Point",
677                "deviceProduct":"VPN-1 & FireWall-1",
678                "deviceVersion":"Check Point",
679                "deviceEventClassId":"Log",
680                "name":"https",
681                "severity":"Unknown",
682                "act": "Accept",
683                "destinationTranslatedAddress": "0.0.0.0",
684                "destinationTranslatedPort": "0",
685                "deviceDirection": "0",
686                "rt": "1543270652000",
687                "sourceTranslatedAddress": "192.168.103.254",
688                "sourceTranslatedPort": "35398",
689                "spt": "49363",
690                "dpt": "443",
691                "cs2Label": "Rule Name",
692                "layer_name": "Network",
693                "layer_uuid": "b406b732-2437-4848-9741-6eae1f5bf112",
694                "match_id": "4",
695                "parent_rule": "0",
696                "rule_action": "Accept",
697                "rule_uid": "9e5e6e74-aa9a-4693-b9fe-53712dd27bea",
698                "ifname": "eth0",
699                "logid": "0",
700                "loguid": "{0x5bfc70fc,0x1,0xfe65a8c0,0xc0000001}",
701                "origin": "192.168.101.254",
702                "originsicname": "CN=R80,O=R80_M..6u6bdo",
703                "sequencenum": "1",
704                "version": "5",
705                "dst": "52.173.84.157",
706                "inzone": "Internal",
707                "nat_addtnl_rulenum": "1",
708                "nat_rulenum": "4",
709                "outzone": "External",
710                "product": "VPN-1 & FireWall-1",
711                "proto": "6",
712                "service_id": "https",
713                "src": "192.168.101.100",
714            })),
715            tdef: type_def(),
716        }
717
718
719        translate_custom_fields {
720            args: func_args! [
721                value: "CEF:0|CyberArk|PTA|12.6|1|Suspected credentials theft|8|suser=mike2@prod1.domain.com cn1=1254323565 shost=prod1.domain.com src=1.1.1.1 cfp1Label=Uptime hours cfp1=35.46 cn1Label=Internal ID",
722                translate_custom_fields: true
723            ],
724            want: Ok(value!({
725                "cefVersion":"0",
726                "deviceVendor":"CyberArk",
727                "deviceProduct":"PTA",
728                "deviceVersion":"12.6",
729                "deviceEventClassId":"1",
730                "name":"Suspected credentials theft",
731                "severity":"8",
732                "suser":"mike2@prod1.domain.com",
733                "shost":"prod1.domain.com",
734                "src":"1.1.1.1",
735                "Uptime hours":"35.46",
736                "Internal ID":"1254323565",
737            })),
738            tdef: type_def(),
739        }
740
741        missing_value {
742            args: func_args! [
743                value: "CEF:0|CyberArk|PTA|12.6||Suspected credentials theft||suser=mike2@prod1.domain.com shost= src=1.1.1.1",
744            ],
745            want: Ok(value!({
746                "cefVersion":"0",
747                "deviceVendor":"CyberArk",
748                "deviceProduct":"PTA",
749                "deviceVersion":"12.6",
750                "deviceEventClassId":"",
751                "name":"Suspected credentials theft",
752                "severity":"",
753                "suser":"mike2@prod1.domain.com",
754                "shost":"",
755                "src":"1.1.1.1"
756            })),
757            tdef: type_def(),
758        }
759
760        missing_key {
761            args: func_args! [
762                value: "CEF:0|Check Point|VPN-1 & FireWall-1|Check Point|Log|https|Unknown|act=Accept =0.0.0.0",
763            ],
764            want: Err("Could not parse whole line successfully"),
765            tdef: type_def(),
766        }
767
768        incomplete_header {
769            args: func_args! [
770                value: "CEF:0|Check Point|VPN-1 & FireWall-1|Check Point|Log|https|",
771            ],
772            want: Err("0: at line 1, in Tag:\nCEF:0|Check Point|VPN-1 & FireWall-1|Check Point|Log|https|\n                                                           ^\n\n1: at line 1, in Alt:\nCEF:0|Check Point|VPN-1 & FireWall-1|Check Point|Log|https|\n                                                           ^\n\n"),
773            tdef: type_def(),
774        }
775
776        utf8_escape {
777            args: func_args! [
778                value: r"CEF:0|xxx|xxx|123456|xxx|xxx|5|TestField={'blabla': 'blabla\xc3\xaablabla'}",
779            ],
780            want: Ok(value!({
781                "cefVersion":"0",
782                "deviceVendor":"xxx",
783                "deviceProduct":"xxx",
784                "deviceVersion":"123456",
785                "deviceEventClassId":"xxx",
786                "name":"xxx",
787                "severity":"5",
788                "TestField": r"{'blabla': 'blabla\xc3\xaablabla'}",
789            })),
790            tdef: type_def(),
791        }
792
793        missing_custom_label {
794            args: func_args! [
795                value: "CEF:0|CyberArk|PTA|12.6|1|Suspected credentials theft|8|cfp1=1.23",
796                translate_custom_fields: true
797            ],
798            want: Err("Custom field with missing label or value"),
799            tdef: type_def(),
800        }
801
802        duplicate_value {
803            args: func_args! [
804                value: "CEF:0|CyberArk|PTA|12.6|1|Suspected credentials theft|8|flexString1=1.23 flexString1=1.24 flexString1Label=Version",
805                translate_custom_fields: true
806            ],
807            want: Err("Custom field with duplicate value"),
808            tdef: type_def(),
809        }
810
811    ];
812}