SRL Guide
Secdit Rule Language reference for custom audit rules, finding output, expressions, variables, helpers, and examples.
What SRL Is
Secdit Rule Language is the scripting language used by Rule Manager for custom audit logic. SRL evaluates appliance configuration data, applies conditional logic, and emits pass, fail, or informational findings.
The editor metadata controls the execution scope. The SRL code decides what to inspect and what findings to emit.
if (this.status equals "enable") {
addPassFinding();
} else {
addFailFinding(
"The setting is disabled.",
"Enable the setting."
);
}
- Run Rule controls whether the rule runs once for the whole device or once per VDOM.
- Configuration Section scopes
thisto a selected config section when one is provided. - Loop Configuration Section controls whether the rule iterates each
editentry within that section. - When looping is enabled,
thisis the current entry during each iteration. - When looping is disabled and a Configuration Section is set,
thisis the selected section node. - If the rule emits no explicit finding but finishes in a fail state in legacy-compatible paths, SRL can still emit an implicit fail finding.
Statements
// Single-line comment
/* Multi-line
comment */
$value = "text";
let $name = "root";
$total += 1;
$counter++;
++$retries;
$counter -= 2;
if (this.name equals "root") {
addPassFinding();
} else {
addFailFinding("Root is missing.", "Create or restore the root entry.");
}
- Block statements use braces:
if (...) { ... },foreach (...) { ... }. - Assignments support both
let $var = ...and$var = .... $var += ...appends text or adds numbers, depending on both operand types.$var -= ...subtracts a numeric value from a numeric variable.$var++,++$var,$var--, and--$varare supported for writable numeric variables.- Prefix forms return the updated value. Postfix forms return the original value.
- Strings may use double quotes, single quotes, or backticks.
- Booleans are
trueandfalse. - Numbers may be integers or decimals.
nullis not a general-purpose runtime value in SRL expressions. It is only special-cased for supported optional finding arguments.
SRL can read both variables and configuration paths.
$adminName = this.name;
$pathText = path(this);
$parentNode = parent(this);
$policyName = config.vdoms.root.firewall.policy.10.name;
$vendor = appliance.vendor;
$wanType = appliance.interfaces.wan1.network_type;
$fieldName = "status";
$dynamicValue = this.$fieldName;
thisis the current scoped configuration node.configis the normalized configuration tree.applianceis the detected device/runtime scope passed into SRL for both audits and Test Rule runs.- Path segments are dot-separated.
- Path interpolation is supported, for example
config.system.interface.$ifaceName.ip. - If a path cannot be resolved, SRL treats it as unresolved rather than throwing a hard parse error. Most helper functions then treat it as empty/missing.
- When a config node is converted to a string, SRL renders it in a FortiGate-style config block format.
| Path | Meaning | Example |
|---|---|---|
appliance.vendor | Normalized vendor slug | fortinet |
appliance.type | Detected appliance type/platform family | fortigate |
appliance.hardware | Detected hardware/platform string | FortiGate-90E |
appliance.os | Detected OS version | 7.2.10 |
appliance.os_major_minor | Major/minor version extracted from appliance.os | 7.2 |
appliance.scope | Runtime scope mode | global, vdom, all |
appliance.multi_vdom | Whether more than one VDOM is selected for the run | true |
appliance.vdoms | Selected/detected VDOM names for the run | ["root","wan-vdom"] |
appliance.interfaces.any.network_type | Built-in catch-all network type token | any |
appliance.interfaces.<name>.network_type | Detected or classified network type for a specific interface. Common values include internet, internal, dmz, external, and restricted. If no value is available for that interface, SRL returns undefined. | internet |
if (this.status equals "enable") {
addPassFinding();
} else if (this.status equals "disable") {
addFailFinding("The setting is disabled.", "Enable the setting.");
} else {
addInfoFinding("The setting could not be classified.", "Review the configuration manually.");
}
foreach ($member in this.member) {
if ($member contains "all") {
addFailFinding("The list contains ALL.", "Replace ALL with explicit members.");
break
}
}
- Supported blocks:
if,else if,elseif,else,foreach. - Supported flow control:
return,break,continue. foreach ($item in some.path)loops visible array items.- Inside a
foreach, the loop variable andthisare both set to the current item.
| Operator | Meaning | Example |
|---|---|---|
equals, ==, is | Case-insensitive equality | this.name equals "root" |
not equals, !=, is not | Case-insensitive inequality | this.status != "enable" |
contains | Substring or list membership match | this.service contains "HTTPS" |
not contains | Inverse contains check | this.srcaddr not contains "all" |
in | Value is in the right-hand list | "HTTPS" in this.service |
exists, not exists | Presence / absence | this.comment exists |
empty, not empty | Empty / non-empty | this.member not empty |
matches_regex, not matches_regex | Regular expression test | this.name matches_regex "^port[0-9]+$" |
>, <, >=, <= | Numeric comparison | number(this.timeout) > 15 |
and, && | Boolean AND | a and b |
or, || | Boolean OR | a or b |
not, ! | Boolean NOT | not (this.status equals "enable") |
+ | String concatenation or numeric addition | "User: " + this.name |
Text and conversion helpers
| Function | Purpose | Example |
|---|---|---|
lower(value) | Lowercase text | lower(this.status) |
upper(value) | Uppercase text | upper(this.name) |
trim(value) | Trim surrounding whitespace | trim(this.comment) |
string(value) | Convert to string | string(this) |
number(value) | Convert to number | number(this.timeout) |
bool(value) | Convert to boolean | bool(this.enabled) |
replace(pattern, replacement, subject) | Literal or regex replacement | replace(" ", "-", this.name) |
split(value, delimiter) | Split text to a list | split("a,b,c", ",") |
join(list, delimiter) | Join a list into text | join(this.member, ", ") |
Presence and counting
| Function | Purpose | Example |
|---|---|---|
count(value) | Count visible array items | count(this.member) |
len(value) | String length or array count | len(this.name) |
exists(value) | Check if a value exists | exists(this.comment) |
not_exists(value) | Inverse exists check | not_exists(this.comment) |
empty(value) | Check for empty/missing | empty(this.member) |
not_empty(value) | Check for non-empty | not_empty(this.member) |
is_true(value) | Truthy conversion test | is_true(this.enabled) |
is_false(value) | Falsy conversion test | is_false(this.enabled) |
Numeric helpers
| Function | Purpose | Example |
|---|---|---|
number_gt(a, b) | Greater than | number_gt(this.timeout, 15) |
number_gte(a, b) | Greater than or equal | number_gte(this.timeout, 15) |
number_lt(a, b) | Less than | number_lt(this.timeout, 15) |
number_lte(a, b) | Less than or equal | number_lte(this.timeout, 15) |
Certificate helpers
| Function | Purpose | Example |
|---|---|---|
cert_days_remaining(certificateBase64) | Returns the number of whole days remaining before a certificate expires. Returns empty/null when the certificate cannot be parsed. | cert_days_remaining(this.certificate) |
Collection helpers
| Function | Purpose | Example |
|---|---|---|
any(list, ...values) | True if any candidate is present | any(this.service, "HTTP", "TELNET") |
all(list, ...values) | True if all candidates are present | all(this.service, "HTTP", "HTTPS") |
Metadata helpers
| Function | Purpose | Example |
|---|---|---|
name(value) | Entry name metadata | name(this) |
parent(value) | Parent config node | parent(this) |
path(value) | Full config path | path(this) |
Network and address helpers
| Function | Purpose | Example |
|---|---|---|
is_ip(value) | Validate IP text | is_ip("192.0.2.1") |
is_cidr(value) | Validate CIDR text | is_cidr("10.0.0.0/24") |
cidr_contains(cidr, ip) | True if the CIDR contains the IP | cidr_contains("10.0.0.0/24", "10.0.0.10") |
network_includes(container, subject) | True when every subject IP, CIDR, or range is fully included inside the container IP, CIDR, or range. Accepts single values or arrays and supports IPv4 and IPv6. | network_includes(getAllIpAddresses($dos_policy.dstaddr), $dstip) |
port_includes(containerPorts, subjectPorts) | True when every subject port or port range is fully included inside the container ports. Supports single ports, ranges, arrays, and special values such as ALL. | port_includes(getAllServicePorts($dos_policy.service), getAllServicePorts(this.service)) |
dos_policy_covers(dosPolicies, interfaceName, destinationRefs, destinationPorts) | True when the enabled DoS policies for the given interface fully cover every referenced destination IP, CIDR, or range and all supplied destination ports. Works with either config.firewall.dos-policy or config.firewall.dos-policy6. | dos_policy_covers(config.firewall.dos-policy, $srcintf, $policy.dstaddr, getAllServicePorts($policy.service)) |
getAddressType(value) | Resolve an address object type | getAddressType(this.srcaddr) |
getAllIpAddressNames(value) | Expand referenced IP address object names | getAllIpAddressNames(this.srcaddr) |
getAllIpAddresses(value) | Expand referenced IP address values | getAllIpAddresses(this.srcaddr) |
getAllAddressFqdnNames(value) | Expand referenced FQDN object names | getAllAddressFqdnNames(this.dstaddr) |
getAllAddressFqdns(value) | Expand referenced FQDN values | getAllAddressFqdns(this.dstaddr) |
getAllAddressGeographyNames(value) | Expand geography object names | getAllAddressGeographyNames(this.dstaddr) |
getAllAddressDeviceNames(value) | Expand device object names | getAllAddressDeviceNames(this.dstaddr) |
getAllAddressDevices(value) | Expand device object values | getAllAddressDevices(this.dstaddr) |
getAllServicePortNames(value) | Expand referenced service object names | getAllServicePortNames(this.service) |
getAllServicePorts(value) | Expand all referenced service ports | getAllServicePorts(this.service) |
getAllServiceTcpPorts(value) | Expand referenced TCP ports | getAllServiceTcpPorts(this.service) |
getAllServiceUdpPorts(value) | Expand referenced UDP ports | getAllServiceUdpPorts(this.service) |
getAllServiceSctpPorts(value) | Expand referenced SCTP ports | getAllServiceSctpPorts(this.service) |
addPassFinding();
addFailFinding(details, remediation, remediation_time, title_override, scope_override, section_override);
addInfoFinding(details, remediation, remediation_time, title_override, scope_override, section_override);
addPassFinding()does not accept any parameters.detailsis required for fail and info findings.remediationis required for fail findings and optional for info findings.remediation_timeis optional for fail and info findings.- If
remediation_timeis omitted, left blank, or set tonull, runtime defaults it to30. title_overrideis optional. If omitted, the rule name is used.scope_overrideis optional.section_overrideis optional.
Examples
addPassFinding();
addFailFinding(
"Administrative access is exposed to unrestricted sources.",
"Restrict this policy to trusted management networks."
);
addFailFinding(
"Password complexity is disabled.",
"Enable password complexity requirements.",
,
"Weak password policy"
);
addInfoFinding(
"Traffic logging is enabled for this policy.",
,
45,
"Logging enabled"
);
addFailFinding(
"The global admin profile allows insecure services.",
"Restrict the allowed administrative services.",
45,
"Global admin exposure",
"global",
"config.system.admin"
);
, , and null. For example,
addFailFinding("...", "...", , "Title"),
addFailFinding("...", "...", null, "Title"), and
addInfoFinding("...", , 15, "Title").
1. Check a single section value
if (number(this.admintimeout) <= 15 and number(this.admintimeout) > 0) {
addPassFinding();
} else {
addFailFinding(
"Administrative timeout is greater than 15 minutes or not set correctly.",
"Set admintimeout to 15 minutes or less."
);
}
2. Loop through section entries
foreach ($admin in this) {
if ($admin.status equals "disable") {
addInfoFinding(
"An administrator entry is disabled: " + name($admin),
"Delete administrators from the system, if they are no longer required.",
10,
"Disabled administrator"
);
}
}
3. Check certificate expiry
$days = cert_days_remaining(this.certificate)
if (not_exists($days)) {
addInfoFinding(
"Certificate expiry could not be checked.",
"Review the certificate manually and confirm it is valid and correctly imported.",
15
);
} else if ($days <= 0) {
addFailFinding(
"Certificate has expired.",
"Renew or replace the expired certificate.",
30
);
} else if ($days <= 30) {
addFailFinding(
"Certificate expires soon.",
"Renew or replace the certificate before it expires.",
30
);
}
4. Match public policy destinations and ports to DoS policy coverage
foreach ($srcintf in this.srcintf) {
if (not any(lower(appliance.interfaces.$srcintf.network_type), "any", "internet")) {
continue;
}
if (dos_policy_covers(config.firewall.dos-policy, $srcintf, this.dstaddr, getAllServicePorts(this.service))) {
addPassFinding();
} else {
addFailFinding(
"No enabled DoS policy on " + $srcintf + " fully covers " + getAllIpAddresses(this.dstaddr) + " for ports " + getAllServicePorts(this.service) + ".",
"Create or expand a DoS policy so its dstaddr and service fully cover the exposed destination and ports."
);
}
}
5. Normalize text before comparison
$normalized = lower(trim(this.status));
if ($normalized equals "enable") {
addPassFinding();
} else {
addFailFinding(
"The feature is not enabled.",
"Enable the feature."
);
}
4. Work with service object expansion
$tcpPorts = getAllServiceTcpPorts(this.service);
if (any($tcpPorts, "23", "2323")) {
addFailFinding(
"The policy permits Telnet-related TCP ports.",
"Remove Telnet and use SSH instead.",
20,
"Insecure management service"
);
}
5. Use appliance metadata in Rule Test or full audits
addInfoFinding(
"Running on " + appliance.vendor + " " + appliance.type + " " + appliance.os,
,
5,
"Appliance context"
);
6. Use increment and decrement operators
$i = 1;
$before = $i++;
$after = ++$i;
$after--;
$after -= 2;
addInfoFinding(
"before=" + $before + ", i=" + $i + ", after=" + $after,
,
0,
"Counter example"
);
$var = exprassigns a variable.$var += exprappends text or adds numbers.$var -= exprsubtracts a numeric value.$var++,++$var,$var--, and--$varincrement or decrement writable numeric variables.if (...) { ... }supportselse ifandelse.foreach ($item in path) { ... }loops array-like nodes.thisis the current scoped object.configis the normalized configuration root.applianceexposes runtime metadata such as vendor, OS version, scope, VDOMs, and interface network types.return,break, andcontinueare supported.
string(array)renders config-like text for config nodes.number(value)strips non-numeric characters before conversion.bool(value)treatstrue,1,yes,on, andenabledas true.- Equality and contains comparisons are case-insensitive.
len(value)returns string length for text and item count for arrays.
matches_regextreats the right side as a regex pattern body.replace()supports both literal replacement and regex replacement when the pattern uses a proper regex delimiter, for example/pattern/i.
- Pass findings must use
addPassFinding()with no arguments. - Fail findings require non-empty
detailsandremediation. - Info findings require non-empty
details.remediationis optional. remediation_timemust be numeric, blank, ornull.- Unterminated strings, unclosed blocks, and unsupported statements are compile errors.
- Runtime errors are surfaced in Test Rule and audit output.
- Write complete finding text inside the finding function itself.
- Prefer explicit titles for reusable or looped findings.
- Use helper functions like
lower()andtrim()before comparing vendor text. - Increment and decrement operators only work on writable numeric SRL variables, not on read-only inputs like
this,config, orappliance. - Use Test Rule with a real config before saving production rule changes.
- Keep remediation text action-oriented and specific to the detected issue.