Sleep Language Guide¶
Introduction to the Sleep scripting language used by Stentor's CNA engine: data types, variables, operators, control flow, functions, closures, arrays, hashes, and string manipulation.
Introduction¶
Stentor's scripting engine is built on CNA (Cobalt Strike's Aggressor Script), which uses the Sleep scripting language as its foundation. Sleep is a Perl-inspired scripting language with C-like syntax, originally created by Raphael Mudge for embedding in Java applications.
Stentor implements a full Sleep interpreter server-side -- including lexer, parser, evaluator, and semantic validator -- allowing operators to write scripts that automate operations, react to beacon events, extend the command set, and build custom workflows. Unlike Cobalt Strike's Java-based interpreter, Stentor's CNA engine is written in Go, providing native performance and tight integration with the backend.
CNA scripts can:
- Define custom beacon commands with
alias - React to beacon lifecycle events with
on - Add context menu items with
popup - Override default behavior with
sethooks - Register console commands with
command - Bind keyboard shortcuts with
bind
Sleep vs. CNA
Sleep is the base scripting language (data types, variables, operators, control flow, functions). CNA extends Sleep with C2-specific keywords (alias, on, popup, set, command, bind) and built-in functions (bshell, bps, blog, etc.) for interacting with beacons and the Stentor platform. Every CNA script is valid Sleep code, but not all Sleep code uses CNA-specific features.
Hello World¶
The simplest CNA script prints a message to the Script Console:
Loading Your Script¶
In the Script Console, use the load command:
The script is parsed, validated, and executed immediately. You should see:
When a script is loaded, Stentor's engine performs three steps:
- Parse -- The lexer tokenizes the source and the parser builds an AST
- Validate -- Semantic analysis checks for undefined functions and variable issues
- Execute -- Top-level code runs (like
println), and CNA keywords (on,alias, etc.) register their handlers
Data Types¶
Stentor's Sleep evaluator supports the following data types:
Strings¶
Double-quoted strings with escape sequence support and variable interpolation:
$greeting = "Hello";
$name = "Operator";
$message = "Welcome, $name!"; # interpolation: "Welcome, Operator!"
$path = "C:\\Users\\Public"; # escape: backslash
$multiline = "Line 1\nLine 2"; # escape: newline
Supported escape sequences: \n (newline), \t (tab), \\ (backslash), \" (double quote), \r (carriage return).
String interpolation: Any $variable inside a double-quoted string is replaced with its value. Use \$ to include a literal dollar sign. The special variable $+ concatenates adjacent interpolated values without whitespace:
Integers¶
64-bit signed integers, with support for decimal and hexadecimal literals:
Doubles¶
64-bit floating-point numbers:
Long¶
Explicit 64-bit long integers (distinct from int in the type system):
Null¶
The null value, represented by $null:
Booleans¶
Boolean values from comparison and predicate operations:
Arrays¶
Ordered lists of values, created with the @() syntax. Arrays are zero-indexed and can hold mixed types:
Hashes¶
Key-value maps (dictionaries), created with the %() syntax. Keys are strings:
Type checking
Use the typeof() function to inspect a value's type at runtime:
println(typeof("hello")); # "String"
println(typeof(42)); # "Int"
println(typeof(@())); # "Array"
println(typeof(%())); # "Hash"
Predicate functions are also available: -isarray, -ishash, -isfunction, -isnumber.
Variables¶
Sleep uses sigil-prefixed variable names to indicate type:
Scalar Variables¶
Scalar variables begin with $ and hold a single value (string, number, null, function, etc.):
Array Variables¶
Array variables begin with @:
Hash Variables¶
Hash variables begin with %:
Variable Scope¶
By default, variables are global -- accessible from any function in the script. Use the local() function to declare variables as local to the current function:
sub processBeacon {
local('$bid $info'); # $bid and $info are local to this function
$bid = $1;
$info = beacon_info($bid);
# $bid and $info are destroyed when this function returns
}
The global() function explicitly declares variables in global scope (useful inside functions where you want to ensure global access):
Special Variables¶
| Variable | Description |
|---|---|
$1, $2, ... $n | Positional arguments to functions, aliases, and event handlers |
$0 | For aliases: the full command string (name + args, unparsed) |
@_ | Array containing all arguments passed to the current function |
$null | The null value |
sub example {
println("First arg: $1");
println("Second arg: $2");
println("All args: " . join(", ", @_));
}
example("hello", "world");
# Output:
# First arg: hello
# Second arg: world
# All args: hello, world
Operators¶
Arithmetic Operators¶
| Operator | Description | Example |
|---|---|---|
+ | Addition | $x = 5 + 3; |
- | Subtraction | $x = 10 - 4; |
* | Multiplication | $x = 6 * 7; |
/ | Division | $x = 20 / 4; |
% | Modulus | $x = 10 % 3; |
** | Exponentiation | $x = 2 ** 10; |
Whitespace required
Sleep requires whitespace between operators and operands. $x=1+2; will not parse. Write $x = 1 + 2; instead.
String Concatenation¶
The dot (.) operator concatenates strings:
The string repetition operator (x) repeats a string:
Numeric Comparison¶
| Operator | Description | Example |
|---|---|---|
== | Equal | if ($x == 5) |
!= | Not equal | if ($x != 0) |
< | Less than | if ($x < 10) |
> | Greater than | if ($x > 0) |
<= | Less or equal | if ($x <= 100) |
>= | Greater or equal | if ($x >= 1) |
String Comparison¶
| Operator | Description | Example |
|---|---|---|
eq | String equal | if ($s eq "admin") |
ne | String not equal | if ($s ne "") |
lt | Lexicographically less | if ($a lt $b) |
gt | Lexicographically greater | if ($a gt $b) |
cmp | Compare (returns -1/0/1) | $r = $a cmp $b; |
Logical Operators¶
| Operator | Description | Example |
|---|---|---|
&& | Logical AND (short-circuit) | if ($a && $b) |
\|\| | Logical OR (short-circuit) | if ($a \|\| $b) |
! | Logical NOT | if (!$found) |
Pattern Matching¶
| Operator | Description | Example |
|---|---|---|
iswm | Wildcard match (* and ?) | if ("admin*" iswm $user) |
ismatch | Regex match (sets matched()) | if ($str ismatch '(\d+)\.(\d+)') |
isin | Membership test | if ("key" isin %hash) |
Assignment Operators¶
| Operator | Description |
|---|---|
= | Simple assignment |
+= | Add and assign |
-= | Subtract and assign |
*= | Multiply and assign |
/= | Divide and assign |
.= | Concatenate and assign |
&= | Bitwise AND and assign |
\|= | Bitwise OR and assign |
Bitwise Operators¶
| Operator | Description |
|---|---|
& | Bitwise AND |
\| | Bitwise OR |
^ | Bitwise XOR |
<< | Left shift |
>> | Right shift |
Control Flow¶
if / else if / else¶
if ($integrity eq "High") {
println("Already elevated!");
}
else if ($integrity eq "Medium") {
println("Need to escalate privileges");
}
else {
println("Low integrity -- limited options");
}
while Loops¶
for Loops¶
C-style for loops with init, condition, and update expressions:
foreach Loops¶
Iterate over arrays and hashes:
# Iterate over an array
@targets = @("10.0.0.20", "10.10.10.21", "10.10.10.22");
foreach $target (@targets) {
println("Scanning: $target");
}
# Iterate with index
foreach $idx => $target (@targets) {
println("$idx: $target");
}
# Iterate over a hash
%config = %(sleep => 30, jitter => 20, protocol => "https");
foreach $key => $value (%config) {
println("$key = $value");
}
break and continue¶
foreach $item (@items) {
if ($item eq "skip") {
continue; # skip to next iteration
}
if ($item eq "stop") {
break; # exit the loop
}
println($item);
}
try / catch¶
Error handling with try-catch blocks:
Ternary with iff()¶
The iff() function provides short-circuit ternary evaluation:
Only the matching branch is evaluated, making iff() safe for expressions with side effects.
Functions¶
Defining Functions¶
Functions are declared with the sub keyword:
Arguments¶
Function arguments are accessed through positional variables $1, $2, etc. The @_ array contains all arguments:
sub add {
return $1 + $2;
}
sub printAll {
foreach $arg (@_) {
println($arg);
}
}
$sum = add(3, 7); # $sum = 10
printAll("a", "b", "c"); # prints a, b, c on separate lines
Return Values¶
Use return to return a value from a function. If no return is used, the function returns the result of the last evaluated expression:
Closures¶
A closure is an anonymous function (code block) that captures its enclosing scope. Closures are created with { ... } syntax:
Invoking closures -- Use bracket notation [$fn] or [$fn : args]:
Function References¶
The & operator creates a reference to a named function:
sub myFunction {
println("Called with: $1");
}
$ref = &myFunction;
[$ref : "test"]; # prints: Called with: test
Lambda¶
The lambda() function creates a copy of a function with bound variables:
sub handler {
println("Beacon $bid says: $1");
}
# Create a handler copy with $bid pre-bound
$bound = lambda(&handler, $bid => "abc123");
[$bound : "hello"]; # prints: Beacon abc123 says: hello
This pattern is commonly used with CNA callbacks and popup menus where you need to pass context through to a handler that will execute later.
Local Variables in Functions¶
Use local() to declare variables that are scoped to the current function:
sub processTarget {
local('$ip $port $result');
$ip = $1;
$port = $2;
$result = scan($ip, $port);
return $result;
}
Always use local
Without local(), variables inside functions leak into global scope and persist after the function returns. This can cause subtle bugs in long-running scripts. Best practice is to always declare function-local variables with local().
Arrays and Hashes¶
Array Operations¶
| Function | Description | Example |
|---|---|---|
push(@arr, $val) | Append value to end | push(@targets, "10.10.10.23"); |
pop(@arr) | Remove and return last element | $last = pop(@queue); |
shift(@arr) | Remove and return first element | $first = shift(@queue); |
add(@arr, $val, $idx) | Insert at index | add(@list, "new", 2); |
size(@arr) | Get array length | $len = size(@targets); |
copy(@arr) | Deep copy array | @clone = copy(@original); |
reverse(@arr) | Reverse in-place | reverse(@items); |
sublist(@arr, $start, $end) | Extract sub-array | @slice = sublist(@arr, 1, 4); |
sorta(@arr) | Sort alphabetically | sorta(@names); |
sortn(@arr) | Sort numerically | sortn(@numbers); |
addAll(@dst, @src) | Append all from src | addAll(@all, @new); |
removeAll(@dst, @remove) | Remove matching elements | removeAll(@list, @exclude); |
Array iteration:
@beacons = @("beacon-1", "beacon-2", "beacon-3");
foreach $bid (@beacons) {
println("Processing: $bid");
}
# With index
foreach $i => $bid (@beacons) {
println("[$i] $bid");
}
Array indexing:
@items = @("first", "second", "third");
println(@items[0]); # "first"
println(@items[-1]); # "third" (negative index)
@items[1] = "SECOND"; # assignment
Hash Operations¶
| Function | Description | Example |
|---|---|---|
keys(%h) | Get array of all keys | @k = keys(%config); |
values(%h) | Get array of all values | @v = values(%config); |
removeAt(%h, "key") | Remove key and return value | $old = removeAt(%map, "key"); |
putAll(%dst, %src) | Merge src into dst | putAll(%config, %overrides); |
ohash() | Create ordered hash | %oh = ohash(); |
size(%h) | Get number of keys | $n = size(%config); |
Hash access and assignment:
%info = %(os => "Windows 10", arch => "x64");
# Read
$os = %info["os"];
# Write
%info["pid"] = 1234;
# Check membership
if ("arch" isin %info) {
println("Architecture: " . %info["arch"]);
}
# Delete
removeAt(%info, "pid");
Hash iteration:
%targets = %(
"10.0.0.20" => "DC01",
"10.10.10.21" => "WEB01",
"10.10.10.22" => "SQL01"
);
foreach $ip => $hostname (%targets) {
println("$ip -> $hostname");
}
String Operations¶
Concatenation¶
$full = "Hello" . " " . "World";
$full .= "!"; # compound concatenation
println($full); # "Hello World!"
Common String Functions¶
| Function | Description | Example |
|---|---|---|
strlen($s) | String length | $len = strlen("hello"); -- 5 |
substr($s, $start, $end) | Substring | substr("hello", 1, 3) -- "el" |
indexOf($s, $sub) | Find substring position | indexOf("hello", "ll") -- 2 |
replace($s, $pattern, $rep) | Regex replace | replace("foo bar", "bar", "baz") |
strrep($s, $old, $new) | Literal string replace | strrep("aabaa", "a", "x") -- "xxbxx" |
split($delim, $s) | Split into array | @parts = split(",", "a,b,c"); |
join($delim, @arr) | Join array to string | $s = join(", ", @items); |
lc($s) | Lowercase | lc("HELLO") -- "hello" |
uc($s) | Uppercase | uc("hello") -- "HELLO" |
trim($s) | Strip whitespace | trim(" hi ") -- "hi" |
left($s, $n) | First N characters | left("hello", 3) -- "hel" |
right($s, $n) | Last N characters | right("hello", 3) -- "llo" |
chr($code) | Character from code point | chr(65) -- "A" |
asc($char) | Code point from character | asc("A") -- 65 |
charAt($s, $idx) | Character at index | charAt("hello", 1) -- "e" |
matches($s, $pattern) | Regex capture groups | @m = matches("v1.2", '(\d+)\.(\d+)'); |
Regex Matching¶
Sleep supports regex matching with ismatch and the matched() function:
$version = "Stentor v3.2.1";
if ($version ismatch 'v(\d+)\.(\d+)\.(\d+)') {
($major, $minor, $patch) = matched();
println("Major: $major, Minor: $minor, Patch: $patch");
}
The matched() function returns capture groups from the most recent ismatch operation.
Wildcard Matching¶
The iswm operator performs wildcard matching (* for any characters, ? for single character):
if ("admin*" iswm $username) {
println("Admin user detected");
}
if ("10.10.10.??" iswm $ip) {
println("Internal subnet");
}
String Formatting¶
The format() function provides printf-style formatting:
Supported format specifiers: %s (string), %d (integer), %f (float), %x (hex), %o (octal), %% (literal percent).
Type Conversion¶
| Function | Description | Example |
|---|---|---|
casti($val) | Convert to integer | $n = casti("42"); |
castd($val) | Convert to double | $d = castd("3.14"); |
castl($val) | Convert to long | $l = castl($n); |
typeof($val) | Get type name string | typeof(42) -- "Int" |
expr($val) | Coerce to numeric | $n = expr("100"); |
Script Loading and the Script Console¶
The Script Console¶
The Script Console is an interactive environment for loading, managing, and debugging CNA scripts. It provides a REPL (Read-Eval-Print Loop) for testing Sleep expressions and executing statements.
Console Commands¶
| Command | Arguments | Description |
|---|---|---|
? | expression | Evaluate a Sleep predicate and print true/false |
e | statement | Execute a Sleep statement and show output |
help | List all available console commands | |
load | /path/to/script.cna | Load and execute a CNA script |
ls | List all currently loaded scripts | |
reload | script.cna | Unload, re-parse, and re-execute a script |
unload | script.cna | Unload a script and remove its registrations |
x | expression | Evaluate a Sleep expression and print its value |
proff | script.cna | Disable the profiler for a script |
profile | script.cna | Dump performance statistics for a script |
pron | script.cna | Enable the profiler for a script |
troff | script.cna | Disable function tracing for a script |
tron | script.cna | Enable function tracing for a script |
Examples:
x 2 + 2
# 4
x @("a", "b", "c")
# @("a", "b", "c")
e println("Hello World!");
# Hello World!
? "admin*" iswm "administrator"
# true
load /opt/scripts/my-automation.cna
# Script loaded successfully
ls
# /opt/scripts/my-automation.cna (loaded)
Loading Scripts via API¶
Script Lifecycle¶
When a CNA script is loaded, it goes through a precise lifecycle:
- Load -- The file is read from disk (or received via API)
- Parse -- The lexer tokenizes the source and the parser builds an AST (Abstract Syntax Tree)
- Validate -- Semantic analysis checks for undefined function calls and variable issues; errors block loading, warnings are logged
- Execute -- Top-level code runs immediately (variable assignments,
println, etc.) - Register -- CNA keywords (
on,alias,popup,set,command,bind) register their handlers with the engine's Registry
Unload cleans up
When a script is unloaded (via unload command or API), all its registered handlers are removed from the Registry. This includes aliases, events, hooks, popup builders, commands, and key bindings. Other scripts' registrations are not affected.
Auto-Loading and Watching¶
Stentor supports automatic script loading from a directory:
- Auto-load: All
*.cnafiles in the configured scripts directory are loaded at startup - Watch mode: The engine polls the directory every 2 seconds, automatically loading new scripts, reloading modified scripts, and unloading deleted scripts
CNA Keywords¶
CNA extends Sleep with six keywords for interacting with the Stentor platform. Each keyword registers a handler that fires under specific conditions.
on -- Event Handlers¶
Register a handler that fires when a specific event occurs:
The on keyword supports a wildcard * meta-event that fires for ALL events:
Multiple scripts can register handlers for the same event -- all handlers fire.
alias -- Beacon Commands¶
Register a custom command available in the beacon console:
alias survey {
btask($1, "Running survey", "T1082");
bshell!($1, "whoami /all");
bshell!($1, "ipconfig /all");
bshell!($1, "netstat -na");
}
Alias arguments:
| Variable | Description |
|---|---|
$0 | Full command string (name + args, unparsed) |
$1 | Beacon ID |
$2, $3, ... | Individual arguments (space-separated, quotes group) |
popup -- Context Menus¶
Add items to context menus in the UI:
popup beacon_top {
item "Quick Survey" {
bshell($1, "whoami /groups");
}
menu "Network" {
item "ARP Table" {
bshell($1, "arp -a");
}
item "Connections" {
bshell($1, "netstat -na");
}
}
separator();
item "Hash Dump" {
bhashdump($1);
}
}
Available popup hooks: beacon_top, beacon, beacon_bottom, ssh, and custom hooks.
set -- Hook Overrides¶
Override default Stentor behavior by setting hook values:
Hooks use last-writer-wins semantics -- the most recently loaded script's set value takes effect.
command -- Console Commands¶
Register commands available in the Script Console:
After loading, type hello Operator in the Script Console to see Hello, Operator!.
bind -- Keyboard Shortcuts¶
Register handlers for keyboard shortcuts:
Shortcuts support modifiers: Ctrl, Shift, Alt, Meta.
Cross-reference
For the complete list of events, hooks, and popup hooks, see the Hooks & Events reference. For all built-in CNA functions (b* functions, data model queries, UI helpers), see the Function Reference.
Practical Examples¶
Auto-Configure New Beacons¶
Set sleep time and jitter when a beacon first checks in:
on beacon_initial {
# Set 30-second sleep with 20% jitter
bsleep($1, 30, 20);
blog($1, "Auto-configured: 30s sleep, 20% jitter");
}
Custom Process Listing Alias¶
Create an alias that lists processes and logs the action:
alias myps {
btask($1, "Listing processes", "T1057");
bps($1);
blog($1, "Process listing requested by operator");
}
Popup Menu for Common Actions¶
Add a context menu with frequently used commands:
popup beacon_top {
item "Quick Hash Dump" {
foreach $bid ($1) {
bhashdump($bid);
}
}
menu "Enumeration" {
item "Who Am I" {
foreach $bid ($1) {
bshell($bid, "whoami /all");
}
}
item "Network Info" {
foreach $bid ($1) {
bshell($bid, "ipconfig /all && arp -a");
}
}
}
}
Event Logging¶
Log beacon output to the console with timestamps:
on beacon_output {
local('$bid $text $time');
$bid = $1;
$text = $2;
$time = formatDate(ticks(), "HH:mm:ss");
println("[$time] Beacon $bid output: $text");
}
Data Processing Script¶
Iterate over all beacons and build a summary:
command beacon_report {
local('$entry $id $user $host');
println("=== Beacon Report ===");
foreach $entry (beacons()) {
$id = $entry["id"];
$user = $entry["user"];
$host = $entry["computer"];
println(format(" %-8s %-15s %s", substr($id, 0, 8), $user, $host));
}
println("Total: " . size(beacons()) . " beacons");
}
Closures with Callbacks¶
Use closures for asynchronous operations:
alias checkports {
local('$bid $target');
$bid = $1;
$target = $2;
btask($bid, "Scanning $target for common ports");
# Use a callback to process results
bportscan($bid, $target, "1-1024", "arp", lambda({
blog($bid, "Scan complete for $target");
}, $bid => $bid, $target => $target));
}
Configuration with Hashes¶
Build reusable configurations:
%profiles = %(
"stealth" => %(sleep => 300, jitter => 50),
"normal" => %(sleep => 30, jitter => 20),
"fast" => %(sleep => 5, jitter => 10)
);
alias profile {
local('$bid $name $config');
$bid = $1;
$name = $2;
if ($name !isin %profiles) {
berror($bid, "Unknown profile: $name. Options: stealth, normal, fast");
return;
}
$config = %profiles[$name];
bsleep($bid, $config["sleep"], $config["jitter"]);
blog($bid, "Applied profile: $name (sleep=" . $config["sleep"] . "s, jitter=" . $config["jitter"] . "%)");
}
Comments¶
Comments begin with # and extend to the end of the line:
There are no multi-line comments in Sleep.
Next Steps¶
- Function Reference -- Complete reference for all ~300+ CNA built-in functions
- Hooks & Events -- Event-driven scripting with 24+ hooks and 57+ events
- Headless Mode -- Run CNA scripts without the UI