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 nom_language::error::convert_error(input, e)
251 }
252 nom::Err::Incomplete(_) => e.to_string(),
253 })?;
254
255 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 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
342fn 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}