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:
@@ -112,6 +112,27 @@ def _validate_errormode(value):
|
||||
return value, None
|
||||
|
||||
|
||||
def _validate_captureoutput(value):
|
||||
"""Normalize the "send output back on success" flag. Returns (value, error).
|
||||
|
||||
Absent means False: a script authored before this field existed never
|
||||
returned its successful output to the server, and reading an omitted field
|
||||
as True would start persisting output from bodies nobody reviewed with that
|
||||
question in mind. Strings are accepted because the field arrives from a
|
||||
form control that may send 'true'/'false' rather than a JSON boolean.
|
||||
"""
|
||||
if value is None:
|
||||
return False, None
|
||||
if isinstance(value, bool):
|
||||
return value, None
|
||||
normalized = str(value).strip().lower()
|
||||
if normalized in ('true', '1', 'yes'):
|
||||
return True, None
|
||||
if normalized in ('false', '0', 'no', ''):
|
||||
return False, None
|
||||
return None, 'captureoutput must be true or false'
|
||||
|
||||
|
||||
def _validate_workingdirectory(value):
|
||||
"""Normalize the optional folder a script runs in. Returns (value, error).
|
||||
|
||||
@@ -176,6 +197,9 @@ def create_script():
|
||||
if err:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, err)
|
||||
errormode, err = _validate_errormode(data.get('errormode'))
|
||||
if err:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, err)
|
||||
captureoutput, err = _validate_captureoutput(data.get('captureoutput'))
|
||||
if err:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, err)
|
||||
|
||||
@@ -185,6 +209,7 @@ def create_script():
|
||||
runcontext=runcontext,
|
||||
workingdirectory=workingdirectory,
|
||||
errormode=errormode,
|
||||
captureoutput=captureoutput,
|
||||
)
|
||||
db.session.add(script)
|
||||
db.session.commit()
|
||||
@@ -192,7 +217,8 @@ def create_script():
|
||||
entityname=name,
|
||||
changes={'runcontext': {'new': runcontext},
|
||||
'workingdirectory': {'new': workingdirectory},
|
||||
'errormode': {'new': errormode}})
|
||||
'errormode': {'new': errormode},
|
||||
'captureoutput': {'new': captureoutput}})
|
||||
db.session.commit()
|
||||
return success_response(script.to_dict(), message='Script created', http_code=201)
|
||||
|
||||
@@ -233,6 +259,18 @@ def update_script(script_id: int):
|
||||
if err:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, err)
|
||||
script.errormode = errormode
|
||||
# Audited on change: this decides whether a successful run's output is
|
||||
# persisted server-side, so who turned it on is worth being able to answer.
|
||||
if 'captureoutput' in data:
|
||||
captureoutput, err = _validate_captureoutput(data.get('captureoutput'))
|
||||
if err:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, err)
|
||||
if captureoutput != script.captureoutput:
|
||||
AuditLog.log('updated', 'Script', entityid=script.scriptid,
|
||||
entityname=script.scriptname,
|
||||
changes={'captureoutput': {'old': script.captureoutput,
|
||||
'new': captureoutput}})
|
||||
script.captureoutput = captureoutput
|
||||
if 'isactive' in data:
|
||||
script.isactive = bool(data.get('isactive'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user