Knowledge Base

SRL Guide

Secdit Rule Language reference for custom audit rules.

Syntax

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 (...) { ... }, and else { ... }.
  • 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 this is not available.
  • Terminate statements with ;.
  • Use // single-line or /* multi-line */ comments.
  • Tabs and repeated spaces are ignored outside quoted strings.
  • Strings can use "double quotes", 'single quotes', or `backticks`.
  • return;, break;, and continue; are supported.
  • Condition operators include is, ==, !=, contains, exists, &&, ||, and !.
Variables

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.
  • null is not allowed in finding arguments.
Findings

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 use addPassFinding() or addFailFinding().
  • title, details, remediation, remediation_time, context, and section are 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_time is in minutes.
  • If section is omitted inside a loop, Secdit Rule Language uses the current concrete section, for example config.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");

Service Functions

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.

Address Functions

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): returns address, group, vip, vipgrp, fqdn, geography, device, or unknown.
  • getAllIPAddressNames(x): returns expanded IP/range address object names only.
  • getAllIPAddresses(x): returns normalized IP/CIDR/range values only.
  • getAllAddressFQDNNames(x) and getAllAddressFQDNs(x).
  • getAllAddressGeographyNames(x) and getAllAddressGeographies(x).
  • getAllAddressDeviceNames(x) and getAllAddressDevices(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");
}
General Functions
  • 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 when pattern is 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"
    );
}
Useful Paths
  • config.system.global
  • config.system.interface
  • config.firewall.policy
  • config.firewall.address, config.firewall.addrgrp, config.firewall.vip, config.firewall.vipgrp
  • config.firewall.service.custom, config.firewall.service.group
  • appliance.vendor, appliance.type, appliance.os, appliance.scope, appliance.vdoms
  • appliance.interfaces.port1.network_type where the value is one of internet, external, dmz, internal, or restricted
  • this inside a loop or section-scoped rule
Examples

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."
    );
}
Validation And Errors
  • 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.
Limits And Notes
  • 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.
Tips
  • 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() and getAllIPAddresses() 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.
Using the Rule Manager

You need a ConfigSentry account to create and manage custom rules.

Sign Up