Complete DNP3 SBO Policy¶
This page embeds the executable, tested multi-object DNP3 Select-Before-Operate policy from the repository. The displayed YAML is not a shortened marketing illustration.

Figure — Complete DNP3 SBO policy example mapped to the protected select/operate sequence.
What to customize¶
- Select and Operate TTLs;
- Direct Operate policy;
- customer severity and incident metadata;
- approved master/outstation scope, if a narrower allow-list is required;
- final default behavior for the deployment boundary.
Operational behavior¶
- malformed control identity fails closed;
- Select request creates only a pending state;
- only a successful matching response confirms the Select;
- a matching Operate consumes the selected state atomically;
- mismatched, expired, concurrent, or replayed Operate is dropped;
- multi-point and multi-object target order is canonicalized.
Canonical YAML¶
# Production-style DNP3 SBO policy for complete multi-point CROB/G41 commands.
# Customize TTL and the final default policy for the deployment. The rule is
# fail-closed for malformed control requests and consumes a Select exactly once.
version: 1
rules:
- name: dnp3-sbo-drop-incomplete-control
order: 5
condition: >
dnp3.IsDNP3() && dnp3.IsRequest() && dnp3.IsControl() &&
(!dnp3.HasCanonicalEndpoints() || !dnp3.HasControlFingerprint())
action: drop
log: true
meta:
policy: '"sbo_required"'
violation: '"incomplete_control_identity"'
function: 'dnp3.meta.FunctionCodeText()'
- name: dnp3-sbo-drop-direct-operate
order: 10
condition: >
dnp3.IsDNP3() && dnp3.IsRequest() &&
dnp3.IsDirectOperate() && dnp3.HasControlFingerprint()
action: drop
log: true
meta:
policy: '"sbo_required"'
violation: '"direct_operate"'
master: 'dnp3.Master()'
outstation: 'dnp3.Outstation()'
targets: 'dnp3.ControlTargetCount()'
fingerprint: 'dnp3.ControlFingerprint()'
- name: dnp3-sbo-record-select-request
order: 20
condition: >
dnp3.IsDNP3() && dnp3.IsRequest() && dnp3.IsSelect() &&
dnp3.HasCanonicalEndpoints() && dnp3.HasApplicationSequence() &&
dnp3.HasControlFingerprint()
action: set_state
state_key_expr: '"dnp3:sbo:" + dnp3.ControlSessionKey()'
state_value_expr: >
"pending|" + string(dnp3.ApplicationSequence()) + "|" +
dnp3.ControlFingerprint()
state_ttl: 5
log: true
meta:
phase: '"select_request"'
master: 'dnp3.Master()'
outstation: 'dnp3.Outstation()'
sequence: 'dnp3.ApplicationSequence()'
targets: 'dnp3.ControlTargetCount()'
fingerprint: 'dnp3.ControlFingerprint()'
- name: dnp3-sbo-confirm-select-response
order: 30
condition: >
dnp3.IsDNP3() && dnp3.IsResponse() &&
dnp3.HasCanonicalEndpoints() && dnp3.HasApplicationSequence() &&
dnp3.ControlResponseOK()
action: transition_state
state_key_expr: '"dnp3:sbo:" + dnp3.ControlSessionKey()'
state_from_expr: >
"pending|" + string(dnp3.ApplicationSequence()) + "|" +
dnp3.ControlFingerprint()
state_value_expr: '"selected|" + dnp3.ControlFingerprint()'
state_ttl: 5
log: true
meta:
phase: '"select_response"'
result: '"success"'
master: 'dnp3.Master()'
outstation: 'dnp3.Outstation()'
sequence: 'dnp3.ApplicationSequence()'
targets: 'dnp3.ControlTargetCount()'
fingerprint: 'dnp3.ControlFingerprint()'
- name: dnp3-sbo-clear-mismatched-control-response
order: 34
description: Invalidate pending state when a successful response cannot confirm it.
condition: >
dnp3.IsDNP3() && dnp3.IsResponse() && dnp3.ControlResponseOK() &&
state_action_rule == "dnp3-sbo-confirm-select-response" &&
!state_action_applied
action: delete_state
state_key_expr: '"dnp3:sbo:" + dnp3.ControlSessionKey()'
log: true
meta:
phase: '"select_response"'
violation: '"select_response_mismatch"'
sequence: 'dnp3.ApplicationSequence()'
targets: 'dnp3.ControlTargetCount()'
fingerprint: 'dnp3.ControlFingerprint()'
- name: dnp3-sbo-clear-failed-control-response
order: 35
condition: >
dnp3.IsDNP3() && dnp3.IsResponse() &&
dnp3.HasCanonicalEndpoints() && dnp3.HasControlFingerprint() &&
!dnp3.ControlResponseOK()
action: delete_state
state_key_expr: '"dnp3:sbo:" + dnp3.ControlSessionKey()'
log: true
meta:
phase: '"control_response"'
result: '"failed"'
master: 'dnp3.Master()'
outstation: 'dnp3.Outstation()'
targets: 'dnp3.ControlTargetCount()'
fingerprint: 'dnp3.ControlFingerprint()'
iin: 'dnp3.meta.IIN()'
- name: dnp3-sbo-consume-operate
order: 40
condition: >
dnp3.IsDNP3() && dnp3.IsRequest() && dnp3.IsOperate() &&
dnp3.HasCanonicalEndpoints() && dnp3.HasControlFingerprint()
action: transition_state
state_key_expr: '"dnp3:sbo:" + dnp3.ControlSessionKey()'
state_from_expr: '"selected|" + dnp3.ControlFingerprint()'
state_value: consumed
state_ttl: 1
log: true
meta:
phase: '"operate"'
master: 'dnp3.Master()'
outstation: 'dnp3.Outstation()'
targets: 'dnp3.ControlTargetCount()'
fingerprint: 'dnp3.ControlFingerprint()'
- name: dnp3-sbo-accept-valid-operate
order: 50
condition: >
dnp3.IsDNP3() && dnp3.IsRequest() && dnp3.IsOperate() &&
state_action_rule == "dnp3-sbo-consume-operate" &&
state_action_applied
action: accept
log: true
meta:
policy: '"sbo_required"'
result: '"valid_operate"'
master: 'dnp3.Master()'
outstation: 'dnp3.Outstation()'
targets: 'dnp3.ControlTargetCount()'
fingerprint: 'dnp3.ControlFingerprint()'
- name: dnp3-sbo-drop-invalid-operate
order: 60
condition: 'dnp3.IsDNP3() && dnp3.IsRequest() && dnp3.IsOperate()'
action: drop
log: true
meta:
policy: '"sbo_required"'
violation: '"operate_without_matching_select"'
master: 'dnp3.Master()'
outstation: 'dnp3.Outstation()'
targets: 'dnp3.ControlTargetCount()'
fingerprint: 'dnp3.ControlFingerprint()'
- name: dnp3-sbo-default-accept
order: 1000
condition: 'true'
action: accept
Source path: rules/muli_state_rules/dnp3_sbo_multi_control.yaml
Test evidence¶
The Rule Engine test suite loads every YAML in rules/muli_state_rules. Dedicated
end-to-end tests exercise Select request, successful response, Operate, mismatch,
failed response, expiration, replay, multi-object ordering, and concurrent consumption.