1use std::net::IpAddr;
2
3use community_id::calculate_community_id;
4
5use crate::compiler::prelude::*;
6
7fn community_id(
8 src_ip: &Value,
9 dst_ip: &Value,
10 protocol: Value,
11 src_port: Option<Value>,
12 dst_port: Option<Value>,
13 seed: Option<Value>,
14) -> Resolved {
15 let src_ip: IpAddr = src_ip
16 .try_bytes_utf8_lossy()?
17 .parse()
18 .map_err(|err| format!("unable to parse source IP address: {err}"))?;
19
20 let dst_ip: IpAddr = dst_ip
21 .try_bytes_utf8_lossy()?
22 .parse()
23 .map_err(|err| format!("unable to parse destination IP address: {err}"))?;
24
25 let protocol = u8::try_from(protocol.try_integer()?)
26 .map_err(|err| format!("protocol must be between 0 and 255: {err}"))?;
27
28 let src_port = src_port
29 .map(VrlValueConvert::try_integer)
30 .transpose()?
31 .map(|value| {
32 u16::try_from(value)
33 .map_err(|err| format!("source port must be between 0 and 65535: {err}"))
34 })
35 .transpose()?;
36
37 let dst_port = dst_port
38 .map(VrlValueConvert::try_integer)
39 .transpose()?
40 .map(|value| {
41 u16::try_from(value)
42 .map_err(|err| format!("destination port must be between 0 and 65535: {err}"))
43 })
44 .transpose()?;
45
46 let seed = seed
47 .map(VrlValueConvert::try_integer)
48 .transpose()?
49 .map_or(Ok(0), u16::try_from)
50 .map_err(|err| format!("seed must be between 0 and 65535: {err}"))?;
51
52 let id = calculate_community_id(seed, src_ip, dst_ip, src_port, dst_port, protocol, false);
53
54 match id {
55 Ok(id) => Ok(Value::Bytes(id.into())),
56 Err(err) => Err(ExpressionError::from(err.to_string())),
57 }
58}
59
60#[derive(Clone, Copy, Debug)]
61pub struct CommunityID;
62
63impl Function for CommunityID {
64 fn identifier(&self) -> &'static str {
65 "community_id"
66 }
67
68 fn usage(&self) -> &'static str {
69 "Generates an ID based on the [Community ID Spec](https://github.com/corelight/community-id-spec)."
70 }
71
72 fn category(&self) -> &'static str {
73 Category::String.as_ref()
74 }
75
76 fn return_kind(&self) -> u16 {
77 kind::BYTES
78 }
79
80 fn parameters(&self) -> &'static [Parameter] {
81 const PARAMETERS: &[Parameter] = &[
82 Parameter::required("source_ip", kind::BYTES, "The source IP address."),
83 Parameter::required("destination_ip", kind::BYTES, "The destination IP address."),
84 Parameter::required("protocol", kind::INTEGER, "The protocol number."),
85 Parameter::optional(
86 "source_port",
87 kind::INTEGER,
88 "The source port or ICMP type.",
89 ),
90 Parameter::optional(
91 "destination_port",
92 kind::INTEGER,
93 "The destination port or ICMP code.",
94 ),
95 Parameter::optional("seed", kind::INTEGER, "The custom seed number."),
96 ];
97 PARAMETERS
98 }
99
100 fn examples(&self) -> &'static [Example] {
101 &[
102 example! {
103 title: "Generate Community ID for TCP",
104 source: r#"community_id!(source_ip: "1.2.3.4", destination_ip: "5.6.7.8", source_port: 1122, destination_port: 3344, protocol: 6)"#,
105 result: Ok("1:wCb3OG7yAFWelaUydu0D+125CLM="),
106 },
107 example! {
108 title: "Generate Community ID for UDP",
109 source: r#"community_id!(source_ip: "1.2.3.4", destination_ip: "5.6.7.8", source_port: 1122, destination_port: 3344, protocol: 17)"#,
110 result: Ok("1:0Mu9InQx6z4ZiCZM/7HXi2WMhOg="),
111 },
112 example! {
113 title: "Generate Community ID for ICMP",
114 source: r#"community_id!(source_ip: "1.2.3.4", destination_ip: "5.6.7.8", source_port: 8, destination_port: 0, protocol: 1)"#,
115 result: Ok("1:crodRHL2FEsHjbv3UkRrfbs4bZ0="),
116 },
117 example! {
118 title: "Generate Community ID for RSVP",
119 source: r#"community_id!(source_ip: "1.2.3.4", destination_ip: "5.6.7.8", protocol: 46)"#,
120 result: Ok("1:ikv3kmf89luf73WPz1jOs49S768="),
121 },
122 ]
123 }
124
125 fn compile(
126 &self,
127 state: &state::TypeState,
128 _ctx: &mut FunctionCompileContext,
129 arguments: ArgumentList,
130 ) -> Compiled {
131 let src_ip = arguments.required("source_ip");
132 let dst_ip = arguments.required("destination_ip");
133 let protocol = arguments.required("protocol");
134 let src_port = arguments.optional("source_port");
135 let dst_port = arguments.optional("destination_port");
136 let seed = arguments.optional("seed");
137
138 if let Some(protocol) = protocol.resolve_constant(state)
139 && let Some(protocol_literal) = protocol.as_integer()
140 && u8::try_from(protocol_literal).is_err()
141 {
142 return Err(function::Error::InvalidArgument {
143 keyword: "protocol",
144 value: protocol,
145 error: r#""protocol" must be between 0 and 255"#,
146 }
147 .into());
148 }
149
150 if let Some(src_port) = &src_port
151 && let Some(src_port) = src_port.resolve_constant(state)
152 && let Some(src_port_literal) = src_port.as_integer()
153 && u16::try_from(src_port_literal).is_err()
154 {
155 return Err(function::Error::InvalidArgument {
156 keyword: "source_port",
157 value: src_port,
158 error: r#""source_port" must be between 0 and 65535"#,
159 }
160 .into());
161 }
162
163 if let Some(dst_port) = &dst_port
164 && let Some(dst_port) = dst_port.resolve_constant(state)
165 && let Some(dst_port_literal) = dst_port.as_integer()
166 && u16::try_from(dst_port_literal).is_err()
167 {
168 return Err(function::Error::InvalidArgument {
169 keyword: "destination_port",
170 value: dst_port,
171 error: r#""destination_port" must be between 0 and 65535"#,
172 }
173 .into());
174 }
175
176 if let Some(seed) = &seed
177 && let Some(seed) = seed.resolve_constant(state)
178 && let Some(seed_literal) = seed.as_integer()
179 && u16::try_from(seed_literal).is_err()
180 {
181 return Err(function::Error::InvalidArgument {
182 keyword: "seed",
183 value: seed,
184 error: r#""seed" must be between 0 and 65535"#,
185 }
186 .into());
187 }
188
189 Ok(CommunityIDFn {
190 src_ip,
191 dst_ip,
192 protocol,
193 src_port,
194 dst_port,
195 seed,
196 }
197 .as_expr())
198 }
199}
200
201#[derive(Debug, Clone)]
202struct CommunityIDFn {
203 src_ip: Box<dyn Expression>,
204 dst_ip: Box<dyn Expression>,
205 protocol: Box<dyn Expression>,
206 src_port: Option<Box<dyn Expression>>,
207 dst_port: Option<Box<dyn Expression>>,
208 seed: Option<Box<dyn Expression>>,
209}
210
211impl FunctionExpression for CommunityIDFn {
212 fn resolve(&self, ctx: &mut Context) -> Resolved {
213 let src_ip: Value = self.src_ip.resolve(ctx)?;
214 let dst_ip: Value = self.dst_ip.resolve(ctx)?;
215 let protocol = self.protocol.resolve(ctx)?;
216
217 let src_port = self
218 .src_port
219 .as_ref()
220 .map(|expr| expr.resolve(ctx))
221 .transpose()?;
222
223 let dst_port = self
224 .dst_port
225 .as_ref()
226 .map(|expr| expr.resolve(ctx))
227 .transpose()?;
228
229 let seed = self
230 .seed
231 .as_ref()
232 .map(|expr| expr.resolve(ctx))
233 .transpose()?;
234
235 community_id(&src_ip, &dst_ip, protocol, src_port, dst_port, seed)
236 }
237
238 fn type_def(&self, _state: &state::TypeState) -> TypeDef {
239 TypeDef::bytes().fallible()
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 test_function![
248 community_id => CommunityID;
249 tcp_default_seed {
251 args: func_args![source_ip: "1.2.3.4", destination_ip: "5.6.7.8", protocol: 6, source_port: 1122, destination_port: 3344],
252 want: Ok("1:wCb3OG7yAFWelaUydu0D+125CLM="),
253 tdef: TypeDef::bytes().fallible(),
254 }
255
256 tcp_reverse_default_seed {
257 args: func_args![source_ip: "5.6.7.8", destination_ip: "1.2.3.4", protocol: 6, source_port: 3344, destination_port: 1122],
258 want: Ok("1:wCb3OG7yAFWelaUydu0D+125CLM="),
259 tdef: TypeDef::bytes().fallible(),
260 }
261
262 tcp_no_ports {
263 args: func_args![source_ip: "1.2.3.4", destination_ip: "5.6.7.8", protocol: 6],
264 want: Err("src port and dst port should be set when protocol is tcp/udp/sctp"),
265 tdef: TypeDef::bytes().fallible(),
266 }
267
268 tcp_source_port_too_large {
269 args: func_args![source_ip: "1.2.3.4", destination_ip: "5.6.7.8", protocol: 6, source_port: u64::MAX, destination_port: 80],
270 want: Err("invalid argument"),
271 tdef: TypeDef::bytes().fallible(),
272 }
273
274 tcp_destination_port_too_large {
275 args: func_args![source_ip: "1.2.3.4", destination_ip: "5.6.7.8", protocol: 6, source_port: 80 , destination_port: u64::MAX],
276 want: Err("invalid argument"),
277 tdef: TypeDef::bytes().fallible(),
278 }
279
280 udp_default_seed {
281 args: func_args![source_ip: "1.2.3.4", destination_ip: "5.6.7.8", protocol: 17, source_port: 1122, destination_port: 3344],
282 want: Ok("1:0Mu9InQx6z4ZiCZM/7HXi2WMhOg="),
283 tdef: TypeDef::bytes().fallible(),
284 }
285
286 udp_reverse_default_seed {
287 args: func_args![source_ip: "5.6.7.8", destination_ip: "1.2.3.4", protocol: 17, source_port: 3344, destination_port: 1122],
288 want: Ok("1:0Mu9InQx6z4ZiCZM/7HXi2WMhOg="),
289 tdef: TypeDef::bytes().fallible(),
290 }
291
292 rsvp_default_seed {
293 args: func_args![source_ip: "1.2.3.4", destination_ip: "5.6.7.8", protocol: 46],
294 want: Ok("1:ikv3kmf89luf73WPz1jOs49S768="),
295 tdef: TypeDef::bytes().fallible(),
296 }
297
298 rsvp_reverse_default_seed {
299 args: func_args![source_ip: "5.6.7.8", destination_ip: "1.2.3.4", protocol: 46],
300 want: Ok("1:ikv3kmf89luf73WPz1jOs49S768="),
301 tdef: TypeDef::bytes().fallible(),
302 }
303
304 tcp_seed_1 {
305 args: func_args![seed: 1, source_ip: "1.2.3.4", destination_ip: "5.6.7.8", protocol: 6, source_port: 1122, destination_port: 3344],
306 want: Ok("1:HhA1B+6CoLbiKPEs5nhNYN4XWfk="),
307 tdef: TypeDef::bytes().fallible(),
308 }
309
310 tcp_reverse_seed_1 {
311 args: func_args![seed: 1,source_ip: "5.6.7.8", destination_ip: "1.2.3.4", protocol: 6, source_port: 3344, destination_port: 1122],
312 want: Ok("1:HhA1B+6CoLbiKPEs5nhNYN4XWfk="),
313 tdef: TypeDef::bytes().fallible(),
314 }
315
316 seed_too_large {
317 args: func_args![seed: u64::MAX,source_ip: "5.6.7.8", destination_ip: "1.2.3.4", protocol: 6, source_port: 3344, destination_port: 1122],
318 want: Err("invalid argument"),
319 tdef: TypeDef::bytes().fallible(),
320 }
321
322 protocol_too_large {
323 args: func_args![source_ip: "5.6.7.8", destination_ip: "1.2.3.4", protocol: i64::MAX, source_port: 3344, destination_port: 1122],
324 want: Err("invalid argument"),
325 tdef: TypeDef::bytes().fallible(),
326 }
327
328 ];
329}