Secdit Rule Language uses brace-based blocks and semicolon-terminated statements.
if (this.action is "accept") {
addFailFinding("Finding title");
}
foreach ($policy in config.firewall.policy) {
if (this.logtraffic is "disable") {
addFailFinding("Policy has logging disabled");
}
}
- Use
if (...) { ... },else if (...) { ... }, andelse { ... }. - Use
foreach ($item in config.some.path) { ... }for loopable config sections. - If no Configuration Section is set, the rule runs once without a loop and
thisis not available. - Terminate statements with
;. - Use
// single-lineor/* multi-line */comments. - Tabs and repeated spaces are ignored outside quoted strings.
- Strings can use
"double quotes",'single quotes', or`backticks`. return;,break;, andcontinue;are supported.- Condition operators include
is,==,!=,contains,exists,&&,||, and!.
Variables are rule-scoped and must start with $. Declare them with let.
let $policyName = this.name;
let $policyPath = path(this);
addFailFinding(
"Policy: $policyName",
"Issue found in $policyPath"
);
- Variables can be interpolated in strings, for example
"Policy $policyName is risky". - Variables can be used inside dotted lookups such as
config.firewall.policy.$policyName.srcaddr. - Use
$variable += ...;to append to an existing variable. nullis not allowed in finding arguments.
Use explicit finding functions. A rule can emit multiple findings.
Signatures
addFinding(result, title, details, remediation, remediation_time, context, section); // legacy compatibility
addPassFinding(title, details, remediation, remediation_time, context, section);
addFailFinding(title, details, remediation, remediation_time, context, section);
addInfoFinding(title, details, remediation, remediation_time, context, section);
addFinding()is supported for legacy rules, but new rules should useaddPassFinding()oraddFailFinding().title,details,remediation,remediation_time,context, andsectionare optional.- If an argument is omitted, Secdit Rule Language uses the rule default where one exists.
- If an argument is explicitly
"", Secdit Rule Language treats that as an empty override, not a default. remediation_timeis in minutes.- If
sectionis omitted inside a loop, Secdit Rule Language uses the current concrete section, for exampleconfig.firewall.policy.17. If the rule has no Configuration Section, the finding is not tied to a loop item.
Examples
addFailFinding("Finding title");
addFailFinding(
"Administrative policy: $policyName",
"Administrative service is exposed to unrestricted sources.",
"Restrict this policy to trusted management networks.",
45,
"root",
path(this)
);
addInfoFinding(
"Administrative policy reviewed: $policyName",
"Trusted source restrictions are present."
);
Sparse arguments are allowed. Example: addFailFinding("Finding details", "Do this fix", 30, "global");
These functions resolve FortiGate service objects and nested service groups from normalized config JSON.
getAllServicePortNames(x): returns expanded service object names, including nested service groups.getAllServicePorts(x): returns normalized concrete port values across all supported protocols.getAllServiceTCPPorts(x): returns only TCP ports.getAllServiceUDPPorts(x): returns only UDP ports.getAllServiceSCTPPorts(x): returns only SCTP ports.
if (getAllServicePortNames(this.service) contains("HTTP", "HTTPS", "RDP")) {
addFailFinding("Sensitive service object is permitted");
}
if (getAllServiceTCPPorts(this.service) contains("3389", "22", "443")) {
addFailFinding("Administrative TCP port exposed");
}
if (getAllServicePorts(this.service) contains("80", "443", "3389")) {
addInfoFinding("Policy contains reviewed service ports");
}
Aliases such as getAllPorts(), getAllTCPPorts(), getAllUDPPorts(), getAllSCTPPorts(), and getAllPortsNames() are still available, but the canonical names are the getAllService... forms.
These functions resolve address objects and nested address groups from normalized config JSON. IP masks are normalized to CIDR, and IP ranges are normalized to start-end.
getAddressType(x): returnsaddress,group,vip,vipgrp,fqdn,geography,device, orunknown.getAllIPAddressNames(x): returns expanded IP/range address object names only.getAllIPAddresses(x): returns normalized IP/CIDR/range values only.getAllAddressFQDNNames(x)andgetAllAddressFQDNs(x).getAllAddressGeographyNames(x)andgetAllAddressGeographies(x).getAllAddressDeviceNames(x)andgetAllAddressDevices(x).
if (getAllIPAddressNames(this.srcaddr) contains("any", "all") || getAllIPAddresses(this.srcaddr) contains("0.0.0.0/0", "::/0")) {
addFailFinding("Policy allows unrestricted sources");
}
if (getAddressType("vip_webserver_via_wan-vdom") is "vip") {
addInfoFinding("VIP object referenced by policy");
}
if (getAllAddressFQDNs(this.dstaddr) contains("example.com")) {
addInfoFinding("FQDN destination object used");
}
count(value),len(value)lower(value),upper(value),trim(value)join(list, separator),split(text, separator)replace(pattern, replacement, subject): literal replace by default, or regex replace whenpatternis written as a delimited regex such as/\r?\n/or#admin#i.exists(value),empty(value)string(value),number(value),bool(value)is_ip(value),is_cidr(value),cidr_contains(cidr, ip)any(list, ...needles),all(list, ...needles)name(value),parent(value),path(value)
let $srcText = join(getAllIPAddresses(this.srcaddr), ", ");
let $htmlText = replace("/\\r?\\n/", "<br>", this);
if (count(getAllIPAddresses(this.srcaddr)) > 10) {
addInfoFinding(
"Policy uses many source addresses",
"Resolved sources: $srcText"
);
}
config.system.globalconfig.system.interfaceconfig.firewall.policyconfig.firewall.address,config.firewall.addrgrp,config.firewall.vip,config.firewall.vipgrpconfig.firewall.service.custom,config.firewall.service.groupappliance.vendor,appliance.type,appliance.os,appliance.scope,appliance.vdomsappliance.interfaces.port1.network_typewhere the value is one ofinternet,external,dmz,internal, orrestrictedthisinside a loop or section-scoped rule
Example 1: unrestricted administrative access
let $policyName = this.name;
if (this.action is "accept" && (getAllIPAddressNames(this.srcaddr) contains("any", "all") || getAllIPAddresses(this.srcaddr) contains("0.0.0.0/0", "::/0")) && getAllServicePortNames(this.service) contains("SSH", "RDP", "HTTPS", "WINBOX", "VNC")) {
addFailFinding(
"Administrative policy: $policyName",
"Administrative services are reachable from unrestricted sources.",
"Restrict this policy to trusted management networks.",
45
);
}
Example 2: multiple findings from one rule
let $policyName = this.name;
if (getAllServiceTCPPorts(this.service) contains("23")) {
addFailFinding(
"Telnet permitted: $policyName",
"Telnet is insecure and should not be exposed.",
"Replace Telnet with SSH.",
30
);
}
if (this.logtraffic is "disable") {
addInfoFinding(
"Logging disabled: $policyName",
"This policy currently disables traffic logging."
);
}
- The rule validator returns the failing line, character, and reason.
- Syntax errors block the rule from being saved.
- Runtime errors fail the rule safely and are surfaced as execution errors in audit output.
- Use the Rule Manager validator and New Audit test runs together before publishing.
- Maximum runtime per rule: 45 seconds cooperative SRL budget.
- Maximum SRL size: 65535 bytes
- Object resolver functions work against normalized imported configuration data, not raw config text.
- Start from a cloned built-in rule where possible.
- Use New Audit to test against real configs before publishing rules.
- Prefer canonical resolver functions like
getAllServicePortNames()andgetAllIPAddresses()instead of checking raw object names directly. - Use parentheses for mixed boolean conditions.
- Omit finding arguments to inherit rule defaults; use
""only when you intentionally want an empty override.
You need a ConfigSentry account to create and manage custom rules.
Sign Up