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