Features
| Pricing | Documentation | Contact | Blog
Binary Range Expression (BREX) is a Domain Specific Language (DSL) for extracting sub-ranges from binary data based on position and/or conditions, with support for Type-Length-Value (TLV) searches. While designed for network packet analysis and processing, BREX can be used for security scanning, file identification, and other binary data manipulation tasks.
Note: BREX expressions are used in various Proxylity UDP Gateway destination configurations, including SQS and IoT Data destinations, to extract specific data from UDP packet payloads.
[5] - Returns byte at offset 5
[2, 4] - Returns 4 bytes starting at offset 2
[0, 10] - Returns first 10 bytes
[2:6] - Returns 4 bytes starting at offset 2 up to but not including byte 6
[0:10] - Returns first 10 bytes
[2:] - Return all bytes starting with the byte at offset 2
[:] - Returns all bytes
[] - Returns an empty range
[u8[0] + 1] - Read byte at (value of byte 0) + 1
[u16le[0] * 2, 4] - Read 4 bytes at ((16-bit value at byte 0) * 2)
[u8[0] + 1, 10] | [5] - Read 10 bytes from calculated offset, get byte 5
Expression chaining allows you to pass the result of one operation as input to the next operation, creating powerful data processing pipelines. BREX uses the pipe operator | to chain expressions explicitly.
The pipe operator | takes the result from the left expression and makes it the data context for the right expression:
[10:20] | [5] - Get bytes 10-19, then get byte 5 of that result
[0:100] | [50:60] - Get bytes 0-99, then get bytes 50-59 of that result
[0:100] | [50:60] | [0] - Multi-step: get range, then sub-range, then first byte
Chaining evaluates from left to right, where each operation receives the output of the previous operation:
[20:30] | [2:8] | [1]
Step-by-step evaluation:
[20:30] → produces 10 bytes (original buffer bytes 20-29)
| [2:8] → produces 6 bytes (bytes 2-7 of the 10-byte result)
| [1] → produces 1 byte (byte 1 of the 6-byte result)
Chaining is particularly useful for:
[0:20] | [16:] | u32le[0:] - Get header, skip to timestamp field, read as uint32
Slice search operations allow you to iterate through structured data like TLV (Type-Length-Value) records, finding entries that match specific conditions. By default, slice search returns the entire matching slice. Use chained range operations to extract specific parts.
[20:, ==31] - Find entire TLV slice where type=31 starting at position 20
[20:, [0]=31] - Same as above but explicit (relative addressing)
[0:, ==0x42] - Find entire TLV slice where type=0x42 from start of buffer
Assumes: type at [0], length at [1], both relative to slice
[20:, [0]==31] | [2:] - Find TLV type 31, return value part (skip type+length)
[20:, [0]==31] | [0] - Find TLV type 31, return just the type byte
[20:, [0]==31] | [1] - Find TLV type 31, return just the length byte
[20:, [0]==31] | [0:2] - Find TLV type 31, return type+length
[20:, [0]==31] | [2:] | [0] - First byte of value part
[20:, [0]>10] - Find slice where type > 10
[20:, [0]==31 && [2]<5] - Type=31 AND first value byte < 5
[20:, [0]==31 || [0]==32] - Type 31 OR type 32
[20:, [0] & 0x80 == 0] - Type field bitmask check
[20:, [0]==31 && [@100]>10] - Type=31 AND buffer position 100 > 10
[20:, [0]==31] - Standard: length at [1] (default, relative)
[20:, [1]==31, [0]] - Length-first: length at [0], type at [1]
[20:, [0]==31, [2]] - Type at [0], length at [2]
[20:, [0]==31, u16be[2:]] - Type=31, u16be length at relative position 2
[20:, [0]==31, u32le[4:]] - Type at [0], u32le length at relative position 4
[20:, [0]==31, 8] - 8-byte records, find where first byte = 31
[0:, [2]>100, 12] - 12-byte records, find where 3rd byte > 100
[20:, [0]==0x42, 16] - 16-byte records, find type 0x42
[20:, [0]==31, 8] | [1:7] - Skip first byte, get remaining 6 bytes
The @ prefix provides absolute addressing to the original buffer, while operations without @ are relative to the current slice context:
[0] - Byte 0 of current slice (typically type in TLV)
[1] - Byte 1 of current slice (typically length in TLV)
[@0] - Position 0 in the original buffer
[@100] - Position 100 in the original buffer
Format: [type:1][length:1][value:N]
[20:, [0]==31] - Find entire TLV slice for type 31
[20:, [0]==31] | [2:] - Find type 31, return value part only
[20:, [0]==31] | [1] - Find type 31, return length byte
Format: [type:1][reserved:1][length:2be][value:N]
[0:, [0]==0x42, u16be[2:]] - Find type 0x42 with u16be length at relative [2]
[0:, [0]==0x42, u16be[2:]] | [4:] - Same, but return value part (skip 4-byte header)
[0:, [0]==0x42, u16be[2:]] | [2:4] - Same, but return just the length field
Format: [length:2le][type:1][flags:1][value:N]
[20:, [2]==31, u16le[0:]] - Length at [0], type at [2] (both relative)
[20:, [2]==31, u16le[0:]] | [4:] - Same, return value part (skip 4-byte header)
[20:, [2]==31, u16le[0:]] | [2:4] - Same, return type+flags
Integer interpretations of byte sequences can be specified for indexes, lengths, and condition operands.
u8[0] - Unsigned 8-bit integer at offset 0
i8[0] - Signed 8-bit integer at offset 0
u16le[0:] - Unsigned 16-bit little-endian
u16be[0:] - Unsigned 16-bit big-endian
i16le[0:] - Signed 16-bit little-endian
i16be[0:] - Signed 16-bit big-endian
u32le[0:] - Unsigned 32-bit little-endian
u32be[0:] - Unsigned 32-bit big-endian
i32le[0:] - Signed 32-bit little-endian
i32be[0:] - Signed 32-bit big-endian
u64le[0:] - Unsigned 64-bit little-endian
u64be[0:] - Unsigned 64-bit big-endian
i64le[0:] - Signed 64-bit little-endian
i64be[0:] - Signed 64-bit big-endian
The endianness specified in the type is preserved throughout expression evaluation:
Given data: byte[] { 0x01, 0x02 }
u16be[0:] == 258 - true (0x01 << 8 + 0x02 = 258)
u16le[0:] == 513 - true (0x01 + 0x02 << 8 = 513)
[u8[0]] - Read byte at offset specified by byte 0
[u16le[2:], u8[0]] - Read (byte 0) bytes at offset (16-bit value at 2)
[u8[u8[0]]] - Three-level pointer following
[u8[u16le[0:]]] - Mixed types in pointer chain
[u8[0] + 4] - Read at (byte 0 value) + 4
[u8[0] * 2, u8[1]] - Dynamic offset and length
[u8[0] + u8[1]] - Add two byte values
u16le[0:] - 100 - Subtract constant
u8[0] * 4 - Multiply by constant
u32le[0:] / u16le[4:] - Divide two values
u8[0] % 16 - Modulo operation
All arithmetic operations convert to the largest type involved:
u8[0] + u16le[1] - Result is 16-bit
u16le[0:] + u32be[2:] - Result is 32-bit
u8[0] > 10 ? [1, u8[0]] : [0, 1] - condition ? true_expression : false_expression
u8[0] == 1 ? [1, 4] : u8[0] == 2 ? [5, 8] : [0, 1] - Nested conditions
u8[0] == 42 - Equality
u8[0] != 0 - Inequality
u16le[0:] > 1000 - Greater than
u16le[0:] >= 1000 - Greater than or equal
u8[0] < 128 - Less than
u8[0] <= 127 - Less than or equal
u8[100] ?? [0, 1] - Return [0,1] if offset 100 is out of bounds
When applied to ranges, &&, ||, and ^^ perform byte-wise bitwise operations:
[0:2] && [2:4] - Bitwise AND between two ranges
[0:2] || [2:4] - Bitwise OR between two ranges
[0:2] ^^ [2:4] - Bitwise XOR between two ranges
[0] && [1] - Single byte AND operation
!(u8[0] > 128) - Logical NOT (for conditions)
Note: For mismatched range lengths, operations use truncation (only overlapping byte positions are used).
BREX provides several shorthand forms to make common expressions more concise.
Instead of explicit pipe operators, you can chain operations by placing brackets directly adjacent:
Verbose: [10:20] | [5]
Concise: [10:20][5]
Verbose: [20:, [0]==31] | [2:]
Concise: [20:, [0]==31][2:]
Multi-step verbose: [20:, [0]==31] | [2:] | [0:4]
Multi-step concise: [20:, [0]==31][2:][0:4]
Simple equality conditions can omit the explicit field reference:
Verbose: [20:, [0]==31]
Concise: [20:, ==31] (assumes [0]==31)
Verbose: [20:, [0]==0x42]
Concise: [20:, ==0x42] (assumes [0]==0x42)
Use verbose syntax when:
Use concise syntax when:
From highest to lowest precedence:
[offset], type[offset], literals, parenthesesexpr[range], expr[offset] (left-associative)!, - (unary minus), @*, /, %+, -<, <=, >, >===, !=| (expression chaining, left-associative)^^&&||??? :
u8[0] + u8[1] * 2 is equivalent to u8[0] + (u8[1] * 2)
[0] && [1] ^^ [2] is equivalent to ([0] && [1]) ^^ [2]
u8[0] == 1 ? 2 : 3 + 4 is equivalent to u8[0] == 1 ? 2 : (3 + 4)
[20:, ==31] | [2:] | [0] + 5 is equivalent to (([20:, ==31] | [2:]) | [0]) + 5
u16le[0:] + u16le[2:]u16le[0:] + [2:] (works but may not be intended)
Good: Safe access with fallback
u8[1] >= 6 ? u32le[u8[2]:] : 0
Also Good: Use null coalescing
u32le[u8[2]:] ?? 0
Good: Clear intent
u8[0] == 0x04 ? [1, u8[0]] : [0, 1]
Consider breaking complex expressions into steps:
[20:, [0]==31][2:] - First find TLV, then extract value
?? to provide fallback values