1use std::collections::{BTreeMap, BTreeSet};
4
5use stix_model::{ObjectStore, ObjectView, StixValue};
6use stix_pattern::ast::{
7 Comparison, ComparisonExpression, ComparisonOperand, ComparisonOperator, Literal,
8 ObservationExpression, Pattern,
9};
10
11use crate::compare::{value_cmp_literal, value_eq_literal, value_in_set};
12use crate::error::MatchError;
13use crate::observation::Observation;
14use crate::pattern_ops::{like_matches, regex_matches};
15use crate::resolve::resolve_path;
16use crate::result::MatchResult;
17use crate::subset::{is_subset, is_superset};
18
19pub fn eval_comparison(obj: &dyn ObjectView, c: &Comparison, store: Option<&ObjectStore>) -> bool {
22 let values = resolve_path(obj, &c.path, store);
23
24 let base = if c.operator == ComparisonOperator::Exists {
25 !values.is_empty()
26 } else {
27 values
28 .iter()
29 .any(|v| operator_holds(v, c.operator, &c.value))
30 };
31
32 base ^ c.negated
33}
34
35fn operator_holds(value: &StixValue, op: ComparisonOperator, operand: &ComparisonOperand) -> bool {
37 use std::cmp::Ordering;
38
39 if op == ComparisonOperator::In {
41 return match operand {
42 ComparisonOperand::Set(set) => value_in_set(value, set),
43 ComparisonOperand::Literal(lit) => value_in_set(value, std::slice::from_ref(lit)),
44 };
45 }
46
47 let lit = match operand {
48 ComparisonOperand::Literal(l) => l,
49 ComparisonOperand::Set(_) => return false,
51 };
52
53 match op {
54 ComparisonOperator::Equal => value_eq_literal(value, lit),
55 ComparisonOperator::NotEqual => !value_eq_literal(value, lit),
56 ComparisonOperator::GreaterThan => value_cmp_literal(value, lit) == Some(Ordering::Greater),
57 ComparisonOperator::GreaterThanOrEqual => matches!(
58 value_cmp_literal(value, lit),
59 Some(Ordering::Greater | Ordering::Equal)
60 ),
61 ComparisonOperator::LessThan => value_cmp_literal(value, lit) == Some(Ordering::Less),
62 ComparisonOperator::LessThanOrEqual => matches!(
63 value_cmp_literal(value, lit),
64 Some(Ordering::Less | Ordering::Equal)
65 ),
66 ComparisonOperator::Like => string_op(value, lit, like_matches),
67 ComparisonOperator::Matches => string_op(value, lit, regex_matches),
68 ComparisonOperator::IsSubset => string_op(value, lit, is_subset),
69 ComparisonOperator::IsSuperset => string_op(value, lit, is_superset),
70 ComparisonOperator::In | ComparisonOperator::Exists => false,
72 }
73}
74
75fn string_op(value: &StixValue, lit: &Literal, f: impl Fn(&str, &str) -> bool) -> bool {
78 let v = match value.as_str() {
79 Some(s) => s,
80 None => return false,
81 };
82 let l = match lit {
83 Literal::String(s) | Literal::Timestamp(s) | Literal::Binary(s) | Literal::Hex(s) => s,
84 _ => return false,
85 };
86 f(v, l)
87}
88
89pub fn eval_comparison_expression(
95 expr: &ComparisonExpression,
96 observation: &Observation,
97 store: Option<&ObjectStore>,
98) -> bool {
99 let mut types: Vec<&str> = Vec::new();
101 collect_types(expr, &mut types, &mut BTreeSet::new());
102
103 let bound: Vec<(&str, Vec<usize>)> = types
107 .into_iter()
108 .map(|t| {
109 let candidates: Vec<usize> = observation
110 .objects
111 .iter()
112 .enumerate()
113 .filter(|(_, o)| o.type_() == Some(t))
114 .map(|(i, _)| i)
115 .collect();
116 (t, candidates)
117 })
118 .filter(|(_, candidates)| !candidates.is_empty())
119 .collect();
120
121 enumerate_bindings(&bound, |binding| {
122 eval_tree(expr, observation, binding, store)
123 })
124}
125
126fn collect_types<'e>(
130 expr: &'e ComparisonExpression,
131 out: &mut Vec<&'e str>,
132 seen: &mut BTreeSet<&'e str>,
133) {
134 match expr {
135 ComparisonExpression::Test(c) => {
136 if seen.insert(&c.path.object_type) {
137 out.push(&c.path.object_type);
138 }
139 }
140 ComparisonExpression::And(xs) | ComparisonExpression::Or(xs) => {
141 for x in xs {
142 collect_types(x, out, seen);
143 }
144 }
145 }
146}
147
148fn enumerate_bindings(
153 bound: &[(&str, Vec<usize>)],
154 predicate: impl Fn(&BTreeMap<String, usize>) -> bool,
155) -> bool {
156 let mut choice = vec![0usize; bound.len()];
157 let mut binding: BTreeMap<String, usize> = bound
158 .iter()
159 .map(|(t, candidates)| (t.to_string(), candidates[0]))
160 .collect();
161 loop {
162 if predicate(&binding) {
163 return true;
164 }
165 let mut i = bound.len();
167 loop {
168 if i == 0 {
169 return false;
170 }
171 i -= 1;
172 let (t, candidates) = &bound[i];
173 choice[i] += 1;
174 if choice[i] == candidates.len() {
175 choice[i] = 0;
176 }
177 binding.insert(t.to_string(), candidates[choice[i]]);
178 if choice[i] != 0 {
179 break;
180 }
181 }
182 }
183}
184
185fn eval_tree(
187 expr: &ComparisonExpression,
188 observation: &Observation,
189 binding: &BTreeMap<String, usize>,
190 store: Option<&ObjectStore>,
191) -> bool {
192 match expr {
193 ComparisonExpression::Test(c) => match binding.get(&c.path.object_type) {
194 Some(&obj_idx) => eval_comparison(&observation.objects[obj_idx], c, store),
195 None => false,
196 },
197 ComparisonExpression::And(xs) => {
200 xs.iter().all(|x| eval_tree(x, observation, binding, store))
201 }
202 ComparisonExpression::Or(xs) => {
203 xs.iter().any(|x| eval_tree(x, observation, binding, store))
204 }
205 }
206}
207
208pub fn eval_pattern(
214 pattern: &Pattern,
215 observations: &[Observation],
216 store: Option<&ObjectStore>,
217) -> Result<MatchResult, MatchError> {
218 let mut matched = Vec::new();
219 let is_match =
220 eval_observation_expression(&pattern.expression, observations, store, &mut matched)?;
221 if is_match {
222 matched.sort_unstable();
223 matched.dedup();
224 Ok(MatchResult::matched(matched))
225 } else {
226 Ok(MatchResult::no_match())
227 }
228}
229
230fn eval_observation_expression(
233 expr: &ObservationExpression,
234 observations: &[Observation],
235 store: Option<&ObjectStore>,
236 matched: &mut Vec<usize>,
237) -> Result<bool, MatchError> {
238 match expr {
239 ObservationExpression::Observation {
240 expression: comparison,
241 ..
242 } => {
243 let mut any = false;
244 for (i, obs) in observations.iter().enumerate() {
245 if eval_comparison_expression(comparison, obs, store) {
246 matched.push(i);
247 any = true;
248 }
249 }
250 Ok(any)
251 }
252 ObservationExpression::And(xs) => {
256 let mut all = true;
257 for x in xs {
258 all &= eval_observation_expression(x, observations, store, matched)?;
259 }
260 Ok(all)
261 }
262 ObservationExpression::Or(xs) => {
263 let mut any = false;
264 for x in xs {
265 any |= eval_observation_expression(x, observations, store, matched)?;
266 }
267 Ok(any)
268 }
269 ObservationExpression::FollowedBy(_) => Err(MatchError::Unsupported(
270 "FOLLOWEDBY sequencing is not yet implemented".to_string(),
271 )),
272 ObservationExpression::Qualified { .. } => Err(MatchError::Unsupported(
273 "observation qualifiers (WITHIN/REPEATS/START..STOP) are not yet implemented"
274 .to_string(),
275 )),
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use stix_model::StixObject;
283 use stix_pattern::ast::{
284 Comparison, ComparisonOperand, ComparisonOperator, Literal, ObjectPath, PathStep,
285 };
286
287 fn obj(json: serde_json::Value) -> StixObject {
288 StixObject::from_json(json).unwrap()
289 }
290
291 fn cmp(
292 object_type: &str,
293 key: &str,
294 operator: ComparisonOperator,
295 negated: bool,
296 value: ComparisonOperand,
297 ) -> Comparison {
298 Comparison {
299 path: ObjectPath {
300 object_type: object_type.to_string(),
301 steps: vec![PathStep::Key(key.to_string())],
302 span: Default::default(),
303 },
304 operator,
305 negated,
306 value,
307 span: Default::default(),
308 }
309 }
310
311 fn lit(s: &str) -> ComparisonOperand {
312 ComparisonOperand::Literal(Literal::String(s.to_string()))
313 }
314
315 #[test]
316 fn equality_against_object() {
317 let o =
318 obj(serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.2.3.4"}));
319 let c = cmp(
320 "ipv4-addr",
321 "value",
322 ComparisonOperator::Equal,
323 false,
324 lit("1.2.3.4"),
325 );
326 assert!(eval_comparison(&o, &c, None));
327
328 let c2 = cmp(
329 "ipv4-addr",
330 "value",
331 ComparisonOperator::Equal,
332 false,
333 lit("9.9.9.9"),
334 );
335 assert!(!eval_comparison(&o, &c2, None));
336 }
337
338 #[test]
339 fn negation_inverts() {
340 let o = obj(serde_json::json!({"type": "file", "id": "file--1", "name": "evil.exe"}));
341 let c = cmp(
342 "file",
343 "name",
344 ComparisonOperator::Equal,
345 true,
346 lit("evil.exe"),
347 );
348 assert!(!eval_comparison(&o, &c, None));
349 }
350
351 #[test]
352 fn exists_checks_presence() {
353 let o = obj(serde_json::json!({"type": "file", "id": "file--1", "name": "x"}));
354 let present = cmp(
355 "file",
356 "name",
357 ComparisonOperator::Exists,
358 false,
359 lit("ignored"),
360 );
361 assert!(eval_comparison(&o, &present, None));
362 let absent = cmp(
363 "file",
364 "size",
365 ComparisonOperator::Exists,
366 false,
367 lit("ignored"),
368 );
369 assert!(!eval_comparison(&o, &absent, None));
370 }
371
372 #[test]
373 fn in_set_against_object() {
374 let o =
375 obj(serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "8.8.8.8"}));
376 let set = ComparisonOperand::Set(vec![
377 Literal::String("1.1.1.1".into()),
378 Literal::String("8.8.8.8".into()),
379 ]);
380 let c = cmp("ipv4-addr", "value", ComparisonOperator::In, false, set);
381 assert!(eval_comparison(&o, &c, None));
382 }
383
384 #[test]
385 fn issubset_against_object() {
386 let o = obj(
387 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "198.51.100.5"}),
388 );
389 let c = cmp(
390 "ipv4-addr",
391 "value",
392 ComparisonOperator::IsSubset,
393 false,
394 ComparisonOperand::Literal(Literal::String("198.51.100.0/24".into())),
395 );
396 assert!(eval_comparison(&o, &c, None));
397 }
398
399 use crate::observation::Observation;
400 use stix_pattern::ast::ComparisonExpression;
401
402 fn observation(objs: Vec<serde_json::Value>) -> Observation {
403 Observation::new(objs.into_iter().map(obj).collect())
404 }
405
406 fn test_expr(c: Comparison) -> ComparisonExpression {
407 ComparisonExpression::Test(c)
408 }
409
410 #[test]
411 fn single_test_matches_some_object() {
412 let o = observation(vec![
413 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.2.3.4"}),
414 serde_json::json!({"type": "domain-name", "id": "domain-name--1", "value": "evil.example"}),
415 ]);
416 let expr = test_expr(cmp(
417 "domain-name",
418 "value",
419 ComparisonOperator::Equal,
420 false,
421 lit("evil.example"),
422 ));
423 assert!(eval_comparison_expression(&expr, &o, None));
424 }
425
426 #[test]
427 fn and_requires_same_object_binding() {
428 let matching = observation(vec![
430 serde_json::json!({"type": "file", "id": "file--1", "name": "evil.exe", "size": 10}),
431 ]);
432 let split = observation(vec![
433 serde_json::json!({"type": "file", "id": "file--1", "name": "evil.exe", "size": 99}),
434 serde_json::json!({"type": "file", "id": "file--2", "name": "ok.txt", "size": 10}),
435 ]);
436 let expr = ComparisonExpression::And(vec![
437 test_expr(cmp(
438 "file",
439 "name",
440 ComparisonOperator::Equal,
441 false,
442 lit("evil.exe"),
443 )),
444 test_expr(cmp(
445 "file",
446 "size",
447 ComparisonOperator::Equal,
448 false,
449 ComparisonOperand::Literal(Literal::Integer(10)),
450 )),
451 ]);
452 assert!(eval_comparison_expression(&expr, &matching, None));
453 assert!(!eval_comparison_expression(&expr, &split, None));
455 }
456
457 #[test]
458 fn or_matches_either_branch() {
459 let o = observation(vec![
460 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.2.3.4"}),
461 ]);
462 let expr = ComparisonExpression::Or(vec![
463 test_expr(cmp(
464 "ipv4-addr",
465 "value",
466 ComparisonOperator::Equal,
467 false,
468 lit("9.9.9.9"),
469 )),
470 test_expr(cmp(
471 "ipv4-addr",
472 "value",
473 ComparisonOperator::Equal,
474 false,
475 lit("1.2.3.4"),
476 )),
477 ]);
478 assert!(eval_comparison_expression(&expr, &o, None));
479 }
480
481 fn eq_test(object_type: &str, value: &str) -> ComparisonExpression {
482 test_expr(cmp(
483 object_type,
484 "value",
485 ComparisonOperator::Equal,
486 false,
487 lit(value),
488 ))
489 }
490
491 #[test]
495 fn comparison_chains_match_their_left_assoc_binary_form() {
496 let o = observation(vec![
497 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.2.3.4"}),
498 serde_json::json!({"type": "domain-name", "id": "domain-name--1", "value": "a.example"}),
499 ]);
500 let leaves = [
501 eq_test("ipv4-addr", "1.2.3.4"),
502 eq_test("ipv4-addr", "9.9.9.9"),
503 eq_test("domain-name", "a.example"),
504 eq_test("url", "absent"),
505 ];
506 for a in &leaves {
507 for b in &leaves {
508 for c in &leaves {
509 type Build = fn(Vec<ComparisonExpression>) -> ComparisonExpression;
510 for build in [ComparisonExpression::And as Build, ComparisonExpression::Or] {
511 let chain = build(vec![a.clone(), b.clone(), c.clone()]);
512 let nested = build(vec![build(vec![a.clone(), b.clone()]), c.clone()]);
513 assert_eq!(
514 eval_comparison_expression(&chain, &o, None),
515 eval_comparison_expression(&nested, &o, None),
516 "{chain:?}"
517 );
518 }
519 }
520 }
521 }
522 }
523
524 #[test]
525 fn many_distinct_types_in_one_chain() {
526 let o = observation(vec![
529 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.2.3.4"}),
530 ]);
531 let mut xs: Vec<ComparisonExpression> = (0..10_000)
532 .map(|i| eq_test(&format!("type-{i}"), "x"))
533 .collect();
534 assert!(!eval_comparison_expression(
535 &ComparisonExpression::Or(xs.clone()),
536 &o,
537 None
538 ));
539 xs.push(eq_test("ipv4-addr", "1.2.3.4"));
540 assert!(eval_comparison_expression(
541 &ComparisonExpression::Or(xs),
542 &o,
543 None
544 ));
545 }
546
547 #[test]
548 fn observation_chains_match_their_left_assoc_binary_form() {
549 let observations = vec![
550 observation(vec![
551 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.1.1.1"}),
552 ]),
553 observation(vec![
554 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--2", "value": "2.2.2.2"}),
555 ]),
556 observation(vec![
557 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--3", "value": "3.3.3.3"}),
558 ]),
559 ];
560 let leaf = |v: &str| ObservationExpression::Observation {
561 expression: Box::new(eq_test("ipv4-addr", v)),
562 span: Default::default(),
563 };
564 let leaves = [leaf("1.1.1.1"), leaf("3.3.3.3"), leaf("9.9.9.9")];
565 let run = |e: ObservationExpression| {
566 eval_pattern(&Pattern { expression: e }, &observations, None).unwrap()
567 };
568 for a in &leaves {
569 for b in &leaves {
570 for c in &leaves {
571 type Build = fn(Vec<ObservationExpression>) -> ObservationExpression;
572 for build in [
573 ObservationExpression::And as Build,
574 ObservationExpression::Or,
575 ] {
576 let chain = build(vec![a.clone(), b.clone(), c.clone()]);
577 let nested = build(vec![build(vec![a.clone(), b.clone()]), c.clone()]);
578 assert_eq!(run(chain.clone()), run(nested), "{chain:?}");
580 }
581 }
582 }
583 }
584 let r = run(ObservationExpression::Or(vec![
586 leaf("1.1.1.1"),
587 leaf("9.9.9.9"),
588 leaf("3.3.3.3"),
589 ]));
590 assert_eq!(r.observations(), &[0, 2]);
591 }
592
593 use stix_pattern::parse;
594
595 #[test]
596 fn single_observation_matches_across_set() {
597 let observations = vec![
598 observation(vec![
599 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.1.1.1"}),
600 ]),
601 observation(vec![
602 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--2", "value": "1.2.3.4"}),
603 ]),
604 ];
605 let pattern = parse("[ipv4-addr:value = '1.2.3.4']").unwrap();
606 let result = eval_pattern(&pattern, &observations, None).unwrap();
607 assert!(result.is_match());
608 assert_eq!(result.observations(), &[1]);
609 }
610
611 #[test]
612 fn observation_and_needs_both() {
613 let observations = vec![
614 observation(vec![
615 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.1.1.1"}),
616 ]),
617 observation(vec![
618 serde_json::json!({"type": "domain-name", "id": "domain-name--1", "value": "evil.example"}),
619 ]),
620 ];
621 let yes = parse("[ipv4-addr:value = '1.1.1.1'] AND [domain-name:value = 'evil.example']")
622 .unwrap();
623 assert!(eval_pattern(&yes, &observations, None).unwrap().is_match());
624
625 let no = parse("[ipv4-addr:value = '1.1.1.1'] AND [domain-name:value = 'good.example']")
626 .unwrap();
627 assert!(!eval_pattern(&no, &observations, None).unwrap().is_match());
628 }
629
630 #[test]
631 fn followedby_is_unsupported() {
632 let observations = vec![observation(vec![
633 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.1.1.1"}),
634 ])];
635 let pattern =
636 parse("[ipv4-addr:value = '1.1.1.1'] FOLLOWEDBY [ipv4-addr:value = '2.2.2.2']")
637 .unwrap();
638 let err = eval_pattern(&pattern, &observations, None).unwrap_err();
639 assert!(matches!(err, crate::error::MatchError::Unsupported(_)));
640 }
641
642 #[test]
643 fn qualifier_is_unsupported() {
644 let observations = vec![observation(vec![
645 serde_json::json!({"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "1.1.1.1"}),
646 ])];
647 let pattern = parse("[ipv4-addr:value = '1.1.1.1'] REPEATS 2 TIMES").unwrap();
648 let err = eval_pattern(&pattern, &observations, None).unwrap_err();
649 assert!(matches!(err, crate::error::MatchError::Unsupported(_)));
650 }
651}