Add log collection, script output capture, and timestamp fixes

Log Collector: a new collect_log action reads a plaintext log file from a
known path and returns its tail for viewing and download. Rides the existing
endpoints.updateapp grant rather than inventing one, since collecting a log is
the same trust level as pushing an application. Collected logs land in their
own table with per-asset retention of 20 instead of overloading the command
audit row, and the agent refuses UNC, traversal, wildcards, directories and
non-text content independently of the server check.

collect_log is POLICY_EXEMPT. Every existing Configuration Policy predates the
action and so cannot name it, and the intersection in ALLOWED_ACTIONS_FOR drops
anything unnamed - so every policied endpoint refused log collection outright,
including for admins, who saw a permission-shaped error for a grant they held.
Exemption from policy is not exemption from permission: the elevated grant
still applies, and a regression test asserts both halves so a future edit
cannot quietly loosen the grant while "fixing" the policy gate.

Script output capture: successful runs can return output, gated per script by
captureoutput. Failures always returned it; success never did, which left a
green badge as the only evidence a run produced.

Timestamps: Activity and Operator Reports rendered date-only because the shared
formatter's 'date' branch drops the clock time. Run Scripts additionally
labels its timestamp, since a bare time meant "completed" on a terminal row but
"queued" on an in-flight one - indistinguishable, and the wrong reading.

Agent 1.16.0. Minor: collect_log is a new action name, so an older agent asked
for it fails closed on the unknown-action default.
This commit is contained in:
benjamin.baker
2026-09-09 19:23:43 -04:00
parent 494b837bae
commit b49fb60c0c
19 changed files with 2207 additions and 21 deletions

View File

@@ -110,6 +110,24 @@ COMMAND_ALLOWLIST = {
'PCDetail.vue).',
'targetkind': 'script',
},
'collect_log': {
'label': 'Collect log file',
'description': 'Read a plaintext log file from a known path on the '
'endpoint and return its contents to ShopDB for '
'viewing and download. Read-only: the agent opens the '
'file for reading with sharing permitted (so a log a '
'running application holds open can still be read), '
'never writes, and never deletes. Only the TAIL of a '
'file is returned, and only for plaintext extensions - '
'a binary or oversized file is refused rather than '
'mangled. Gated on endpoints.updateapp because the '
'reason to collect a log is almost always to diagnose '
'an application push, so it belongs with app lifecycle '
'rights rather than as a separate grant. Note this '
'still reads a caller-supplied path as SYSTEM: it is '
'bounded to text files, but it is not a sandbox.',
'targetkind': 'logpath',
},
'update_app': {
'label': 'Install/update application',
'description': 'Push a ShopDB-hosted installer for an application (by app '
@@ -192,11 +210,18 @@ COMMAND_ALLOWLIST = {
# update_app - runs a pinned installer
# run_script - arbitrary code as SYSTEM unless the descriptor pins a
# user context
#
# collect_log rides endpoints.updateapp rather than taking a grant of its own.
# The reason to read a log is nearly always to find out why an app push behaved
# the way it did, so the right to install an application and the right to read
# that application's log are the same job. It is read-only and strictly weaker
# than the installer push the same permission already allows.
ELEVATED_ACTION_PERMISSIONS = {
'system_restart': 'endpoints.restart',
'scheduledtask_run': 'endpoints.corptools',
'run_script': 'endpoints.runscript',
'update_app': 'endpoints.updateapp',
'collect_log': 'endpoints.updateapp',
}
@@ -339,6 +364,19 @@ POLICY_EXEMPT_ACTIONS = frozenset({
# policy is non-empty - a denial no operator can act on, because the fix
# lives in a policy that was never about this action.
'collect_now',
# Read-only diagnostic retrieval. Policy describes how a machine should be
# shaped - which monitors run, which service primitives are reachable - and
# reading a log file shapes nothing. An existing policy was authored before
# this action existed and so can never mention it, which under the
# intersection means every endpoint carrying any policy at all refuses log
# collection, including for an admin. That denial is also unactionable in
# the way that matters: logs are what an operator reaches for when an
# endpoint is already misbehaving, so gating them behind editing that same
# endpoint's policy withholds the evidence exactly when it is needed.
# Exemption from POLICY is not exemption from PERMISSION - collect_log
# still requires endpoints.updateapp via ELEVATED_ACTION_PERMISSIONS, and
# the agent still refuses UNC, traversal, directories and non-text files.
'collect_log',
})