Skip to content
Last updated

Writing BNDRY Policies

BNDRY uses CEL (Common Expression Language) to power our entity risk ratings and policy engine.

CEL is a simple expression language that can be used to evaluate expressions in a safe and efficient way.

Literals

Comment// (line comments only)
Booleantrue, false
Integer42, -17, 0x2A (hex)
Unsigned42u, 0x2Au
Float3.14, -0.5, 6.022e23
String"foo", 'bar', """multiline"""
Bytesb"data", b'data'
List[1, 2, 3]
Map{"a": 1, "b": 2}
Nullnull

Strings

Strings can be enclosed in single quotes or double quotes. For multiline strings, use triple quotes (""" or ''').

"Hello\nWorld"
'Single quoted'
"""Multiline
string"""

Raw strings (prefixed with r) don't interpret escape sequences:

r"\n\t"  // literal backslash-n-backslash-t

Bytes

Bytes literals are prefixed with b:

b"hello"
b'\xF0\x9F\xA4\xAA'  // UTF-8 encoding of 🤪

Operators

Arithmetic+, -, *, /, % (modulus)
Comparison==, !=, <, >, <=, >=
Logical! (not), && (and), || (or)
Conditional? : (ternary)
Membershipin
Indexing[], .

Membership Operators

The in operator checks if an item exists in a list or a key exists in a map.

2 in [1, 2, 3]              // true
"name" in {"name": "John"}  // true

Field Selection

Access fields of messages or map values with . or []:

user.name
user["name"]

Access list elements with []:

items[0]     // first element
items[-1]    // last element

Conditional Operator

The ternary operator evaluates a condition and returns one of two values:

score >= 60 ? "pass" : "fail"

Logical Operators

The && and || operators provide commutative evaluation (may evaluate in any order):

user.admin && user.active
error || true  // returns true, ignoring error

For traditional short-circuit evaluation, use the ternary operator:

// Short-circuit AND
condition ? second_condition : false

// Short-circuit OR  
condition ? true : second_condition

String Functions

contains(str, substring)

Checks if a string contains a substring.

"hello world".contains("world")  // true

startsWith(str, prefix)

Checks if a string starts with a prefix.

"hello world".startsWith("hello")  // true

endsWith(str, suffix)

Checks if a string ends with a suffix.

"hello world".endsWith("world")  // true

matches(str, regex)

Tests if a string matches a regular expression pattern (RE2 syntax).

"foobar".matches("foo.*")  // true
matches("test123", "[a-z]+[0-9]+")  // true

size(str)

Returns the length of a string in Unicode code points.

"hello".size()     // 5
size("world!")     // 6

List Functions

size(list)

Returns the number of elements in a list.

[1, 2, 3].size()    // 3
size([])            // 0

in operator

Checks if a value exists in a list.

2 in [1, 2, 3]      // true
5 in [1, 2, 3]      // false

Map Functions

size(map)

Returns the number of entries in a map.

{"a": 1, "b": 2}.size()  // 2

in operator

Checks if a key exists in a map.

"name" in {"name": "John", "age": 30}  // true

Comprehension Macros

has(field)

Tests whether a field is set in a message or whether a key exists in a map.

has(user.address)
has(map.key_name)

all(var, predicate)

Tests if all elements satisfy a condition.

[1, 2, 3].all(x, x > 0)              // true
{"a": 1, "b": 2}.all(k, k != 'c')    // true

exists(var, predicate)

Tests if any element satisfies a condition.

[1, 2, 3].exists(x, x > 2)           // true
[].exists(x, x > 0)                   // false

exists_one(var, predicate)

Tests if exactly one element satisfies a condition.

[1, 2, 3].exists_one(x, x == 2)      // true
[2, 2, 3].exists_one(x, x == 2)      // false

map(var, transform)

Transforms each element using an expression.

[1, 2, 3].map(x, x * 2)              // [2, 4, 6]
["a", "b"].map(s, s.upperAscii())    // ["A", "B"]

With filter predicate:

[1, 2, 3, 4].map(x, x % 2 == 0, x * 2)  // [4, 8]

filter(var, predicate)

Returns elements that satisfy a condition.

[1, 2, 3, 4].filter(x, x % 2 == 0)   // [2, 4]
{"a": 1, "b": 2}.filter(k, k == "a")  // ["a"]

Timestamp and Duration Functions

timestamp(string)

Creates a timestamp from an RFC3339 string.

timestamp("2023-08-26T12:39:00-07:00")

duration(string)

Creates a duration from a string (supports h, m, s, ms, us, ns).

duration("1h30m")
duration("500ms")
duration("-1.5h")

Timestamp Methods

Get components of a timestamp:

timestamp("2023-12-25T12:30:45Z").getFullYear()     // 2023
timestamp("2023-12-25T12:30:45Z").getMonth()        // 11 (December, 0-indexed)
timestamp("2023-12-25T12:30:45Z").getDate()         // 25
timestamp("2023-12-25T12:30:45Z").getHours()        // 12
timestamp("2023-12-25T12:30:45Z").getMinutes()      // 30
timestamp("2023-12-25T12:30:45Z").getSeconds()      // 45
timestamp("2023-12-25T12:30:45Z").getDayOfWeek()    // 1 (Monday)
timestamp("2023-12-25T12:30:45Z").getDayOfYear()    // 358

With timezone:

timestamp("2023-12-25T00:00:00Z").getDate("America/Los_Angeles")  // 24

Duration Methods

Convert or extract from durations:

duration("1h30m").getHours()         // 1
duration("1h30m").getMinutes()       // 90
duration("1.234s").getMilliseconds() // 234

Arithmetic with Time

Add or subtract time values:

timestamp("2023-01-01T00:00:00Z") + duration("24h")
timestamp("2023-01-10T12:00:00Z") - timestamp("2023-01-10T00:00:00Z")
duration("1h") + duration("30m")

Type Conversion Functions

int(value)

Converts to a signed integer.

int("123")                // 123
int(3.14)                 // 3
int(timestamp("2023-08-26T12:00:00Z"))  // Unix epoch seconds

uint(value)

Converts to an unsigned integer.

uint("123")               // 123u
uint(3.14)                // 3u

double(value)

Converts to a double.

double(10)                // 10.0
double("3.14")            // 3.14

string(value)

Converts to a string.

string(123)               // "123"
string(true)              // "true"
string(b'hello')          // "hello"
string(duration("1m1ms")) // "60.001s"

bytes(value)

Converts to bytes.

bytes("hello")            // b'hello'
bytes("🤪")              // b'\xF0\x9F\xA4\xAA'

bool(value)

Converts to boolean.

bool("true")              // true
bool("FALSE")             // false

type(value)

Returns the type of a value.

type(123)                 // int
type("hello")             // string
type([1, 2])              // list

dyn(value)

Marks a value as dynamically typed (for type-checking purposes).

dyn([1, 3.14, "foo"])     // list(dyn)

Bytes Functions

size(bytes)

Returns the number of bytes.

b'hello'.size()           // 5
size(b'\xF0\x9F\xA4\xAA') // 4

Common Patterns

Safe Navigation

Check field existence before accessing:

has(user.profile) && user.profile.name == "John"

List Processing

// Filter then transform
orders.filter(o, o.total > 100).map(o, o.id)

// Count matching items
users.filter(u, u.age >= 18).size()

// Check all items
items.all(item, item.quantity > 0)

Map Operations

// Check if key exists
has(config.timeout)

// Get value with default
has(settings.retries) ? settings.retries : 3

// Filter map to list of keys
{"a": 10, "b": 5, "c": 20}.filter(k, map[k] > 10)  // ["c"]

Time-based Logic

// Check if timestamp is in range
now > timestamp("2023-01-01T00:00:00Z") && 
now < timestamp("2024-01-01T00:00:00Z")

// Calculate age
duration(now - user.birthdate).getHours() / 24 / 365

String Validation

// Email-like pattern
user.email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")

// Required fields
user.name.size() > 0 && user.email.size() > 0

Error Handling

CEL expressions can produce errors at runtime. Common errors include:

  • no_matching_overload: Function called with wrong argument types
  • no_such_field: Accessing a non-existent field
  • Division by zero
  • Type conversion failures
  • Overflow errors

Use logical operators to handle potential errors:

// This won't error even if division by zero occurs on left side
x / 0 || true  // returns true

// Safe field access
has(obj.field) && obj.field > 10

Best Practices

  1. Use has() before accessing optional fields

    has(user.address) && user.address.city == "NYC"
  2. Prefer explicit type conversions

    int(userInput) + 5  // instead of relying on implicit conversion
  3. Use macros for list/map operations

    items.all(x, x.valid)  // instead of manual iteration
  4. Chain operations clearly

    users
      .filter(u, u.active)
      .map(u, u.email)
      .exists(e, e.endsWith("@company.com"))
  5. Handle null values explicitly

    user.name != null && user.name.size() > 0