Files
shopdb-flask/migrations/versions/7d48_script_workingdirectory.py
benjamin.baker d6163f54a4 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.
2026-09-09 11:36:42 -04:00

42 lines
1.4 KiB
Python

"""Add scripts workingdirectory
Backs the optional "run in this folder" field on a library script. The agent
otherwise runs a delivered script from its own staging directory, which is
correct for a self-contained script and wrong for one that shells out to a
corporate tool already installed on the endpoint (Sysinternals and similar,
delivered by Intune) and referenced by relative path.
NULLABLE with no default, so every script row that existed before this
migration stays valid and keeps today's behaviour exactly: empty means the
staging directory. The path is validated in shopdb/core/api/scripts.py where a
rejection can explain itself, not in the schema where it would only produce an
IntegrityError.
This column grants no capability. A script body is already arbitrary PowerShell
run as SYSTEM and can change its own directory; the boundary is the
endpoints.runscript grant and the digest-pinned library.
Revision ID: 7d48_script_workingdir
Revises: 7d47_endpoints_operatorreports
Create Date: 2026-09-09
"""
from alembic import op
import sqlalchemy as sa
revision = '7d48_script_workingdir'
down_revision = '7d47_endpoints_operatorreports'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('scripts', sa.Column(
'workingdirectory', sa.String(length=255), nullable=True,
comment='Folder to run the script in; empty means the agent staging dir'))
def downgrade():
op.drop_column('scripts', 'workingdirectory')