Skip to content

Commit

Permalink
dns: new v3 style logging for alerts
Browse files Browse the repository at this point in the history
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: OISF#6281
  • Loading branch information
jasonish committed Jul 4, 2024
1 parent 354af42 commit ca62fa7
Show file tree
Hide file tree
Showing 3 changed files with 276 additions and 43 deletions.
97 changes: 72 additions & 25 deletions etc/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,9 @@
"ttl": {
"type": "integer"
},
"soa": {
"$ref": "#/$defs/dns.soa"
},
"srv": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -1237,6 +1274,13 @@
"type": "string"
}
},
"SOA": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/dns.soa"
}
},
"SRV": {
"type": "array",
"minItems": 1,
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
204 changes: 202 additions & 2 deletions rust/src/dns/log.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 2 additions & 16 deletions src/output-json-dns.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit ca62fa7

Please sign in to comment.