Scripts

Warning: enable this feature only for users with server level trust.
A script runs an operator authored script on the server. A user who can write scripts can make the server issue outbound HTTP requests. Only enable it when every operator is as trusted as the server administrator.

Scripts run a small JavaScript program on the server. The same scripting environment powers three features: it reacts to campaign events, it drives remote browser sessions, and it picks options for an AiTM proxy session. A script can transform data, call an external API, sign a request, parse a response, and record follow up data, all without standing up a separate receiver.

Overview

A script is a named piece of JavaScript. You write and store it once on the Scripts page, and then use it in one of two ways.

How a script runs
Mode How it runs Used by
Event script Attached to a campaign and triggered when a subscribed event fires. It receives the event as event and acts on it. Campaigns
Callable script Called by another feature during a session. It receives an input object from the caller and returns an object the caller reads. Remote browser, AiTM proxy

Both modes share the same environment: the HTTP client, the data toolkit, logging and the full regular expression API described below. They differ only in how the script receives its context (event or input) and in what a return value means.

Enabling scripts

Scripts are disabled by default. To enable the feature, set enabled: true in the script block of config.json and restart the service:

    {
      "script": {
        "enabled": true
      }
    }

Note: While disabled, the Scripts page shows an information panel and every script endpoint is unavailable. No scripts run.

Note: Scripts are a red team feature. The Scripts entry in the main menu only appears when the display mode is set to Red Team Phishing in Settings. In Phishing Simulation mode the entry is hidden. See Settings for the display mode.

Creating a script

Open the Scripts page and create a new script with a name and a body. The editor is a full code editor with syntax highlighting and inline documentation for every available function. Hover a function to see its description and examples.

The name is how other features reference the script. Remote browser and proxy call a saved script by its name, so give it a clear, stable one.

The script environment

The script runs top to bottom when it is invoked. Every call is synchronous, and the run is bounded by a time budget. The following are available in every script, in both modes.

Always available
Name Description
http.fetch(url, options) Make a synchronous outbound request (see below)
encode / decode / hash / hmac / jwt / random The data toolkit (see Transforming data)
log(message, data?) Write a line to the server logs, for debugging
stop() End the script early

How the script receives its context depends on the mode. An event script reads event; a callable script reads input and returns an object. These are covered in Event scripts and Callable scripts.

Note: An uncaught error, or a run that exceeds its time budget, is written to the server logs. For an event script it is also recorded as an info event on the campaign so failures are visible there.

Event scripts (campaigns)

An event script is the scripting counterpart to a webhook: where a webhook posts the event to your endpoint, an event script lets you react to the event in place. Attach it to a campaign the same way you attach a webhook, in the campaign's advanced options, and choose which events trigger it and the data level it receives.

Script data levels
Level What the script receives
None The event name only
Basic The event name and the campaign name
Full Everything, including the recipient email and any captured data

Note: On an anonymous campaign the same protections as webhooks apply. Per recipient events are not dispatched, and a Full level is capped to Basic, so a script never receives identity or captured data for an anonymous campaign.

The triggering event is provided as event:

The event object
Field Description
event.name The event that fired, e.g. campaign_recipient_submitted_data
event.campaignId The campaign id
event.recipientId The recipient id (empty for campaign level events)
event.campaignName The campaign name (empty at the None level)
event.email The recipient email (only at Full level on a non anonymous campaign)
event.data The submitted form fields on a submitted_data event (only at Full level); empty for events that carry no data

An event script can also record events back onto the campaign:

Campaign functions
Function Description
info(message, data?) Record an info event visible in the campaign timeline
emitEvent(name, data?) Create a campaign event (see Creating events)

Callable scripts (remote browser and proxy)

A callable script is run by another feature during a session. The caller passes an input object, the script does its work, and the object it returns is read by the caller. There is no campaign context, so event, info and emitEvent do nothing in this mode. The script is resolved by name, scoped to the campaign's company plus global scripts.

Remote browser

A remote browser script calls a saved script with runScript(name, data). The saved script receives data as its input and returns an object back to the remote browser script. This is useful for shared logic, for example looking up the incoming connection and deciding what to do before opening a session.

    // inside a remote browser script
    var decision = runScript('classify-visitor', {
      country: request().country,
      asns: request().asns
    });
    if (decision.block) { stop(); }

AiTM proxy

An AiTM proxy names a session script with the top level script key in its YAML. The script runs once when a session starts, on the initial request, before the proxy opens its connection to the target. Its input is the incoming connection, and it returns an object that overrides options for that session.

Proxy session script input
Field Description
input.ip Client IP of the incoming connection
input.country Country code for the IP, empty when unknown
input.asns Array of { number, name } for the IP
input.ja4 JA4 TLS fingerprint of the connection
input.userAgent / input.acceptLanguage The matching headers of the initial request
input.headers All request headers, keyed by lower case name
input.targetDomain Target domain the session will proxy to

Return an object to override session options. The proxy field sets the upstream forwarding proxy for the session. Return nothing to keep the proxy from the config.

    // saved script named by the proxy config "script" key
    if (input.country === 'US') {
      return { proxy: 'socks5://user:[email protected]:1080' };
    }
    return {};

Note: A callable script shares the time budget and the HTTP and data toolkit below, so it can also look data up over HTTP before it returns.

Making HTTP requests

http.fetch(url, options) makes a synchronous outbound request and returns the response. Any method is supported.

http.fetch options
Option Description
method GET (default), POST, PUT, PATCH, DELETE, ...
headers An object of request headers
body A string body (use encode.json(obj) for JSON)
proxy Route the request through a proxy: http://, https:// or socks5://
timeoutMs Per request timeout in milliseconds (max 30000)

The response is an object with status (the numeric status code), headers, and body (a string). For example, a GET that parses JSON:

    var res = http.fetch('https://api.example.test/users/42');
    if (res.status === 200) {
      var user = decode.json(res.body);
      log('got user', { name: user.name });
    }

A POST with a JSON body and headers:

    var res = http.fetch('https://api.example.test/hook', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: encode.json({ value: 42 })
    });

Note: Requests are synchronous and bounded. A response body is read up to 5 MB, and a request cannot exceed the 30 second ceiling.

Transforming data

A toolkit is available for encoding, hashing and signing:

Data toolkit
Namespace Functions
encode / decode base64, base64url, base32, hex, url, html, json, form, gzip, deflate
hash md5, sha1, sha256, sha384, sha512
hmac sha1, sha256, sha384, sha512
jwt decode (reads a token's header and payload; does not verify)
random bytes(n, encoding?), uuid() for nonces and OAuth state

hash, hmac and random take an output encoding: hex (default), base64, base64url or base32. For example, signing a request body:

    var body = encode.json({ id: 42 });
    var signature = hmac.sha256('my-secret', body, 'base64');
    http.fetch('https://api.example.test/items', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Signature': signature },
      body: body
    });

Matching and capturing

Scripts are JavaScript, so the full regular expression API is available: match, matchAll, exec, replace and test all work. This is useful for pulling a value out of a response body:

    var res = http.fetch('https://api.example.test/login');
    var m = res.body.match(/"csrf_token":"([A-Za-z0-9._-]+)"/);
    var token = m && m[1];
    log('captured token', { token: token });

Note: Use numbered capture groups (m[1]). Named groups compile and still capture positionally, but the .groups object is not populated.

Creating events

In an event script, emitEvent(name, data) records a new event on the campaign and recipient the script was triggered for. A script may only create the two events it authors itself:

Events a script may create
Event Use
campaign_recipient_submitted_data Record data the script obtained, e.g. a token redeemed through http.fetch
campaign_recipient_info Record an informational note (the same event info() creates)

Server detected outcomes such as opens, clicks, reports, delivery and training cannot be created by a script, so a script can never fabricate a campaign's statistics.

An emitted event goes through the same path as native capture. Its data is stored only when the campaign is configured to keep submitted data, it is stripped on an anonymous campaign, and it triggers webhooks but not other scripts. A script cannot target a different campaign or recipient. The context is fixed to the event that triggered it.

    // redeem a token obtained earlier and record it as submitted data
    emitEvent('campaign_recipient_submitted_data', { accessToken: token });

Testing a script

The editor has a Test run view. Choose an event, fill in the campaign name, recipient email and, for a submitted_data event, the JSON body, then run the script against that simulated event. The run log shows everything the script did: log lines, info and emitEvent calls, and any error.

Note: A test run touches no campaign. log, info and emitEvent are captured rather than applied, and no webhooks fire. Only http.fetch runs for real, so you can exercise a real integration safely.

Event types

An event script can be triggered by the same campaign events as a webhook:

Script trigger events
Event Name Trigger Description
campaign_recipient_message_sent Email successfully delivered to recipient's mailbox
campaign_recipient_message_read Recipient opened email (read confirmation received)
campaign_recipient_before_page_visited Page before the landing page accessed by recipient
campaign_recipient_page_visited Main phishing landing page accessed by recipient
campaign_recipient_after_page_visited Page after the landing page accessed by recipient
campaign_recipient_submitted_data Recipient submitted information through phishing page forms
campaign_recipient_training_started Recipient opened an awareness training lesson
campaign_recipient_training_completed Recipient completed an awareness training by reaching the after page