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
pull/11454/head
Jason Ish 2 years ago committed by Victor Julien
parent 9ecc3573a7
commit df656324ba

@ -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

@ -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,

@ -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,

Loading…
Cancel
Save