From: Jason Ish Date: Thu, 27 Jun 2024 22:54:25 +0000 (-0600) Subject: dns: new v3 style logging for alerts X-Git-Tag: suricata-8.0.0-beta1~1042 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=df656324baa73abfc089feb33286fad3f07a3df8;p=thirdparty%2Fsuricata.git dns: new v3 style logging for alerts 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 --- diff --git a/etc/schema.json b/etc/schema.json index 356e4fe982..af054c6e10 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -1062,6 +1062,9 @@ "ttl": { "type": "integer" }, + "soa": { + "$ref": "#/$defs/dns.soa" + }, "srv": { "type": "object", "properties": { @@ -1108,6 +1111,40 @@ "$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": { @@ -1237,6 +1274,13 @@ "type": "string" } }, + "SOA": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/dns.soa" + } + }, "SRV": { "type": "array", "minItems": 1, @@ -6302,6 +6346,33 @@ } }, "$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, @@ -6321,31 +6392,7 @@ "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 diff --git a/rust/src/dns/log.rs b/rust/src/dns/log.rs index e4bfd91976..32f68603cc 100644 --- a/rust/src/dns/log.rs +++ b/rust/src/dns/log.rs @@ -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 { js.close()?; Ok(js) -} +} /// Log SOA section fields. fn dns_log_soa(soa: &DNSRDataSOA) -> Result { @@ -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 { @@ -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, diff --git a/src/output-json-dns.c b/src/output-json-dns.c index be3767e2ea..b5d9f45d83 100644 --- a/src/output-json-dns.c +++ b/src/output-json-dns.c @@ -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,