From 1777b58fa905c7154c066ec363b47df93b641eb9 Mon Sep 17 00:00:00 2001 From: Philip Akekudaga Date: Mon, 21 Sep 2026 11:13:11 -0400 Subject: [PATCH] doc/userguide: document modbus keywords Document modbus.unit_id, modbus.transaction_id, modbus.protocol_id, modbus.function, modbus.subfunction, modbus.exception_code, modbus.read.address, modbus.read.quantity, modbus.write.address, modbus.write.quantity and modbus.write.value, naming the EVE field each one is taken from and how error responses and write response echoes are matched. List the names accepted by modbus.function, modbus.subfunction and modbus.exception_code. Say that the names logged for codes that have none are not accepted, as they stand for more than one value, and that a name does not match the error response of a request, since the logged name drops the error bit that the matched value carries. Note how the address keywords differ from the 1-based addressing of the modbus keyword in the sections of the two keywords concerned, rather than once for the whole set. Ticket: #8131. --- doc/userguide/rules/modbus-keyword.rst | 287 +++++++++++++++++++++++++ 1 file changed, 287 insertions(+) diff --git a/doc/userguide/rules/modbus-keyword.rst b/doc/userguide/rules/modbus-keyword.rst index a485c71a19..18200b1a1a 100644 --- a/doc/userguide/rules/modbus-keyword.rst +++ b/doc/userguide/rules/modbus-keyword.rst @@ -129,3 +129,290 @@ V1.0b, the MODBUS slave device addresses on serial line are assigned from 1 to Paper and presentation (in french) on Modbus support are available : http://www.ssi.gouv.fr/agence/publication/detection-dintrusion-dans-les-systemes-industriels-suricata-et-le-cas-modbus/ + +Per-value keywords +------------------ + +The following keywords match on single values of a Modbus transaction, +taken from the same fields the EVE ``modbus`` event logs. They all use +:ref:`unsigned integers ` and support ranges and +negation. In to-server direction they match on the request; in to-client +direction they match on the response, for the values a response carries. +Read requests are only matched in to-server direction, as a read response +carries data rather than an address and a quantity. + +modbus.unit_id +~~~~~~~~~~~~~~ + +Match on the Modbus unit identifier, the address of a slave device +connected on a sub-network behind a bridge or gateway. + +modbus.unit_id uses an :ref:`unsigned 8-bits integer `. + +Examples:: + + modbus.unit_id:10; + modbus.unit_id:>247; + +modbus.transaction_id +~~~~~~~~~~~~~~~~~~~~~ + +Match on the Modbus/TCP transaction identifier used to pair requests +and responses. + +modbus.transaction_id uses an +:ref:`unsigned 16-bits integer `. + +Example:: + + modbus.transaction_id:1; + +modbus.protocol_id +~~~~~~~~~~~~~~~~~~ + +Match on the Modbus/TCP protocol identifier. The specification requires +this field to be 0, so a non-zero value is an anomaly. + +modbus.protocol_id uses an +:ref:`unsigned 16-bits integer `. + +Example:: + + modbus.protocol_id:!0; + +modbus.function +~~~~~~~~~~~~~~~ + +Match on the Modbus function code, as it is logged in ``function_raw``. +It can be specified as the integer value or as the function code name +logged in ``function_code``. Names are case insensitive and can be +negated with ``!``. + +.. note:: + + Error responses carry the request function code with the highest bit + set (e.g. 136 for a failed function 8), so ``modbus.function:>127;`` + matches any error response. ``function_code`` is logged without that + bit, so a name matches a request, and a response that is not an error, + but never an error response. Match those by value, with + ``modbus.function:>127;`` or the raised code. + + ``Unknown`` is logged in ``function_code`` for every code that has no + name, so it does not identify one value and is not accepted here. The + codes it covers are still matched by value. + +modbus.function uses an :ref:`unsigned 8-bits integer `. + +Syntax:: + + modbus.function:[op] + modbus.function:[!] + +See ``suricata --list-keywords=json | jq '."modbus.function".enum_values'`` + +Where name is one of: + +* RdCoils +* RdDiscreteInputs +* RdHoldRegs +* RdInputRegs +* WrSingleCoil +* WrSingleReg +* RdExcStatus +* Diagnostic +* Program484 +* Poll484 +* GetCommEventCtr +* GetCommEventLog +* ProgramController +* PollController +* WrMultCoils +* WrMultRegs +* ReportServerID +* Program884 +* ResetCommLink +* RdFileRec +* WrFileRec +* MaskWrReg +* RdWrMultRegs +* RdFIFOQueue +* MEI + +Examples:: + + modbus.function:8; + modbus.function:Diagnostic; # same as above + modbus.function:!RdCoils; + modbus.function:>127; + +modbus.subfunction +~~~~~~~~~~~~~~~~~~ + +Match on the subfunction code of Modbus Diagnostics (function 8) +messages. It can be specified as the integer value or as the +subfunction name logged in ``diagnostic.code``. Names are case +insensitive and can be negated with ``!``. + +.. note:: + + ``Reserved`` is logged in ``diagnostic.code`` for every subfunction + that has no name, so it does not identify one value and is not + accepted here. Those subfunctions are still matched by value. + +modbus.subfunction uses an +:ref:`unsigned 16-bits integer `. + +Syntax:: + + modbus.subfunction:[op] + modbus.subfunction:[!] + +See ``suricata --list-keywords=json | jq '."modbus.subfunction".enum_values'`` + +Where name is one of: + +* RetQueryData +* RestartCommOpt +* RetDiagReg +* ChangeInputDelimiter +* ForceListenOnlyMode +* ClearCtrDiagReg +* RetBusMsgCount +* RetBusCommErrCount +* RetBusExcErrCount +* RetServerMsgCount +* RetServerNoRespCount +* RetServerNAKCount +* RetServerBusyCount +* RetBusCharOverrunCount +* RetOverrunErrCount +* ClearOverrunCounterFlag +* GetClearPlusStats + +Examples:: + + modbus.function:8; modbus.subfunction:4; + modbus.function:Diagnostic; modbus.subfunction:ForceListenOnlyMode; + +modbus.exception_code +~~~~~~~~~~~~~~~~~~~~~ + +Match on the exception code of Modbus error responses. It can be +specified as the integer value or as the exception name logged in +``exception.code``. Names are case insensitive and can be negated with +``!``. + +.. note:: + + ``Unknown`` is logged in ``exception.code`` for every exception code + that has no name, so it does not identify one value and is not + accepted here. Those codes are still matched by value. + +modbus.exception_code uses an +:ref:`unsigned 8-bits integer `. + +Syntax:: + + modbus.exception_code:[op] + modbus.exception_code:[!] + +See ``suricata --list-keywords=json | jq '."modbus.exception_code".enum_values'`` + +Where name is one of: + +* IllegalFunction +* IllegalDataAddr +* IllegalDataValue +* ServerDeviceFail +* Ack +* ServerDeviceBusy +* NegAck +* MemParityErr +* GatewayPathUnavailable +* GatewayTargetFailToResp + +Examples:: + + modbus.exception_code:11; + modbus.exception_code:GatewayTargetFailToResp; # same as above + +modbus.read.address +~~~~~~~~~~~~~~~~~~~ + +Match on the starting address of Modbus read requests. + +Unlike the ``address`` option of the ``modbus`` keyword above, this +matches the raw protocol value as it is logged, without the +1 offset, +and matches only the starting address of an access rather than the +whole accessed range. + +modbus.read.address uses an +:ref:`unsigned 16-bits integer `. + +Example:: + + modbus.read.address:1000; + +modbus.read.quantity +~~~~~~~~~~~~~~~~~~~~ + +Match on the quantity of items in Modbus read requests. + +modbus.read.quantity uses an +:ref:`unsigned 16-bits integer `. + +Example:: + + modbus.read.quantity:>1000; + +modbus.write.address +~~~~~~~~~~~~~~~~~~~~ + +Match on the starting address of Modbus write accesses. In to-client +direction this matches the address echoed in write responses. + +Unlike the ``address`` option of the ``modbus`` keyword above, this +matches the raw protocol value as it is logged, without the +1 offset, +and matches only the starting address of an access rather than the +whole accessed range. + +modbus.write.address uses an +:ref:`unsigned 16-bits integer `. + +Example:: + + modbus.write.address:500; + +modbus.write.quantity +~~~~~~~~~~~~~~~~~~~~~ + +Match on the quantity of items in Modbus multiple-write requests, as it +is logged in ``write.quantity``. In to-client direction this matches the +quantity echoed in multiple-write responses, which the protocol carries +in the value field and the log reports as ``write.data``. + +modbus.write.quantity uses an +:ref:`unsigned 16-bits integer `. + +Example:: + + modbus.write.quantity:>100; + +modbus.write.value +~~~~~~~~~~~~~~~~~~ + +Match on the value of Modbus single-write accesses, as it is logged in +``write.data``. In to-client direction this matches the value echoed in +single-write responses. + +.. note:: + + Multiple-write responses are not matched, as they echo a quantity + rather than a value (see ``modbus.write.quantity``). + +modbus.write.value uses an +:ref:`unsigned 16-bits integer `. + +Example:: + + modbus.write.address:500; modbus.write.value:>200;