Initial: Node-RED flow validator with conftest/Rego
- Declarative YAML flow format - Python flow2json converter with auto-positioning - Rego policies: 14 validation rules for flow integrity - Conftest integration - justfile workflow: generate → validate → deploy
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
package nodered
|
||||
import future.keywords.if
|
||||
import future.keywords.contains
|
||||
import future.keywords.in
|
||||
|
||||
# helpers
|
||||
config_node(typ) if typ in {"mqtt-broker", "ui_tab", "ui_base"}
|
||||
welcome_isolated(typ) if typ in {"debug", "mqtt-broker", "ui_tab", "ui_group", "ui_text", "ui_chart", "ui_gauge", "ui_button", "ui_slider", "ui_switch", "inject", "comment", "mqtt in", "catch", "status", "link in"}
|
||||
|
||||
broker_node(id) if {
|
||||
some node in input
|
||||
node.type == "mqtt-broker"
|
||||
node.id == id
|
||||
}
|
||||
|
||||
ui_group(id) if {
|
||||
some node in input
|
||||
node.type == "ui_group"
|
||||
node.id == id
|
||||
}
|
||||
|
||||
ui_tab(id) if {
|
||||
some node in input
|
||||
node.type == "ui_tab"
|
||||
node.id == id
|
||||
}
|
||||
|
||||
node_exists(id) if {
|
||||
some node in input
|
||||
node.id == id
|
||||
}
|
||||
|
||||
has_inbound_wire(target_id) if {
|
||||
some node in input
|
||||
some wire in node.wires
|
||||
target_id in wire
|
||||
}
|
||||
|
||||
has_outbound_wire(source_id) if {
|
||||
some node in input
|
||||
node.id == source_id
|
||||
node.wires
|
||||
count(node.wires) > 0
|
||||
}
|
||||
|
||||
# ---- DENY RULES ----
|
||||
|
||||
deny_missing_type contains msg if {
|
||||
some node in input
|
||||
not node.type
|
||||
msg := sprintf("node %s: missing type field", [node.id])
|
||||
}
|
||||
|
||||
deny_missing_field contains msg if {
|
||||
some node in input
|
||||
node.type == "mqtt-broker"
|
||||
required := {"type", "broker", "port"}
|
||||
some f in required
|
||||
not node[f]
|
||||
msg := sprintf("node %s (mqtt-broker): missing %s", [node.id, f])
|
||||
}
|
||||
|
||||
deny_missing_field contains msg if {
|
||||
some node in input
|
||||
node.type == "mqtt in"
|
||||
required := {"type", "topic", "broker", "z"}
|
||||
some f in required
|
||||
not node[f]
|
||||
msg := sprintf("node %s (mqtt in): missing %s", [node.id, f])
|
||||
}
|
||||
|
||||
deny_missing_field contains msg if {
|
||||
some node in input
|
||||
node.type == "mqtt out"
|
||||
required := {"type", "broker", "z"}
|
||||
some f in required
|
||||
not node[f]
|
||||
msg := sprintf("node %s (mqtt out): missing %s", [node.id, f])
|
||||
}
|
||||
|
||||
deny_missing_field contains msg if {
|
||||
some node in input
|
||||
node.type == "debug"
|
||||
required := {"type", "z"}
|
||||
some f in required
|
||||
not node[f]
|
||||
msg := sprintf("node %s (debug): missing %s", [node.id, f])
|
||||
}
|
||||
|
||||
deny_missing_field contains msg if {
|
||||
some node in input
|
||||
node.type in {"ui_text", "ui_button", "ui_chart", "ui_gauge", "ui_slider", "ui_switch"}
|
||||
required := {"type", "group", "z"}
|
||||
some f in required
|
||||
not node[f]
|
||||
msg := sprintf("node %s (%s): missing %s", [node.id, node.type, f])
|
||||
}
|
||||
|
||||
deny_missing_field contains msg if {
|
||||
some node in input
|
||||
node.type == "ui_group"
|
||||
required := {"type", "tab", "name"}
|
||||
some f in required
|
||||
not node[f]
|
||||
msg := sprintf("node %s (ui_group): missing %s", [node.id, f])
|
||||
}
|
||||
|
||||
deny_missing_field contains msg if {
|
||||
some node in input
|
||||
node.type == "ui_tab"
|
||||
required := {"type", "name"}
|
||||
some f in required
|
||||
not node[f]
|
||||
msg := sprintf("node %s (ui_tab): missing %s", [node.id, f])
|
||||
}
|
||||
|
||||
deny_broker_ref contains msg if {
|
||||
some node in input
|
||||
node.type in {"mqtt in", "mqtt out"}
|
||||
not broker_node(node.broker)
|
||||
msg := sprintf("node %s (%s): references unknown broker %s", [node.id, node.type, node.broker])
|
||||
}
|
||||
|
||||
deny_group_ref contains msg if {
|
||||
some node in input
|
||||
node.type in {"ui_text", "ui_button", "ui_chart", "ui_gauge", "ui_slider", "ui_switch"}
|
||||
not ui_group(node.group)
|
||||
msg := sprintf("node %s (%s): references unknown group %s", [node.id, node.type, node.group])
|
||||
}
|
||||
|
||||
deny_tab_ref contains msg if {
|
||||
some node in input
|
||||
node.type == "ui_group"
|
||||
not ui_tab(node.tab)
|
||||
msg := sprintf("node %s (ui_group): references unknown tab %s", [node.id, node.tab])
|
||||
}
|
||||
|
||||
deny_wire_ref contains msg if {
|
||||
some node in input
|
||||
some wire in node.wires
|
||||
some target in wire
|
||||
not node_exists(target)
|
||||
msg := sprintf("node %s (%s): wire to unknown node %s", [node.id, node.type, target])
|
||||
}
|
||||
|
||||
deny_missing_flow contains msg if {
|
||||
some node in input
|
||||
not config_node(node.type)
|
||||
not node.z
|
||||
msg := sprintf("node %s (%s): missing flow assignment (z)", [node.id, node.type])
|
||||
}
|
||||
|
||||
deny_isolated_node contains msg if {
|
||||
some node in input
|
||||
not welcome_isolated(node.type)
|
||||
not has_inbound_wire(node.id)
|
||||
not has_outbound_wire(node.id)
|
||||
msg := sprintf("node %s (%s): has no wires", [node.id, node.type])
|
||||
}
|
||||
|
||||
# MQTT brokers must have valid port
|
||||
deny_broker_port contains msg if {
|
||||
some node in input
|
||||
node.type == "mqtt-broker"
|
||||
not is_valid_port(to_number(node.port))
|
||||
msg := sprintf("node %s (mqtt-broker): invalid port %s", [node.id, node.port])
|
||||
}
|
||||
|
||||
is_valid_port(p) if { p >= 1; p <= 65535 }
|
||||
Reference in New Issue
Block a user