‘tid’ is the transaction id to identify a request/response couple. I want to know if a robot hasn’t responded to the Controller by logging only requests that did not have a response within 1 second.
My code is :
module Modbus_Extended;
export {
redef enum Log::ID += { LOG_DETAILED,
LOG_MASK_WRITE_REGISTER,
LOG_READ_WRITE_MULTIPLE_REGISTERS};
type Modbus_Detailed: record {
ts : time &log; # Timestamp of event
tid : count &log; # Zeek unique ID for connection
id : conn_id &log; # Zeek connection struct (addresses and ports)
unit_id : count &log; # Modbus unit-id
func : string &log &optional; # Modbus Function
network_direction : string &log &optional; # Message direction (request or response)
address : count &log &optional; # Starting address for value(s) field
quantity : count &log &optional; # Number of addresses/values read or written to
values : string &log &optional; # Coils, discrete_inputs, or registers read/written to
};
global log_modbus_detailed: event(rec: Modbus_Detailed);
global transaction_ids: set[string, string] = {};
event modbus_message(c: connection,
headers: ModbusHeaders,
is_orig: bool) &priority=-5 {
local modbus_detailed_rec: Modbus_Detailed;
if(headers$tid !in transaction_ids[count]){
add transaction_ids[headers$tid, c$modbus$ts]
}else{
delete transaction_ids[headers$tid, c$modbus$ts]
}
for(i in transaction_ids[timestamp]){
if(c$modbus$ts > transactions_ids[headers$tid, i] +1)
{
Log::write(LOG_DETAILED, modbus_detailed_rec);
}
}
}
}
My guess is that I have to store transaction ids and check if I get only one occurence within this timelapse and then log it into a file, but I can figure out how to do it. Currently I can only generate logs with all the modbus traffic.
One way is to add a &create_expire attribute to your transaction_ids table and register an expiration function (&expire_func) See the docs for an explanation. The other “expire” attributes you can use follow in the docs.
Within the expiration function you can then log the just to be expired record.
You may want to key transactions by the connection identifier c$id as well as different connections may use the same tid values.
In the Zeek base code, examples of this approach are in the Tunnels module using a global table and the MQTT module, tracking publish/subscribes on the connection objects directly, for example.
I would need to log a lot of data from the event (timestamp, tid, cid, source_ip, dest_ip, unit_id, func) so I added fields to my set to store it.
The problem is that I can’t set a condition to find if the tid has already been seen in order to “refresh” the timer. In the end all my entries will be logged after 1 sec. I can’t look for a specific value in a multiple fields set. How should I do it ?
instead of keeping the individual values in as set, use a table of [conn_id, count] representing connection and transaction id, pointing to the record you want to log.
Something like:
global transactions: table[conn_id, count] of Modbus_Detailed &create_expire=1sec &expire_func=expire;
You should then be able to use
if ( [c$id, headers$tid] !in transactions ) {
local rec = Create a new Modbus_Detailed record...
transactions[c$id, headers$tid] = rec;
} else {
Log::write(transactions[c$id, headers$tid]);
delete transactions[c$id, headers$tid];
}
…and have a corresponding Log::write() in the expire() function.