Skip to content

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 set hooks
  • 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:

println("Hello from Stentor CNA!");

Loading Your Script

In the Script Console, use the load command:

load /path/to/hello.cna

The script is parsed, validated, and executed immediately. You should see:

Hello from Stentor CNA!

Load a script via the REST API:

curl -s -X POST "https://stentor.app/api/v1/scripts/load" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"path": "/path/to/hello.cna"}'

When a script is loaded, Stentor's engine performs three steps:

  1. Parse -- The lexer tokenizes the source and the parser builds an AST
  2. Validate -- Semantic analysis checks for undefined functions and variable issues
  3. 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:

$first = "Red";
$last = "Team";
println("$first $+ $last");  # prints: RedTeam

Integers

64-bit signed integers, with support for decimal and hexadecimal literals:

$count = 42;
$hex = 0x1A;
$negative = -10;
$big = 9223372036854775807;  # max int64

Doubles

64-bit floating-point numbers:

$pi = 3.14159;
$rate = 0.5;
$sci = 1.5e10;

Long

Explicit 64-bit long integers (distinct from int in the type system):

$ts = ticks();  # returns current time in ms as a Long

Null

The null value, represented by $null:

$empty = $null;
if ($empty is $null) {
    println("Value is null");
}

Booleans

Boolean values from comparison and predicate operations:

$result = 5 > 3;     # true
$check = "a" eq "b"; # false

Arrays

Ordered lists of values, created with the @() syntax. Arrays are zero-indexed and can hold mixed types:

@names = @("Alice", "Bob", "Charlie");
@mixed = @(1, "two", 3.0, $null);
@empty = @();

Hashes

Key-value maps (dictionaries), created with the %() syntax. Keys are strings:

%config = %(
    sleep_time => 30,
    jitter     => 20,
    protocol   => "https"
);
%empty = %();

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.):

$name = "operator";
$count = 0;
$callback = { println("fired!"); };

Array Variables

Array variables begin with @:

@targets = @("10.0.0.20", "10.10.10.21");
@results = @();

Hash Variables

Hash variables begin with %:

%beacon_info = %(os => "Windows 10", arch => "x64");
%tasks = %();

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):

sub initConfig {
    global('$config');
    $config = %(sleep => 30, jitter => 20);
}

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:

$full = "Hello" . " " . "World";  # "Hello World"

The string repetition operator (x) repeats a string:

$line = "-" x 40;  # 40 dashes

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

$i = 0;
while ($i < 10) {
    println("Iteration: $i");
    $i++;
}

for Loops

C-style for loops with init, condition, and update expressions:

for ($i = 0; $i < 10; $i++) {
    println("Count: $i");
}

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:

try {
    $result = riskyOperation();
}
catch $error {
    println("Error caught: $error");
}

Ternary with iff()

The iff() function provides short-circuit ternary evaluation:

$label = iff($is_admin, "Admin", "User");

Only the matching branch is evaluated, making iff() safe for expressions with side effects.


Functions

Defining Functions

Functions are declared with the sub keyword:

sub greet {
    println("Hello, $1!");
}

greet("Operator");  # prints: Hello, Operator!

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:

sub max {
    if ($1 > $2) {
        return $1;
    }
    return $2;
}

println(max(42, 17));  # prints: 42

Closures

A closure is an anonymous function (code block) that captures its enclosing scope. Closures are created with { ... } syntax:

$greet = { println("Hello from closure!"); };

Invoking closures -- Use bracket notation [$fn] or [$fn : args]:

$add = { return $1 + $2; };
$result = [$add : 3, 7];
println($result);  # prints: 10

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:

$msg = format("Beacon %s checked in from %s (PID: %d)", $id, $host, $pid);
println($msg);

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

curl -s -X POST "https://stentor.app/api/v1/scripts/load" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"path": "/opt/scripts/automation.cna"}'
curl -s "https://stentor.app/api/v1/scripts" \
  -H "Authorization: Bearer $TOKEN"
curl -s -X POST "https://stentor.app/api/v1/scripts/unload" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"path": "/opt/scripts/automation.cna"}'

Script Lifecycle

When a CNA script is loaded, it goes through a precise lifecycle:

Load -> Parse -> Semantic Validate -> Execute -> Register Keywords
  1. Load -- The file is read from disk (or received via API)
  2. Parse -- The lexer tokenizes the source and the parser builds an AST (Abstract Syntax Tree)
  3. Validate -- Semantic analysis checks for undefined function calls and variable issues; errors block loading, warnings are logged
  4. Execute -- Top-level code runs immediately (variable assignments, println, etc.)
  5. 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 *.cna files 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:

on beacon_initial {
    println("New beacon: $1");
}

The on keyword supports a wildcard * meta-event that fires for ALL events:

on * {
    $event = shift(@_);
    println("Event fired: $event");
}

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)

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:

set PROCESS_INJECT_SPAWN {
    return "C:\\Windows\\System32\\RuntimeBroker.exe";
}

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:

command hello {
    println("Hello, $1!");
}

After loading, type hello Operator in the Script Console to see Hello, Operator!.

bind -- Keyboard Shortcuts

Register handlers for keyboard shortcuts:

bind Ctrl+H {
    println("Help shortcut triggered!");
}

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");
}

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:

# This is a comment
$x = 42;  # inline comment

There are no multi-line comments in Sleep.


Next Steps