1#[derive(Debug, Clone)]
5pub struct Pattern {
6 inner: stix::pattern::Pattern,
7}
8
9impl Pattern {
10 pub(crate) fn new(inner: stix::pattern::Pattern) -> Self {
11 Pattern { inner }
12 }
13
14 pub(crate) fn inner(&self) -> &stix::pattern::Pattern {
15 &self.inner
16 }
17
18 pub fn to_json(&self) -> String {
20 serde_json::to_string(&self.inner).unwrap_or_else(|_| "null".to_string())
22 }
23
24 pub fn ir_json(&self) -> String {
26 serde_json::to_string(&self.lowered()).unwrap_or_else(|_| "null".to_string())
28 }
29
30 pub fn ir_listing(&self) -> String {
32 self.lowered().to_listing()
33 }
34
35 pub fn canonical(&self) -> String {
37 stix::pattern::ir::render(&self.lowered())
38 }
39
40 fn lowered(&self) -> stix::pattern::ir::Program {
41 stix::pattern::ir::lower(&self.inner)
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct Bundle {
48 inner: stix::model::Bundle,
49}
50
51impl Bundle {
52 pub(crate) fn new(inner: stix::model::Bundle) -> Self {
53 Bundle { inner }
54 }
55
56 pub(crate) fn inner(&self) -> &stix::model::Bundle {
57 &self.inner
58 }
59
60 pub fn object_count(&self) -> usize {
62 self.inner.objects.len()
63 }
64
65 pub fn object_json(&self, index: usize) -> Option<String> {
67 let obj = self.inner.objects.get(index)?;
68 Some(serde_json::to_string(obj).unwrap_or_else(|_| "null".to_string()))
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct MatchOutcome {
75 pub matched: bool,
77 pub observations: Vec<u64>,
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn pattern_to_json_round_trips() {
87 let inner = stix::parse("[ipv4-addr:value = '1.2.3.4']").unwrap();
88 let handle = Pattern::new(inner.clone());
89 let json = handle.to_json();
90 let back: stix::pattern::Pattern = serde_json::from_str(&json).unwrap();
91 assert_eq!(back, inner);
92 }
93
94 #[test]
95 fn bundle_object_access() {
96 let raw = r#"{"type":"bundle","id":"bundle--1","objects":[
97 {"type":"ipv4-addr","id":"ipv4-addr--1","value":"1.2.3.4"}
98 ]}"#;
99 let inner = stix::model::Bundle::from_json_str(raw).unwrap();
100 let handle = Bundle::new(inner);
101 assert_eq!(handle.object_count(), 1);
102 let obj_json = handle.object_json(0).unwrap();
103 assert!(obj_json.contains("ipv4-addr--1"));
104 assert!(handle.object_json(5).is_none());
105 }
106
107 #[test]
108 fn match_outcome_fields() {
109 let o = MatchOutcome {
110 matched: true,
111 observations: vec![0, 2],
112 };
113 assert!(o.matched);
114 assert_eq!(o.observations, vec![0, 2]);
115 }
116
117 const TWO_OBSERVATIONS: &str = "[file:size > 1024] OR [file:name = 'a']";
118
119 #[test]
120 fn pattern_ir_json_has_blocks_and_main() {
121 let handle = Pattern::new(stix::parse(TWO_OBSERVATIONS).unwrap());
122 let ir: serde_json::Value = serde_json::from_str(&handle.ir_json()).unwrap();
123 assert_eq!(ir["schema_version"], 1);
124 assert_eq!(ir["blocks"].as_array().unwrap().len(), 2);
125 assert_eq!(ir["main"]["kind"], "main");
126 }
127
128 #[test]
129 fn pattern_ir_listing_names_blocks() {
130 let handle = Pattern::new(stix::parse(TWO_OBSERVATIONS).unwrap());
131 let listing = handle.ir_listing();
132 assert!(listing.contains("block b1 (comparison):"), "{listing}");
133 assert!(listing.contains("block main (observation):"), "{listing}");
134 }
135
136 #[test]
137 fn pattern_canonical_normalizes_spacing() {
138 let handle = Pattern::new(stix::parse("[file:size>1024] OR [file:name='a']").unwrap());
139 assert_eq!(handle.canonical(), TWO_OBSERVATIONS);
140 }
141}