Promote the 2026-08-30 review security fixes and supplies ordering to main

This commit is contained in:
2026-08-31 09:24:13 -04:00
parent 6680c3c5de
commit 0e3d5a6226
30 changed files with 991 additions and 98 deletions

View File

@@ -7,6 +7,7 @@ These routes are the human-facing read surface, gated by user JWT + RBAC.
"""
import hashlib
import hmac
import json
import os
import time
@@ -941,6 +942,43 @@ _CLAIM_HOLD_SECONDS = 25
_CLAIM_POLL_INTERVAL = 1.0
def _pin_holds(command, descriptor):
"""Does the descriptor still carry the digest this command was queued for?
True when nothing was pinned (actions that deliver no content), or when the
descriptor is missing entirely - a vanished script or version already sends
the command through with no descriptor so the agent fails closed, and that
path is not a content swap.
"""
if not command.pinnedsha256:
return True
if descriptor is None:
return True
return hmac.compare_digest(
str(descriptor.get('sha256') or ''), command.pinnedsha256)
def _fail_on_pin_drift(command, action):
"""Refuse a claim whose content changed after the operator approved it."""
command.status = EndpointCommand.STATUS_FAILED
command.resultmessage = (
'Refused: the {} content changed after this command was queued'
.format(action))
command.completedat = _naive_utcnow()
db.session.commit()
current_app.logger.warning(
'Endpoint command %s (%s, asset %s) refused: pinned sha256 %s no '
'longer matches the resolved descriptor',
command.endpointcommandid, action, command.assetid,
command.pinnedsha256)
AuditLog.log('refused', 'EndpointCommand',
entityid=command.endpointcommandid,
entityname=action,
details='content digest changed after enqueue')
db.session.commit()
return ('', 204)
def _reap_stale_commands(asset_id):
"""Expire in-flight commands an agent will never finish for this endpoint.
@@ -1234,18 +1272,25 @@ def enqueue_endpoint_command(asset_id: int):
# update_app pins a specific application version; verify it resolves to a
# real installer now so a broken push is rejected at enqueue rather than
# failing opaquely on the endpoint minutes later.
# Digest of the content this push was APPROVED against. Kept on the row so
# the claim path can refuse a descriptor that has changed since; see
# EndpointCommand.pinnedsha256.
pinnedsha256 = None
if action == 'update_app':
_descriptor, resolve_error = _resolve_update_app(target)
descriptor, resolve_error = _resolve_update_app(target)
if resolve_error is not None:
return error_response(ErrorCodes.VALIDATION_ERROR, resolve_error)
pinnedsha256 = (descriptor or {}).get('sha256')
# run_script pins a ShopDB-hosted script; verify it resolves to a real
# uploaded body now so a broken push is rejected at enqueue rather than
# failing opaquely on the endpoint later.
if action == 'run_script':
_script, script_error = _resolve_run_script(target)
script, script_error = _resolve_run_script(target)
if script_error is not None:
return error_response(ErrorCodes.VALIDATION_ERROR, script_error)
pinnedsha256 = (script or {}).get('sha256')
state = EndpointState.query.filter_by(assetid=asset_id).first()
if state is None:
@@ -1277,6 +1322,7 @@ def enqueue_endpoint_command(asset_id: int):
targetkind=targetkind if target else None,
status=EndpointCommand.STATUS_PENDING,
requestedby=getattr(current_user, 'userid', None),
pinnedsha256=pinnedsha256,
)
db.session.add(command)
@@ -1337,6 +1383,8 @@ def claim_next_command():
# fail closed rather than 500 the poll.
if command.action == 'update_app':
descriptor, _resolve_error = _resolve_update_app(command.target)
if not _pin_holds(command, descriptor):
return _fail_on_pin_drift(command, 'update_app')
if descriptor is not None:
claim['app'] = descriptor
# run_script rides a resolved script descriptor: the agent gets a
@@ -1347,6 +1395,8 @@ def claim_next_command():
# agent fail closed rather than 500 the poll.
if command.action == 'run_script':
script_descriptor, _script_error = _resolve_run_script(command.target)
if not _pin_holds(command, script_descriptor):
return _fail_on_pin_drift(command, 'run_script')
if script_descriptor is not None:
claim['script'] = script_descriptor
return success_response(claim)
@@ -1376,6 +1426,33 @@ def report_command_result(command_id: int):
return error_response(ErrorCodes.NOT_FOUND,
f'No command {command_id}', http_code=404)
# An agent may only finalize ITS OWN command. The collector key is shared
# fleet-wide, so without this any agent could post a result for any command
# id - marking an install succeeded on a machine that never ran it, which
# writes a false compliance record. Binding to the claimed hostname is a
# floor, not a fix: the credential still does not prove which endpoint is
# calling, and only a per-endpoint credential closes that.
# Optional because the shipped agent does not send it yet; enforced when it
# does, so an updated fleet gets the check without a flag day.
hostname = (request.args.get('hostname') or '').strip()
if hostname:
caller_asset_id = _resolve_asset_for_hostname(hostname)
if caller_asset_id != command.assetid:
return error_response(
ErrorCodes.FORBIDDEN,
'This command belongs to another endpoint', http_code=403)
# Only a claimed command can be finalized. Needs no agent change, and it
# closes two things the hostname check cannot: reporting on a command that
# was never claimed, and overwriting a terminal result - which would let a
# recorded failure be replaced by a success.
if command.status != EndpointCommand.STATUS_RUNNING:
return error_response(
ErrorCodes.VALIDATION_ERROR,
'Command {} is {}, not running'.format(
command_id, command.status),
http_code=409)
payload = request.get_json(silent=True) or {}
succeeded = bool(payload.get('succeeded'))
command.status = (EndpointCommand.STATUS_SUCCEEDED if succeeded