Writing a variables correlation script

As is the case for scriptable protocols, variable correlation scripts are written in Lua. For more information on the language, please refer to the official Lua documentation (https://www.lua.org/start.html).

The only requirement for variable correlation scripts is that a global function with name on_receive_variable exists. Thus, the basic structure of a variable correlation script is:


function on_receive_variable(node_id, namespace, name, data_value, is_from_config)
end

Function on_receive_variable will be invoked on every variable update that takes place within Guardian. On every invocation, this function will receive 5 arguments which will provide information on the new variable value and its context:

  • node_id: The identity of the node, to which the variable belongs.
  • namespace: Identifier of the variable container, also known as remote terminal unit (RTU) identifier (ID). When the variable update is coming from a protocol that does not support it, it will be empty or hold a fixed, hardcoded value.
  • name: The name of the variable being updated.
  • data_value: The data value is a table value describing the updated variable value. Consult the API reference for the fields exposed by this value.
  • is_from_config: Variable updates may arrive over traffic or due to configuration commands. This argument will be true in case the variable update has been administered via command.

By adding logic in the on_receive_variable function, it is possible to make the system respond to variable update events in user specific ways. For example, the script below will raise a Variable Flow Anomaly alert, if the variable with name ioa-515 raises above a threshold of 200:


function on_receive_variable(node_id, namespace, name, data_value, is_from_config)
    if name == "ioa-515" and data_value.value > 200 then
	      AlertFactory.variable_flow_anomaly("Unexpected variable value!")
    end
end

The above example shows how a simple variables correlation script may look like. In practice though such usages are not expected, since Guardian provides other, easier ways for implementing such checks (i.e. assertions).

The scriptable variables correlation becomes a much more interesting mechanism when scripts become stateful: by using top level scope variables, it is possible to maintain state that is consulted and updated across different script invocations. For example, if we want to introduce a check on the first derivative of variable ioa-515, the script below may serve as a basis:


local values_per_node = {}

function on_receive_variable(node_id, namespace, name, data_value, is_from_config)
    if name == "ioa-515" then
    if values_per_node[node_id] == nil then
        values_per_node[node_id] = {}
    end
    work_table = values_per_node[node_id]

    work_table.previous = work_table.current
    work_table.current = { time=data_value.time, value=data_value.value }

    if work_table.previous ~= nil then
        delta_position = work_table.current.value - work_table.previous.value
			
        -- Time is reported in msec
        delta_time = (work_table.current.time - work_table.previous.time) / 1000.0

        computed_rate = delta_position / delta_time

        if (computed_rate > 1.0) then
            AlertFactory.variable_flow_anomaly("Derivative increased above threshold!")
        end
    end
  end
end
    

Notes on above script:

  • The local values_per_node = {} statement initializes a table on top level script scope. This table is maintained across script invocations and is used to keep the two last values of variable ioa-515 per node.
  • In Lua, accessing fields that don't exist does not raise an error, but simply returns value nil. Thus the statement work_table.previous = work_table.current can be invoked regardless if there exists a current value or not.
  • In production level scripts, it would be necessary to check whether the current and previous times are identical, in order to avoid dividing by zero.