]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
dns: new v3 style logging for alerts
authorJason Ish <jason.ish@oisf.net>
Thu, 27 Jun 2024 22:54:25 +0000 (16:54 -0600)
committerVictor Julien <victor@inliniac.net>
Tue, 9 Jul 2024 10:15:24 +0000 (12:15 +0200)
V3 style DNS logging fixes the discrepancies between request and
response logging better dns records and alert records.

The main change is that queries and answers are always logged as
arrays, and header fields are not logged in array items.

For alerts this means that answers are now logged as arrays, queries
already were.

DNS records will get this new format as well, but with a configuration
parameter.

Bug: #6281

etc/schema.json
rust/src/dns/log.rs
src/output-json-dns.c

index 356e4fe9829a60d9a19934d3a3ba48815001a18d..af054c6e10e32c1be08cfd703ea6c7eed85dedc8 100644 (file)
                             "ttl": {
                                 "type": "integer"
                             },
+                            "soa": {
+                                "$ref": "#/$defs/dns.soa"
+                            },
                             "srv": {
                                 "type": "object",
                                 "properties": {
                     "$ref": "#/$defs/dns.additionals"
                 },
                 "query": {
+                    "$comment": "EVE DNS v2 style query logging; as of Suricata 8 only used in DNS records when v2 logging is enabled, not used for DNS records logged as part of an event.",
+                    "type": "array",
+                    "minItems": 1,
+                    "items": {
+                        "type": "object",
+                        "properties": {
+                            "id": {
+                                "type": "integer"
+                            },
+                            "rrname": {
+                                "type": "string"
+                            },
+                            "rrtype": {
+                                "type": "string"
+                            },
+                            "tx_id": {
+                                "type": "integer"
+                            },
+                            "type": {
+                                "type": "string"
+                            },
+                            "z": {
+                                "type": "boolean"
+                            },
+                            "opcode": {
+                                "description": "DNS opcode as an integer",
+                                "type": "integer"
+                            }
+                        },
+                        "additionalProperties": false
+                    }
+                },
+                "queries": {
+                    "$comment": "EVE DNS v3 style query logging.",
                     "type": "array",
                     "minItems": 1,
                     "items": {
                                 "type": "string"
                             }
                         },
+                        "SOA": {
+                            "type": "array",
+                            "minItems": 1,
+                            "items": {
+                                "$ref": "#/$defs/dns.soa"
+                            }
+                        },
                         "SRV": {
                             "type": "array",
                             "minItems": 1,
         }
     },
     "$defs": {
+        "dns.soa": {
+            "type": "object",
+            "properties": {
+                "expire": {
+                    "type": "integer"
+                },
+                "minimum": {
+                    "type": "integer"
+                },
+                "mname": {
+                    "type": "string"
+                },
+                "refresh": {
+                    "type": "integer"
+                },
+                "retry": {
+                    "type": "integer"
+                },
+                "rname": {
+                    "type": "string"
+                },
+                "serial": {
+                    "type": "integer"
+                }
+            },
+            "additionalProperties": false
+        },
         "dns.authorities": {
             "type": "array",
             "minItems": 1,
                         "type": "integer"
                     },
                     "soa": {
-                        "type": "object",
-                        "properties": {
-                            "expire": {
-                                "type": "integer"
-                            },
-                            "minimum": {
-                                "type": "integer"
-                            },
-                            "mname": {
-                                "type": "string"
-                            },
-                            "refresh": {
-                                "type": "integer"
-                            },
-                            "retry": {
-                                "type": "integer"
-                            },
-                            "rname": {
-                                "type": "string"
-                            },
-                            "serial": {
-                                "type": "integer"
-                            }
-                        },
-                        "additionalProperties": false
+                        "$ref": "#/$defs/dns.soa"
                     }
                 },
                 "additionalProperties": false
index e4bfd91976d755f58019cce9959b3f9a104e2b39..32f68603cc07ab0dbb80c875e2628dcaa6a3dbcb 100644 (file)
@@ -1,4 +1,4 @@
-/* Copyright (C) 2017 Open Information Security Foundation
+/* Copyright (C) 2017-2024 Open Information Security Foundation
  *
  * You can copy, redistribute or modify this Program under the terms of
  * the GNU General Public License version 2 as published by the Free
@@ -403,7 +403,7 @@ fn dns_log_opt(opt: &DNSRDataOPT) -> Result<JsonBuilder, JsonError> {
 
     js.close()?;
     Ok(js)
-} 
+}
 
 /// Log SOA section fields.
 fn dns_log_soa(soa: &DNSRDataSOA) -> Result<JsonBuilder, JsonError> {
@@ -647,6 +647,97 @@ fn dns_log_json_answer(
     Ok(())
 }
 
+/// V3 style answer logging.
+fn dns_log_json_answers(
+    jb: &mut JsonBuilder, response: &DNSMessage, flags: u64,
+) -> Result<(), JsonError> {
+    if !response.answers.is_empty() {
+        let mut js_answers = JsonBuilder::try_new_array()?;
+
+        // For grouped answers we use a HashMap keyed by the rrtype.
+        let mut answer_types = HashMap::new();
+
+        for answer in &response.answers {
+            if flags & LOG_FORMAT_GROUPED != 0 {
+                let type_string = dns_rrtype_string(answer.rrtype);
+                match &answer.data {
+                    DNSRData::A(addr) | DNSRData::AAAA(addr) => {
+                        if !answer_types.contains_key(&type_string) {
+                            answer_types
+                                .insert(type_string.to_string(), JsonBuilder::try_new_array()?);
+                        }
+                        if let Some(a) = answer_types.get_mut(&type_string) {
+                            a.append_string(&dns_print_addr(addr))?;
+                        }
+                    }
+                    DNSRData::CNAME(bytes)
+                    | DNSRData::MX(bytes)
+                    | DNSRData::NS(bytes)
+                    | DNSRData::TXT(bytes)
+                    | DNSRData::NULL(bytes)
+                    | DNSRData::PTR(bytes) => {
+                        if !answer_types.contains_key(&type_string) {
+                            answer_types
+                                .insert(type_string.to_string(), JsonBuilder::try_new_array()?);
+                        }
+                        if let Some(a) = answer_types.get_mut(&type_string) {
+                            a.append_string_from_bytes(bytes)?;
+                        }
+                    }
+                    DNSRData::SOA(soa) => {
+                        if !answer_types.contains_key(&type_string) {
+                            answer_types
+                                .insert(type_string.to_string(), JsonBuilder::try_new_array()?);
+                        }
+                        if let Some(a) = answer_types.get_mut(&type_string) {
+                            a.append_object(&dns_log_soa(soa)?)?;
+                        }
+                    }
+                    DNSRData::SSHFP(sshfp) => {
+                        if !answer_types.contains_key(&type_string) {
+                            answer_types
+                                .insert(type_string.to_string(), JsonBuilder::try_new_array()?);
+                        }
+                        if let Some(a) = answer_types.get_mut(&type_string) {
+                            a.append_object(&dns_log_sshfp(sshfp)?)?;
+                        }
+                    }
+                    DNSRData::SRV(srv) => {
+                        if !answer_types.contains_key(&type_string) {
+                            answer_types
+                                .insert(type_string.to_string(), JsonBuilder::try_new_array()?);
+                        }
+                        if let Some(a) = answer_types.get_mut(&type_string) {
+                            a.append_object(&dns_log_srv(srv)?)?;
+                        }
+                    }
+                    _ => {}
+                }
+            }
+
+            if flags & LOG_FORMAT_DETAILED != 0 {
+                js_answers.append_object(&dns_log_json_answer_detail(answer)?)?;
+            }
+        }
+
+        js_answers.close()?;
+
+        if flags & LOG_FORMAT_DETAILED != 0 {
+            jb.set_object("answers", &js_answers)?;
+        }
+
+        if flags & LOG_FORMAT_GROUPED != 0 {
+            jb.open_object("grouped")?;
+            for (k, mut v) in answer_types.drain() {
+                v.close()?;
+                jb.set_object(&k, &v)?;
+            }
+            jb.close()?;
+        }
+    }
+    Ok(())
+}
+
 fn dns_log_query(
     tx: &DNSTransaction, i: u16, flags: u64, jb: &mut JsonBuilder,
 ) -> Result<bool, JsonError> {
@@ -687,6 +778,115 @@ pub extern "C" fn SCDnsLogJsonQuery(
     }
 }
 
+/// Common logger for DNS requests and responses.
+///
+/// It is expected that the JsonBuilder is an open object that the DNS
+/// transaction will be logged into. This function will not create the
+/// "dns" object.
+///
+/// This logger implements V3 style DNS logging.
+fn log_json(tx: &mut DNSTransaction, flags: u64, jb: &mut JsonBuilder) -> Result<(), JsonError> {
+    jb.open_object("dns")?;
+    jb.set_int("version", 3)?;
+
+    let message = if let Some(request) = &tx.request {
+        jb.set_string("type", "request")?;
+        request
+    } else if let Some(response) = &tx.response {
+        jb.set_string("type", "response")?;
+        response
+    } else {
+        debug_validate_fail!("unreachable");
+        return Ok(());
+    };
+
+    // The internal Suricata transaction ID.
+    jb.set_uint("tx_id", tx.id - 1)?;
+
+    // The on the wire DNS transaction ID.
+    jb.set_uint("id", tx.tx_id() as u64)?;
+
+    // Log header fields. Should this be a sub-object?
+    let header = &message.header;
+    jb.set_string("flags", format!("{:x}", header.flags).as_str())?;
+    if header.flags & 0x8000 != 0 {
+        jb.set_bool("qr", true)?;
+    }
+    if header.flags & 0x0400 != 0 {
+        jb.set_bool("aa", true)?;
+    }
+    if header.flags & 0x0200 != 0 {
+        jb.set_bool("tc", true)?;
+    }
+    if header.flags & 0x0100 != 0 {
+        jb.set_bool("rd", true)?;
+    }
+    if header.flags & 0x0080 != 0 {
+        jb.set_bool("ra", true)?;
+    }
+    if header.flags & 0x0040 != 0 {
+        jb.set_bool("z", true)?;
+    }
+    let opcode = ((header.flags >> 11) & 0xf) as u8;
+    jb.set_uint("opcode", opcode as u64)?;
+    jb.set_string("rcode", &dns_rcode_string(header.flags))?;
+
+    if !message.queries.is_empty() {
+        jb.open_array("queries")?;
+        for query in &message.queries {
+            if dns_log_rrtype_enabled(query.rrtype, flags) {
+                jb.start_object()?
+                    .set_string_from_bytes("rrname", &query.name)?
+                    .set_string("rrtype", &dns_rrtype_string(query.rrtype))?
+                    .close()?;
+            }
+        }
+        jb.close()?;
+    }
+
+    if !message.answers.is_empty() {
+        dns_log_json_answers(jb, message, flags)?;
+    }
+
+    if !message.authorities.is_empty() {
+        jb.open_array("authorities")?;
+        for auth in &message.authorities {
+            let auth_detail = dns_log_json_answer_detail(auth)?;
+            jb.append_object(&auth_detail)?;
+        }
+        jb.close()?;
+    }
+
+    if !message.additionals.is_empty() {
+        let mut is_jb_open = false;
+        for add in &message.additionals {
+            if let DNSRData::OPT(rdata) = &add.data {
+                if rdata.is_empty() {
+                    continue;
+                }
+            }
+            if !is_jb_open {
+                jb.open_array("additionals")?;
+                is_jb_open = true;
+            }
+            let add_detail = dns_log_json_answer_detail(add)?;
+            jb.append_object(&add_detail)?;
+        }
+        if is_jb_open {
+            jb.close()?;
+        }
+    }
+
+    jb.close()?;
+    Ok(())
+}
+
+/// FFI wrapper around the common V3 style DNS logger.
+#[no_mangle]
+pub extern "C" fn SCDnsLogJson(tx: &mut DNSTransaction, flags: u64, jb: &mut JsonBuilder) -> bool {
+    log_json(tx, flags, jb).is_ok()
+}
+
 #[no_mangle]
 pub extern "C" fn SCDnsLogJsonAnswer(
     tx: &DNSTransaction, flags: u64, js: &mut JsonBuilder,
index be3767e2eaf7393dee607af1e494567882c4df0e..b5d9f45d8333d8c3524901fb8c33184f91a29af9 100644 (file)
@@ -246,65 +246,10 @@ typedef struct LogDnsLogThread_ {
     OutputJsonThreadCtx *ctx;
 } LogDnsLogThread;
 
-static JsonBuilder *JsonDNSLogQuery(void *txptr)
-{
-    JsonBuilder *queryjb = jb_new_array();
-    if (queryjb == NULL) {
-        return NULL;
-    }
-    bool has_query = false;
-
-    for (uint16_t i = 0; i < UINT16_MAX; i++) {
-        JsonBuilder *js = jb_new_object();
-        if (!SCDnsLogJsonQuery((void *)txptr, i, LOG_ALL_RRTYPES, js)) {
-            jb_free(js);
-            break;
-        }
-        jb_close(js);
-        has_query = true;
-        jb_append_object(queryjb, js);
-        jb_free(js);
-    }
-
-    if (!has_query) {
-        jb_free(queryjb);
-        return NULL;
-    }
-
-    jb_close(queryjb);
-    return queryjb;
-}
-
-static JsonBuilder *JsonDNSLogAnswer(void *txptr)
-{
-    if (!SCDnsLogAnswerEnabled(txptr, LOG_ALL_RRTYPES)) {
-        return NULL;
-    } else {
-        JsonBuilder *js = jb_new_object();
-        SCDnsLogJsonAnswer(txptr, LOG_ALL_RRTYPES, js);
-        jb_close(js);
-        return js;
-    }
-}
-
 bool AlertJsonDns(void *txptr, JsonBuilder *js)
 {
-    bool r = false;
-    jb_open_object(js, "dns");
-    JsonBuilder *qjs = JsonDNSLogQuery(txptr);
-    if (qjs != NULL) {
-        jb_set_object(js, "query", qjs);
-        jb_free(qjs);
-        r = true;
-    }
-    JsonBuilder *ajs = JsonDNSLogAnswer(txptr);
-    if (ajs != NULL) {
-        jb_set_object(js, "answer", ajs);
-        jb_free(ajs);
-        r = true;
-    }
-    jb_close(js);
-    return r;
+    return SCDnsLogJson(
+            txptr, LOG_FORMAT_DETAILED | LOG_QUERIES | LOG_ANSWERS | LOG_ALL_RRTYPES, js);
 }
 
 static int JsonDnsLoggerToServer(ThreadVars *tv, void *thread_data,