diff --git a/etc/schema.json b/etc/schema.json index 356e4fe9829a..af054c6e10e3 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 e4bfd91976d7..32f68603cc07 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 be3767e2eaf7..ef1ed97d0f39 100644 --- a/src/output-json-dns.c +++ b/src/output-json-dns.c @@ -289,22 +289,8 @@ static JsonBuilder *JsonDNSLogAnswer(void *txptr) 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,