CEL (Common Expression Language)¶
While you can use direct field matchers during rule building, more advanced scenarios might require using CEL expressions.
Common Expression Language (CEL) has been designed by Google to be a safe, expressive, and efficient way to express conditional logic. This page contains a list of CEL fields and functions that are available for you to use - some of them are default features of the CEL, some of them are specific to rescaled WAF.
Upon startup, all CEL expressions are being compiled. If a compilation error occurs, the startup will fail. Pre-compiling expressions during startup not only allow to catch errors early, but also to avoid unnecessary runtime overhead.
Using Expressions in Rules¶
The expression field during rule building accepts three forms.
Single Expression¶
A plain string is evaluated as-is:
Multiple Expressions - All Must Match (AND)¶
When using the all key, every expression must evaluate to true:
- name: deny-post-to-upload
action: DENY
expression:
all:
- 'method == "POST"'
- 'path == "/upload"'
Internally the expressions are joined with &&:
( method == "POST" ) && ( path == "/upload" )
This forms a logical AND of all provided expressions.
Multiple Expressions - Any Must Match (OR)¶
When using the any key, at least one provided expression must evaluate to true:
- name: challenge-suspicious-countries
action: CHALLENGE
expression:
any:
- 'geoCountry == "CN"'
- 'geoCountry == "RU"'
- 'geoCountry == "KP"'
Internally the expressions are joined with ||:
( geoCountry == "CN" ) || ( geoCountry == "RU" ) || ( geoCountry == "KP" )
This forms a logical OR of all provided expressions.
You cannot combine all and any in the same expression field. If you need both, nest the logic in a single expression string using && and ||.
Variables¶
Core Request Variables¶
Always available on every request.
| Variable | Type | Description |
|---|---|---|
remoteAddress |
string |
Client IP address (from the configured client IP header). |
host |
string |
HTTP Host header value. |
method |
string |
HTTP method (GET, POST, PUT, etc.). |
path |
string |
Request path without the query string. |
userAgent |
string |
User-Agent header value. Empty string when absent. |
contentLength |
int |
Content-Length header parsed as integer. 0 when absent. |
headers |
map(string, string) |
All HTTP headers, normalized to lowercase names |
query |
map(string, string) |
Parsed query parameters. Multi-valued parameters are comma-joined into a single string. |
Examples¶
Working with headers¶
As string operations in CEL are case-sensitive, all header names are normalized to lowercase. When writing CEL expressions against headers, make sure that you use lowercase names throughout all your rules. The content of a header will not be touched or modified in any way.
// Check if a header exists.
"authorization" in headers
// Match a header value.
headers["content-type"] == "application/json"
// Partial match.
headers["accept"].contains("text/html")
// Number of headers.
headers.size() > 0
Working with query¶
// Check if a parameter exists.
"token" in query
// Match a parameter value.
query["format"] == "json"
// Partial match on a comma-joined multi-value parameter.
// Given ?tag=foo&tag=bar, query["tag"] is "foo,bar".
query["tag"].contains("foo")
GeoIP Variables¶
Available when both features.geoip.enabled and
features.geoip.policy_usage.enabled are true. Referencing these variables
when GeoIP is disabled causes a compilation error at startup.
| Variable | Type | Description | Example value |
|---|---|---|---|
geoCountry |
string |
ISO 3166-1 alpha-2 country code. | "US", "DE" |
geoCountryName |
string |
English country name. | "United States", "Germany" |
geoCity |
string |
City name in English. | "San Francisco" |
geoContinent |
string |
Two-letter continent code. | "NA", "EU", "AS" |
Examples¶
ASN Variables¶
Available when both features.asn.enabled and
features.asn.policy_usage.enabled are true. Referencing these variables when
ASN is disabled causes a compilation error at startup.
| Variable | Type | Description | Example value |
|---|---|---|---|
asnNumber |
int |
Autonomous System Number. | 15169 (Google) |
asnOrg |
string |
Registered organization name. | "GOOGLE" |
Examples¶
Custom Functions¶
Aside from the standard CEL functionality, rescaled WAF supports a number of custom functions that are useful for implementing complex logic.
missingHeader(headers, name) -> bool¶
Returns true if the named header is absent from the request.
| Parameter | Type | Description |
|---|---|---|
headers |
map(string, string) |
The headers variable. |
name |
string |
Header name to check (case-sensitive, use lowercase). |
// Require Accept header on API paths (combine with all mode).
// all:
// - 'path.startsWith("/api/")'
// - 'missingHeader(headers, "accept")'
randInt(n) -> int¶
Returns a random integer in the range [0, n). Useful for probabilistic
sampling or gradual rollouts.
| Parameter | Type | Description |
|---|---|---|
n |
int |
Upper bound (exclusive). Must be > 0. |
regexSafe(s) -> string¶
Escapes regex metacharacters in s so it can be safely interpolated into a
regex pattern. Escapes: \ . : * ? - [ ] ( ) + { } | ^ $
| Parameter | Type | Description |
|---|---|---|
s |
string |
String to escape. |
ip_list(name) -> IPList¶
Returns an IP list container for use with the in operator. The named list must
be defined in the ip_lists configuration section. Available only when at least
one IP list is configured.
| Parameter | Type | Description |
|---|---|---|
name |
string |
Name of a configured IP list. |
The returned container supports CIDR range matching — if the list contains
10.0.0.0/8, any address in that range matches.
// Combine with other conditions.
remoteAddress in ip_list("rfc1918") && path.startsWith("/internal/")
Referencing a non-existent list name causes a runtime error. Always make sure the list name matches a key in ip_lists.
String Extension Functions¶
The CEL environment includes the Google CEL Strings extension,
providing these methods on string values. All indices are zero-based.
charAt(index) -> string¶
Returns the character at the given position.
indexOf(substring) -> int / indexOf(substring, offset) -> int¶
Returns the index of the first occurrence, or -1 if not found. The optional
offset starts the search from that position.
lastIndexOf(substring) -> int / lastIndexOf(substring, offset) -> int¶
Returns the index of the last occurrence, or -1 if not found.
lowerAscii() -> string¶
Converts ASCII characters to lowercase. Prefer this for case-insensitive ASCII comparisons.
upperAscii() -> string¶
Converts ASCII characters to uppercase.
replace(target, replacement) -> string / replace(target, replacement, count) -> string¶
Replaces occurrences of target with replacement. With count, limits the
number of replacements.
split(separator) -> list(string) / split(separator, limit) -> list(string)¶
Splits the string by separator. With limit, stops after that many parts.
substring(start) -> string / substring(start, end) -> string¶
Returns a substring from start (inclusive) to end (exclusive) or end of
string.
trim() -> string¶
Removes leading and trailing whitespace.
reverse() -> string¶
Returns the string with characters in reverse order.
join() -> string / join(separator) -> string¶
Joins a list(string) into a single string.
format(args) -> string¶
Printf-style formatting. Supports %s, %d, %f, %e, %b, %x, %o.
quote() -> string¶
Escapes special characters to make the string safe to print.
Built-in String Methods (CEL Standard)¶
These are part of CEL itself, not the extension:
// Substring containment.
userAgent.contains("bot")
// Prefix check.
path.startsWith("/api/")
// Suffix check.
path.endsWith(".json")
// RE2 regex match.
userAgent.matches("(?i)bot|crawl|spider")
// String length.
userAgent.size() > 0
// Or equivalently:
size(userAgent) > 0
Operators¶
Comparison¶
| Operator | Description | Example |
|---|---|---|
== |
Equal | method == "GET" |
!= |
Not equal | path != "/" |
< |
Less than | contentLength < 1024 |
<= |
Less than or equal | asnNumber <= 100 |
> |
Greater than | contentLength > 0 |
>= |
Greater than or equal | headers.size() >= 5 |
in |
Membership | "accept" in headers |
Logical¶
| Operator | Description | Example |
|---|---|---|
&& |
AND (short-circuit) | method == "POST" && path == "/login" |
\|\| |
OR (short-circuit) | method == "PUT" \|\| method == "DELETE" |
! |
NOT | !missingHeader(headers, "accept") |
Arithmetic¶
| Operator | Description | Example |
|---|---|---|
+ |
Add / string concat | contentLength + 0 |
- |
Subtract | asnNumber - 1 |
* |
Multiply | randInt(10) * 2 |
/ |
Divide | contentLength / 1024 |
% |
Modulo | randInt(100) % 10 |
Ternary¶
// Not directly useful for boolean rule matching, but valid in sub-expressions:
(method == "POST" ? contentLength : 0) > 1024
Common Patterns¶
Block Requests Missing Common Headers¶
- name: deny-missing-headers
action: DENY
expression:
all:
- 'missingHeader(headers, "user-agent")'
- 'missingHeader(headers, "accept")'
Challenge Known Scanner User-Agents¶
- name: challenge-scanners
action: CHALLENGE
expression: 'userAgent.matches("(?i)(sqlmap|nikto|nmap|masscan)")'
challenge:
algorithm: pow
difficulty: 8
Weight Suspicious Requests (Auto-Challenge)¶
- name: weigh-no-accept
action: WEIGH
weight: 15
expression: 'missingHeader(headers, "accept")'
- name: weigh-curl
action: WEIGH
weight: 20
expression: 'userAgent.lowerAscii().contains("curl")'
- name: weigh-empty-ua
action: WEIGH
weight: 25
expression: 'userAgent == ""'
HIT Score Sensitive File Access Attempts¶
- name: hit-dotfile-probe
action: HIT
amount: 5
expression: 'path.matches("\\.(env|git|bak|sql|config)$")'
- name: hit-admin-probe
action: HIT
amount: 3
expression: 'path.matches("(?i)/(admin|wp-admin|phpmyadmin)")'
Log Requests from Specific Networks¶
- name: log-datacenter-traffic
action: LOG
expression:
any:
- 'asnOrg.contains("AMAZON")'
- 'asnOrg.contains("GOOGLE")'
- 'asnOrg.contains("MICROSOFT")'
Allow Health Checks from Internal Networks¶
- name: allow-internal-health
action: ALLOW
expression:
all:
- 'remoteAddress in ip_list("rfc1918")'
- 'path == "/healthz"'
Probabilistic Sampling¶
- name: challenge-sample
action: CHALLENGE
expression: 'randInt(100) < 5'
challenge:
algorithm: metarefresh
difficulty: 1
Complex Multi-Condition Rule¶
When you need both AND and OR logic, write a single expression:
- name: deny-suspicious-api-post
action: DENY
expression: >-
method == "POST"
&& path.startsWith("/api/")
&& (missingHeader(headers, "content-type")
|| missingHeader(headers, "authorization"))
Geo-Fencing with ASN Exception¶
- name: allow-known-cdn
action: ALLOW
expression: 'asnNumber == 13335'
- name: deny-geo-blocked
action: DENY
expression:
any:
- 'geoCountry == "KP"'
- 'geoCountry == "IR"'
Error Handling¶
Compilation Errors (Startup)¶
CEL expressions are compiled and type-checked when rescaled-waf starts. Errors prevent startup entirely, with a clear message:
policy: compilation failed:
rule[3] "my-rule": CEL compilation failed: undeclared reference to 'geoCountry'
Common causes:
- Referencing
geoCountry/geoCity/geoContinent/geoCountryNamewhen GeoIP policy usage is disabled. - Referencing
asnNumber/asnOrgwhen ASN policy usage is disabled. - Type mismatch (e.g.
method == 123— comparing string to int). - Syntax errors in the expression string.
- Referencing an IP list name that does not exist in
ip_lists.
Runtime Behavior¶
- Missing map keys: Accessing a header or query parameter that does not exist
(e.g.
headers["x-missing"]) produces a CEL error value, which causes the rule to be skipped. Use"key" in headersormissingHeader()to safely check first. - Invalid IP in
ip_list(): IfremoteAddressis not a valid IP address, theincheck returnsfalse(no error). - Invalid regex in
matches(): Malformed regex patterns cause the expression evaluation to fail; the rule is skipped with a warning log. - Scope match errors: Any expression evaluation error is logged at
warnlevel and the rule is skipped — it does not block the request or crash the server.