Automation Scripts - rogers-group-inc/polaris GitHub Wiki
Automations → Scripts. A registry of operator-authored scripts that automations can run — on the Polaris server, or on the device that triggered the alert.
This is an RCE-equivalent surface. A server script runs as the
polarisservice user. An agent script runs as the agent's user — unprivileged on Linux, LocalSystem on Windows. Anyone who can write a script here, or attach one to an automation, can execute arbitrary code on your estate.The
automationScriptskey stops atwrite— it has nofullwriterung. It is seededwriteonly for admin-equivalent roles andnonefor every other built-in role. That is the right default. Widening it is a decision about who may run code on your hosts, not about who may edit automations.Every surface in Polaris that touches this carries a human-review warning. Treat any script that reaches production the way you would treat a change to the hosts themselves.
| Gate | Grants |
|---|---|
automationScripts:read |
see the tab and run history |
automationScripts:write |
create, edit, delete, test-run, and attach a script action to an automation |
+ Add script opens the editor. Each script is a card showing its
interpreter and run target, its timeout and the start of its sha256, with
Test run, Edit and Delete for write.
| Field | |
|---|---|
| Name | unique across the registry |
| Description | free text |
| Interpreter |
bash · sh · powershell · cmd · python3
|
| Body | the script source, ≤ 64 KB |
| Runs on |
server (the Polaris server, the default) · agent (the triggering asset's agent) · either
|
| Default timeout | 1–600 seconds, default 60; a script action may override it |
| Enabled | a disabled script refuses to run |
Polaris computes a sha256 of the body on every save. That digest is what an agent verifies before executing, and a client-supplied hash is never accepted.
- Creating a script writes a warning-level Event.
- Changing the body writes a warning Event carrying the old and new sha256. An edit that leaves the body alone writes an info Event.
- Deleting a script writes a warning Event carrying its last sha256.
Script tampering has to be visible in the audit log, and Events are shipped off-host by the syslog and SFTP archivers.
Refused with a friendly 409 while any automation's actions or escalation tiers reference the script. Run rows snapshot the script name and digest, so history survives deletion.
Not arguments by default, and not alert context on the command line. Alert context rides environment variables:
| Variable | Holds |
|---|---|
POLARIS_ALERT_ID |
the triggering alert's id (empty on a test run) |
POLARIS_RULE |
the automation's id |
POLARIS_ASSET |
the triggering asset's id |
Use those. A script that needs more should call back into the REST API with a bearer token bound to a suitably narrow role.
A script action may carry an args template using the same {token}
vocabulary as an email body — so {asset} and {asset.ip} are available.
The rendered string travels as a single argv entry. It is never concatenated into a shell string, on either the server or the agent.
cmd /chas no argv. It re-parses the raw command line, in which&,|,<,>,(,)and^are operators — so on that path, cmd.exe is the shell.Ordinary argv quoting follows the C-runtime convention, which cmd.exe does not implement, so a naively quoted argument of
x" & <command> & rem "would execute<command>. That is reachable rather than theoretical:argsis a rendered template over alert context, so a device's own hostname can reach it.Polaris therefore
^-escapes every metacharacter, wraps the command in the extra quote pair/srequires, and passes it verbatim so nothing re-quotes it. Arguments containing",%,!or any control character are refused with an actionable error rather than mangled — cmd has no in-quote escape for a quote, expands%and!at parse time, and treats CR / LF / NUL / 0x1A as line or file enders.The Go agent applies the identical escaping (agent ≥ 0.17.2). The two move in lockstep.
If you can express the job in
powershellinstead, do.
The child process gets process.env minus secret-shaped keys — anything
matching SECRET, TOKEN, PASSWORD, PASSWD, DATABASE_URL, SESSION,
CREDENTIAL, PRIVATE_KEY, or ending in _KEY.
This is not a privilege boundary — authoring a script already requires an
RCE-equivalent permission. It exists because stdout is stored on the run row
and rendered in the Scripts tab: without the scrub, a one-line env would copy
DATABASE_URL and POLARIS_SECRET_KEY into a displayed, backed-up, unencrypted
column.
It is a denylist, so PATH, HOME, proxy and locale variables stay
inherited.
| Body | 64 KB |
| stdout / stderr | 64 KB each, then capped — an over-cap kill is classified as a failure, not a timeout |
| Timeout | up to 600 s, then SIGKILL |
| Temp file | mode 0600, under the state directory, always removed |
| Interpreter | resolved to a known absolute path where one exists on disk, falling back to a PATH lookup only when none does |
That last one matters: an install with a non-standard layout must keep running,
but the inherited PATH should not get first say in which binary executes your
scripts.
Executed by the runAutomationScripts job on a 5-second tick, on the web (or
all) role.
Execution never happens inline in the alert engine or the delivery drain. A
wedged script must not stall alert evaluation or deliveries — so a run is a
queued row, claimed with a status re-check so two processes cannot double-run
it, and a stuck-running sweep flips rows past timeout + 60 s.
Queued as an AgentCommand on the triggering asset's agent, which is
preflighted before the run row is created: the agent must be installed,
active, and at version ≥ 0.13.0.
The agent is a satellite — it never self-acts and refuses unknown actions. For a script it:
- Verifies the sha256 against the body it was handed. A mismatch refuses.
- Refuses unknown interpreters, and platform-mismatched ones —
bash,shandpython3are refused on Windows;cmdis refused off Windows. - Writes the body into a 0700 temp directory, always removed.
- Runs it with the same argv posture, timeout and 64 KB caps as the server.
- Reports exit code, stdout and stderr back.
Remember the privilege: LocalSystem on Windows, and on Linux whatever tier the agent unit runs at.
The action decides. The run target on the action must be compatible with the script's, or the request is refused before a run row exists.
Each script has a Test run button (automationScripts:write). After a
confirmation it starts a server-side run with no arguments and shows the
status, exit code, stdout and stderr under the card when it finishes. A script
that runs on agent only cannot be test-run from here — trigger it through an
automation on a real asset.
Script and api_call actions are never offered in an automation's
test-delivery block on the Summary step — the server refuses to run them from a
button, on purpose.
Every run is kept as a row, read through the API
(/automations/scripts/runs, filterable by script, alert and status). The
Scripts tab itself shows only the result of a Test run.
| Column | |
|---|---|
| Script name + sha256 | snapshots, so history survives deletion or a body change |
| Run on |
server / agent
|
| Asset | agent runs: the triggering device |
| Args | the rendered template snapshot |
| Status |
pending · running · succeeded · failed · timeout
|
| Exit code, stdout, stderr | capped at 64 KB each |
| Requested by |
system:automation, or the operator who pressed Test |
| Timestamps | requested / started / completed |
Rows are pruned after ~90 days. Every run also writes an
automation.script.run Event.
The alert link (notificationId) deliberately carries no foreign key — the
alert lifecycle is independent, and alerts get cleared and pruned on their own
schedule.
- Make scripts idempotent. A reminder or an escalation tier can fire the same action again, and a run can be retried.
- Exit non-zero on failure, and write the reason to stderr. Both are stored and both are visible.
- Do not print secrets. stdout is stored, displayed, and included in backups.
- Keep them short. The default timeout is 60 seconds, and a long-running script is better as something the script triggers than something it waits on.
-
Prefer
api_callwhere an HTTP request would do. It does not need the RCE-equivalent grant. - Review the body, not the diff. The warning Event records the digests precisely so that a change is reviewable — use it.