Commit Graph

8 Commits

Author SHA1 Message Date
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
benjamin.baker
d6bd7b5f54 Fix endpoint user-context collection running in the wrong session
Mapped drives reported Unavailable on endpoints where the operator could
see them fine. The data path was intact; the collection was running under
the wrong token. Mapped drives are per logon token, so an elevated probe
enumerates a different drive table than the interactive user.

Collection:
- Pin the user-context spool path so the probe and service agree on one
  location, and persist/overwrite it instead of deleting on read.
- Refuse to collect while elevated (exit code 2) rather than emitting a
  confident wrong answer, and stamp elevated/username on the spool.
- Reject spools stamped elevated or from an unexpected account on read,
  so an admin run cannot masquerade as valid.
- Log netuse/smbmapping/userlistener counts so each run leaves evidence
  of what it actually saw.

Install and launch context:
- Move collection to a 'ShopDB user-context probe' scheduled task under
  S-1-5-32-545 at RunLevel Limited, the only mechanism that reliably runs
  in the operator's own session. Verify registration and record it in the
  install ledger.
- Stop starting the tray as a child of the elevated installer; explorer.exe
  de-elevates without changing ACCOUNT, so an admin install produced an
  admin tray. The Run key only fires at logon, which left the tray dead on
  a machine already signed in, so add a transient 'ShopDB tray first launch'
  task that starts it once in the existing session and unregisters itself.
- Hide the console window during --collect-usercontext. The agent is a
  console exe; the tray used to mask this with CreateNoWindow, but the
  scheduled task invokes it directly and flashed a window every cycle.

Installer edits are in _install_script() in plugins/endpoints/api/siteagent.py,
which generates the install.ps1 that actually ships. The standalone
agent/installer/install-shopdbagent.ps1 was never shipped once the package
moved to the UI builder and is deleted here to stop the divergence that
caused an earlier fix to land in a file no endpoint ever ran.

Versions: agent 1.10.1, tray 1.3.0. These share the pinned spool path and
must be deployed together.

Pre-existing test failures unrelated to this work (8, identical on a clean
tree): docs publishable/versions, plugin contract symlink scan, import
guard tamper check, security headers upload sandbox, and three endpoint
timestamp tests.
2026-09-05 17:32:03 -04:00
benjamin.baker
86985efcf2 Fix user-context collection, split agent log destinations, widen upload cap
Mapped drives stopped reporting because the user-context path failed silently
in three separate places, and none of them left a trace to read back.

- Probe: launch via -EncodedCommand instead of staging a temp .ps1 and running
  powershell.exe -File. A missing or quarantined script file fails BEFORE any
  statement runs, so the script's own try/catch never sees it and the only
  symptom is exit -196608.
- Spool: pin to the ShopFloor profile. Resolving it through WTSQueryUserToken
  is privileged, endpoint protection blocks it, and on failure the tray
  (writer) and service (reader) silently disagree about the path.
- Spool: consume after a successful parse, so a stopped tray cannot keep
  re-reporting stale mapped drives as current. Unparseable files are left in
  place for the next cycle.
- Logging: user-session components cannot write to Program Files or to the
  SYSTEM-owned ProgramData log directory, and the file sink degrades to a
  silent no-op when it cannot create its directory. Tray and probe now log to
  C:\Users\ShopFloor\ShopDB\Logs, installs transcript to ProgramData, and the
  sink records a SinkProblem instead of swallowing UnauthorizedAccessException.
- Installer: provision the operator spool/log directories with ShopFloor:Modify
  before the tray first runs, and stop the transcript via trap so a throw
  cannot orphan exactly the failed install worth reading.
- Upload cap 64 -> 128 MB: the self-contained tray publishes at ~69 MB and was
  rejected on SIZE, but the size check returns before the PE is parsed, so the
  operator only ever saw a version-detection failure.

AGENT-CONTRACT.md gains a normative section defining the three security
contexts, their log destinations, the fixed spool path and consume-on-read.

Agent 1.8.0 -> 1.8.1, tray 1.1.0 -> 1.1.1 so the deployed build is
identifiable from a log line.
2026-09-05 14:20:07 -04:00
benjamin.baker
9ab1b4f702 Document Phase 2 system telemetry
Records why rebootreasons is not redundant with pendingreboot, why bootinfo carries two facts that must not be collapsed, and why defender is stored unsurfaced.

Also corrects the notes: pending file-rename was already being collected and OR-ed into pendingreboot, so the gap was that the stronger signal was flattened into the weaker one rather than missing. Records that the columns landed as 0015 because 0014 was taken by disk health earlier in this phase.
2026-09-05 06:41:28 -04:00
benjamin.baker
71847d940a Document the disk health field
Records why there are two sources and why they are not reconciled, so the next person does not simplify the ATA SMART path back in and silently blind the fleet on NVMe hardware.

Also notes that diskhealth is per physical device and is not the same list as disks, and that a null predictfailure is unknown rather than healthy.
2026-09-05 06:26:06 -04:00
benjamin.baker
862b290545 Report each Windows event once, and persist the inventory already sent
Curated events were re-read from the newest 25 entries per channel on every
collect, and the server appended whatever arrived. One real event was
therefore stored again on every cadence tick for as long as it stayed in
that window, so event counts measured how long a box had been up rather
than what had happened to it.

The agent now keeps a per-channel watermark of the last Windows event
record id it reported, persisted under ProgramData so a service restart
does not replay history, and sends eventrecordid with each event. The
server dedupes on (assetid, eventchannel, eventrecordid) behind a unique
index, so a replay from an older agent cannot double-store either.

Six inventory keys the agent had been sending all along - bios, disks,
domain, memory, osinfo, processor - were parsed, logged as unpersisted,
and dropped. They now have columns and are written on check-in, along
with user context, pending reboot, and the cpupercent/memorypercent/
idleseconds fields that were declared in the contract but always null.

Session-scoped data cannot be read from the agent's LocalSystem session 0
at all: mapped drives and user-owned listeners do not exist there. A probe
runs in the interactive user's session and hands results back through a
spool file. It is not yet producing output on a live endpoint; the column
and panel are in place and read empty until that is fixed.

Also bounds the user-context script wait. It was WaitForSingleObject with
INFINITE, so a hung script pinned the command worker permanently.
2026-09-04 19:09:12 -04:00
benjamin.baker
51ad18ca81 report and cache the endpoint tray version
The agent already reported agentversion but the interactive tray app, which ships and updates independently, had no reported version. The agent now reads the tray exe file version and reports it as trayversion; EndpointState caches it (migration 0011) and the Fleet card surfaces it beside Agent. The tray csproj gains an explicit Version so the file version is stamped. Groundwork for per-PC version surfacing and push-update targeting.
2026-09-02 19:04:02 -04:00
benjamin.baker
01734e5449 harden CSV import reconciliation and preserve saved model selections
Default fuzzy reconcile threshold to 0 and refuse merges when digit runs differ so numbered labels like Bay 311 never bind to Bay 310. Keep edit forms from clearing saved model fields when category filtering hides untyped rows. Includes in-progress computers policy and endpoints command feature work.
2026-08-28 09:29:09 -04:00