Stop scripts reporting success after doing nothing
A run_script command was judged solely on PowerShell's exit code, and PowerShell treats most errors as non-terminating: it writes the error, runs the next line, and still exits zero. The account-provisioning script reported succeeded having provisioned nothing, and because no output was captured there was no evidence to contradict the verdict. Scripts now carry an errormode. Strict prepends $ErrorActionPreference = 'Stop' so the first error ends the run and the exit code means what it says. It is opt-in per script rather than global because bodies authored against the permissive semantics rely on them - tolerating a failing cleanup, probing with a cmdlet that errors when a thing is absent, looping past a bad item - and would start aborting midway, leaving endpoints half-configured. The default is unchanged, so every existing script keeps running exactly as before. The prologue is applied after digest verification, so the SHA-256 still validates the bytes the server delivered. stdout/stderr is captured on both paths: redirected streams for SYSTEM, a transcript file under the ShopFloor profile for user-session runs, since CreateProcessAsUser cannot inherit the service's pipes across the session boundary. Output is always logged locally and returned to the server only on failure, so routine output stays on the machine that produced it instead of filling command history. Also adds the optional per-script working directory, which the same surfaces needed, and a collapsible explainer on the scripts page - the dropdown is not self-explanatory, and an operator who does not know about non-terminating errors has no reason to ever leave the default. Agent 1.14.0. An older agent ignoring errormode keeps today's behaviour.
This commit is contained in:
@@ -465,6 +465,9 @@ Allowlisted actions (server source of truth: `plugins/endpoints/commands.py`):
|
||||
connections with a `truncated` flag in `detail`, because an oversized payload
|
||||
rejected by the server returns nothing at all and a partial answer is more
|
||||
useful than a failed one. Standard reports carry listening ports only.
|
||||
- `run_script` - run a reviewed PowerShell script from the script library. The
|
||||
body is NOT carried on the command channel; the claim carries a descriptor and
|
||||
the agent fetches the bytes separately (section 7.4).
|
||||
|
||||
### 7.1 Claim the next command (long-poll)
|
||||
|
||||
@@ -509,3 +512,69 @@ Allowlisted actions (server source of truth: `plugins/endpoints/commands.py`):
|
||||
gated by user JWT + `endpoints.command`. Only one command may be in flight per
|
||||
endpoint (a second enqueue returns HTTP 409 until the first finalizes).
|
||||
|
||||
### 7.4 `run_script` claim descriptor
|
||||
|
||||
A `run_script` claim carries a `script` object alongside the usual
|
||||
`endpointcommandid` and `action`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"scriptid": 12,
|
||||
"scriptname": "sfld_autologon",
|
||||
"installpath": "/api/endpoints/scriptbody/12", // fetch with X-API-Key
|
||||
"sha256": "3f9a...", // 64 hex chars, lowercase
|
||||
"runcontext": "system", // "system" | "user"
|
||||
"workingdirectory": "C:\\Tools", // "" = agent staging folder
|
||||
"errormode": "default" // "default" | "strict"
|
||||
}
|
||||
```
|
||||
|
||||
The agent MUST download `installpath`, verify the SHA-256 of the received bytes
|
||||
against `sha256`, and refuse to run on mismatch. The body never travels on the
|
||||
command channel so a queued command cannot be used to smuggle code past review.
|
||||
|
||||
`workingdirectory` is the folder the script runs from. Empty means the agent's
|
||||
staging directory, which is correct for a self-contained script and wrong for
|
||||
one invoking a separately-installed tool by relative path. A pinned folder that
|
||||
does not exist MUST fail the command rather than falling back, so the script
|
||||
never runs somewhere unintended.
|
||||
|
||||
`errormode` selects how the run is judged:
|
||||
|
||||
- `default` - PowerShell's normal behaviour. Most errors are non-terminating:
|
||||
the script writes the error, continues, and still exits zero.
|
||||
- `strict` - the agent prepends `$ErrorActionPreference = 'Stop'` so the first
|
||||
error terminates the run and the exit code reflects reality.
|
||||
|
||||
`default` is the default precisely BECAUSE it is permissive. Every script
|
||||
authored before this field existed relies on that behaviour, and some rely on it
|
||||
deliberately - tolerating a failing cleanup, probing with a cmdlet that errors
|
||||
when a thing is absent, looping past a bad item. Promoting `strict` globally
|
||||
would make those bodies abort partway and leave endpoints half-configured, so it
|
||||
is opt-in per script.
|
||||
|
||||
Two consequences the agent MUST honour:
|
||||
|
||||
- The strict prologue is applied AFTER digest verification, so the hash still
|
||||
validates the bytes the server actually delivered.
|
||||
- Because the prologue adds a line, an error reported on line N of a strict run
|
||||
refers to line N-1 of the stored body. Surfaces showing the body should say so.
|
||||
|
||||
An agent that does not recognise an `errormode` value MUST treat it as
|
||||
`default`. The server always sends the field explicitly, even at the default, so
|
||||
an agent that received it and ignored it stays distinguishable from one that
|
||||
never saw it.
|
||||
|
||||
#### Script output
|
||||
|
||||
Script stdout/stderr is ALWAYS written to the local agent log on the endpoint.
|
||||
It is returned to the server in `detail.output` only when the command FAILED.
|
||||
Routine output stays on the machine that produced it: shipping every successful
|
||||
run back would put arbitrary script output, including whatever an author printed
|
||||
while debugging, into the server's command history for no diagnostic gain.
|
||||
Output is truncated to 8000 characters and flagged when truncated, because an
|
||||
oversized payload rejected by the server returns nothing at all.
|
||||
|
||||
Timed-out runs do not carry output: the script is still running and still holds
|
||||
the capture file open, so anything read at that moment would be misleading.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user