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

@@ -6,6 +6,8 @@ body never lives on the endpoint ahead of time and never crosses the command
wire; the agent downloads it with its collector credential and verifies sha256.
"""
import sqlalchemy as sa
from shopdb.extensions import db
from .base import BaseModel
@@ -69,6 +71,17 @@ class Script(BaseModel):
# existed reads as the permissive behavior it was authored under.
errormode = db.Column(db.String(16), default=ERRORMODE_DEFAULT, nullable=False,
server_default=ERRORMODE_DEFAULT)
# Whether a SUCCESSFUL run sends its output back to the server. Failures
# always do: a failed run has to be diagnosable. Success is opt-in because
# script output carries whatever the body printed - hostnames, paths, a
# token an author echoed while debugging - and a run nobody needs to
# diagnose is not worth that exposure by default. The endpoint log keeps the
# full copy either way, so this only controls what crosses to the server.
#
# Off by default so existing scripts keep the behavior they were authored
# under, same rule as errormode above.
captureoutput = db.Column(db.Boolean, default=False, nullable=False,
server_default=sa.false())
def to_dict(self):
result = super().to_dict()