vrl/stdlib/
community_id.rs

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        // Examples from https://github.com/corelight/community-id-spec/tree/master/baseline
250        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}