dnstap_parser/vrl_functions/
parse_dnstap.rs

1use std::sync::LazyLock;
2
3use base64::prelude::{BASE64_STANDARD, Engine as _};
4use dnsmsg_parser::dns_message_parser::DnsParserOptions;
5use vector_core::event::LogEvent;
6use vrl::prelude::*;
7
8use crate::{parser::DnstapParser, schema::DnstapEventSchema};
9
10static DEFAULT_LOWERCASE_HOSTNAMES: LazyLock<Value> = LazyLock::new(|| Value::Boolean(false));
11
12static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
13    vec![
14        Parameter::required(
15            "value",
16            kind::BYTES,
17            "The base64 encoded representation of the DNSTAP data to parse.",
18        ),
19        Parameter::optional(
20            "lowercase_hostnames",
21            kind::BOOLEAN,
22            "Whether to turn all hostnames found in resulting data lowercase, for consistency.",
23        )
24        .default(&DEFAULT_LOWERCASE_HOSTNAMES),
25    ]
26});
27
28#[derive(Clone, Copy, Debug)]
29pub struct ParseDnstap;
30
31impl Function for ParseDnstap {
32    fn identifier(&self) -> &'static str {
33        "parse_dnstap"
34    }
35
36    fn usage(&self) -> &'static str {
37        "Parses the `value` as base64 encoded DNSTAP data."
38    }
39
40    fn internal_failure_reasons(&self) -> &'static [&'static str] {
41        &[
42            "`value` is not a valid base64 encoded string.",
43            "dnstap parsing failed for `value`",
44        ]
45    }
46
47    fn category(&self) -> &'static str {
48        Category::Parse.as_ref()
49    }
50
51    fn return_kind(&self) -> u16 {
52        kind::OBJECT
53    }
54
55    fn parameters(&self) -> &'static [Parameter] {
56        &PARAMETERS
57    }
58
59    fn examples(&self) -> &'static [Example] {
60        &[example!(
61            title: "Parse dnstap query message",
62            source: r#"parse_dnstap!("ChVqYW1lcy1WaXJ0dWFsLU1hY2hpbmUSC0JJTkQgOS4xNi4zGgBy5wEIAxACGAEiEAAAAAAAAAAAAAAAAAAAAAAqECABBQJwlAAAAAAAAAAAADAw8+0CODVA7+zq9wVNMU3WNlI2kwIAAAABAAAAAAABCWZhY2Vib29rMQNjb20AAAEAAQAAKQIAAACAAAAMAAoACOxjCAG9zVgzWgUDY29tAGAAbQAAAAByZLM4AAAAAQAAAAAAAQJoNQdleGFtcGxlA2NvbQAABgABAAApBNABAUAAADkADwA1AAlubyBTRVAgbWF0Y2hpbmcgdGhlIERTIGZvdW5kIGZvciBkbnNzZWMtZmFpbGVkLm9yZy54AQ==")"#,
63            result: Ok(indoc!(
64                r#"{
65                        "dataType": "Message",
66                        "dataTypeId": 1,
67                        "extraInfo": "",
68                        "messageType": "ResolverQuery",
69                        "messageTypeId": 3,
70                        "queryZone": "com.",
71                        "requestData": {
72                            "fullRcode": 0,
73                            "header": {
74                                "aa": false,
75                                "ad": false,
76                                "anCount": 0,
77                                "arCount": 1,
78                                "cd": false,
79                                "id": 37634,
80                                "nsCount": 0,
81                                "opcode": 0,
82                                "qdCount": 1,
83                                "qr": 0,
84                                "ra": false,
85                                "rcode": 0,
86                                "rd": false,
87                                "tc": false
88                            },
89                            "opt": {
90                                "do": true,
91                                "ednsVersion": 0,
92                                "extendedRcode": 0,
93                                "options": [
94                                    {
95                                        "optCode": 10,
96                                        "optName": "Cookie",
97                                        "optValue": "7GMIAb3NWDM="
98                                    }
99                                ],
100                                "udpPayloadSize": 512
101                            },
102                            "question": [
103                                {
104                                    "class": "IN",
105                                    "domainName": "facebook1.com.",
106                                    "questionType": "A",
107                                    "questionTypeId": 1
108                                }
109                            ],
110                            "rcodeName": "NoError"
111                        },
112                        "responseData": {
113                            "fullRcode": 16,
114                            "header": {
115                                "aa": false,
116                                "ad": false,
117                                "anCount": 0,
118                                "arCount": 1,
119                                "cd": false,
120                                "id": 45880,
121                                "nsCount": 0,
122                                "opcode": 0,
123                                "qdCount": 1,
124                                "qr": 0,
125                                "ra": false,
126                                "rcode": 16,
127                                "rd": false,
128                                "tc": false
129                            },
130                            "opt": {
131                                "do": false,
132                                "ednsVersion": 1,
133                                "extendedRcode": 1,
134                                "ede": [
135                                    {
136                                        "extraText": "no SEP matching the DS found for dnssec-failed.org.",
137                                        "infoCode": 9,
138                                        "purpose": "DNSKEY Missing"
139                                    }
140                                ],
141                                "udpPayloadSize": 1232
142                            },
143                            "question": [
144                                {
145                                    "class": "IN",
146                                    "domainName": "h5.example.com.",
147                                    "questionType": "SOA",
148                                    "questionTypeId": 6
149                                }
150                            ],
151                            "rcodeName": "BADVERS"
152                        },
153                        "responseAddress": "2001:502:7094::30",
154                        "responsePort": 53,
155                        "serverId": "james-Virtual-Machine",
156                        "serverVersion": "BIND 9.16.3",
157                        "socketFamily": "INET6",
158                        "socketProtocol": "UDP",
159                        "sourceAddress": "::",
160                        "sourcePort": 46835,
161                        "time": 1593489007920014129,
162                        "timePrecision": "ns",
163                        "timestamp": "2020-06-30T03:50:07.920014129Z"
164                    }"#
165            )),
166        )]
167    }
168
169    fn compile(
170        &self,
171        _state: &TypeState,
172        _ctx: &mut FunctionCompileContext,
173        arguments: ArgumentList,
174    ) -> Compiled {
175        let value = arguments.required("value");
176        let lowercase_hostnames = arguments.optional("lowercase_hostnames");
177        Ok(ParseDnstapFn {
178            value,
179            lowercase_hostnames,
180        }
181        .as_expr())
182    }
183}
184
185#[derive(Debug, Clone)]
186struct ParseDnstapFn {
187    value: Box<dyn Expression>,
188    lowercase_hostnames: Option<Box<dyn Expression>>,
189}
190
191impl FunctionExpression for ParseDnstapFn {
192    fn resolve(&self, ctx: &mut Context<'_>) -> Resolved {
193        let value = self.value.resolve(ctx)?;
194        let input = value.try_bytes_utf8_lossy()?;
195
196        let mut event = LogEvent::default();
197
198        DnstapParser::parse(
199            &mut event,
200            BASE64_STANDARD
201                .decode(input.as_bytes())
202                .map_err(|_| format!("{input} is not a valid base64 encoded string"))?
203                .into(),
204            DnsParserOptions {
205                lowercase_hostnames: self
206                    .lowercase_hostnames
207                    .map_resolve_with_default(ctx, || DEFAULT_LOWERCASE_HOSTNAMES.clone())?
208                    .try_boolean()?,
209            },
210        )
211        .map_err(|e| format!("dnstap parsing failed for {input}: {e}"))?;
212
213        Ok(event.value().clone())
214    }
215
216    fn type_def(&self, _: &TypeState) -> TypeDef {
217        TypeDef::object(DnstapEventSchema.request_message_schema_definition()).fallible()
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use chrono::{DateTime, TimeZone, Utc};
224    use vrl::value;
225
226    use super::*;
227
228    test_function![
229        parse_dnstap => ParseDnstap;
230
231        query {
232            args: func_args![value: value!("ChVqYW1lcy1WaXJ0dWFsLU1hY2hpbmUSC0JJTkQgOS4xNi4zGgBy5wEIAxACGAEiEAAAAAAAAAAAAAAAAAAAAAAqECABBQJwlAAAAAAAAAAAADAw8+0CODVA7+zq9wVNMU3WNlI2kwIAAAABAAAAAAABCWZhY2Vib29rMQNjb20AAAEAAQAAKQIAAACAAAAMAAoACOxjCAG9zVgzWgUDY29tAGAAbQAAAAByZLM4AAAAAQAAAAAAAQJoNQdleGFtcGxlA2NvbQAABgABAAApBNABAUAAADkADwA1AAlubyBTRVAgbWF0Y2hpbmcgdGhlIERTIGZvdW5kIGZvciBkbnNzZWMtZmFpbGVkLm9yZy54AQ==")],
233            want: Ok({
234                let timestamp = Value::Timestamp(
235                    Utc.from_utc_datetime(
236                        &DateTime::parse_from_rfc3339("2020-06-30T03:50:07.920014129Z")
237                            .unwrap()
238                            .naive_utc(),
239                    ),
240                );
241                value!({
242                    dataType: "Message",
243                    dataTypeId: 1,
244                    extraInfo: "",
245                    messageType: "ResolverQuery",
246                    messageTypeId: 3,
247                    queryZone: "com.",
248                    requestData: {
249                        fullRcode: 0,
250                        header: {
251                            aa: false,
252                            ad: false,
253                            anCount: 0,
254                            arCount: 1,
255                            cd: false,
256                            id: 37634,
257                            nsCount: 0,
258                            opcode: 0,
259                            qdCount: 1,
260                            qr: 0,
261                            ra: false,
262                            rcode: 0,
263                            rd: false,
264                            tc: false,
265                        },
266                        opt: {
267                            do: true,
268                            ednsVersion: 0,
269                            extendedRcode: 0,
270                            options: [
271                            {
272                                optCode: 10,
273                                optName: "Cookie",
274                                optValue: "7GMIAb3NWDM=",
275                            }
276                            ],
277                            udpPayloadSize: 512,
278                        },
279                        question: [
280                        {
281                            class: "IN",
282                            domainName: "facebook1.com.",
283                            questionType: "A",
284                            questionTypeId: 1,
285                        }
286                        ],
287                        rcodeName: "NoError",
288                    },
289                    responseData: {
290                        fullRcode: 16,
291                        header: {
292                            aa: false,
293                            ad: false,
294                            anCount: 0,
295                            arCount: 1,
296                            cd: false,
297                            id: 45880,
298                            nsCount: 0,
299                            opcode: 0,
300                            qdCount: 1,
301                            qr: 0,
302                            ra: false,
303                            rcode: 16,
304                            rd: false,
305                            tc: false,
306                        },
307                        opt: {
308                            do: false,
309                            ede: [
310                            {
311                                extraText: "no SEP matching the DS found for dnssec-failed.org.",
312                                infoCode: 9,
313                                purpose: "DNSKEY Missing",
314                            }
315                            ],
316                            ednsVersion: 1,
317                            extendedRcode: 1,
318                            udpPayloadSize: 1232,
319                        },
320                        question: [
321                        {
322                            class: "IN",
323                            domainName: "h5.example.com.",
324                            questionType: "SOA",
325                            questionTypeId: 6,
326                        }
327                        ],
328                        rcodeName: "BADVERS",
329                    },
330                    responseAddress: "2001:502:7094::30",
331                    responsePort: 53,
332                    serverId: "james-Virtual-Machine",
333                    serverVersion: "BIND 9.16.3",
334                    socketFamily: "INET6",
335                    socketProtocol: "UDP",
336                    sourceAddress: "::",
337                    sourcePort: 46835,
338                    time: 1_593_489_007_920_014_129i64,
339                    timePrecision: "ns",
340                    timestamp: timestamp
341                })
342            }),
343            tdef: TypeDef::object(DnstapEventSchema.request_message_schema_definition()).fallible(),
344        }
345
346        update {
347            args: func_args![value: value!("ChVqYW1lcy1WaXJ0dWFsLU1hY2hpbmUSC0JJTkQgOS4xNi4zcmsIDhABGAEiBH8AAAEqBH8AAAEwrG44AEC+iu73BU14gfofUh1wi6gAAAEAAAAAAAAHZXhhbXBsZQNjb20AAAYAAWC+iu73BW0agDwvch1wi6gAAAEAAAAAAAAHZXhhbXBsZQNjb20AAAYAAXgB")],
348            want: Ok({
349                let timestamp = Value::Timestamp(
350                    Utc.from_utc_datetime(
351                        &DateTime::parse_from_rfc3339("2020-06-30T18:32:30.792494106Z")
352                            .unwrap()
353                            .naive_utc(),
354                    ),
355                );
356                value!({
357                    dataType: "Message",
358                    dataTypeId: 1,
359                    messageType: "UpdateResponse",
360                    messageTypeId: 14,
361                    requestData: {
362                        fullRcode: 0,
363                        header: {
364                            adCount: 0,
365                            id: 28811,
366                            opcode: 5,
367                            prCount: 0,
368                            qr: 1,
369                            rcode: 0,
370                            upCount: 0,
371                            zoCount: 1
372                        },
373                        zone: {
374                            zClass: "IN",
375                            zName: "example.com.",
376                            zType: "SOA",
377                            zTypeId: 6
378                        },
379                        rcodeName: "NoError",
380                    },
381                    responseAddress: "127.0.0.1",
382                    responseData: {
383                        fullRcode: 0,
384                        header: {
385                            adCount: 0,
386                            id: 28811,
387                            opcode: 5,
388                            prCount: 0,
389                            qr: 1,
390                            rcode: 0,
391                            upCount: 0,
392                            zoCount: 1
393                        },
394                        zone: {
395                            zClass: "IN",
396                            zName: "example.com.",
397                            zType: "SOA",
398                            zTypeId: 6
399                        },
400                        rcodeName: "NoError",
401                    },
402                    responsePort: 0,
403                    serverId: "james-Virtual-Machine",
404                    serverVersion: "BIND 9.16.3",
405                    socketFamily: "INET",
406                    socketProtocol: "UDP",
407                    sourceAddress: "127.0.0.1",
408                    sourcePort: 14124,
409                    time: 1_593_541_950_792_494_106i64,
410                    timePrecision: "ns",
411                    timestamp: timestamp
412                })
413            }),
414            tdef: TypeDef::object(DnstapEventSchema.request_message_schema_definition()).fallible(),
415        }
416
417        non_base64_value {
418            args: func_args![value: value!("non base64 string")],
419            want: Err("non base64 string is not a valid base64 encoded string"),
420            tdef: TypeDef::object(DnstapEventSchema.request_message_schema_definition()).fallible(),
421        }
422
423        invalid_dnstap_data {
424            args: func_args![value: value!("bm9uIGRuc3RhcCBkYXRh")],
425            want: Err("dnstap parsing failed for bm9uIGRuc3RhcCBkYXRh: failed to decode Protobuf message: invalid wire type value: 6"),
426            tdef: TypeDef::object(DnstapEventSchema.request_message_schema_definition()).fallible(),
427        }
428    ];
429}