Files
truf-server/app/host_agent_reconcile.py
2026-09-30 20:30:56 +03:00

95 lines
3.1 KiB
Python

"""Bounded runtime reconciliation of fixed host-agent result evidence."""
import json
import os
from pathlib import Path
import stat
from host_agent_state import MAX_STATE_BYTES
from runtime_security import reject_reparse_components
HOST_RESULT_DIRECTORY = Path('/data/host-agent-results')
HOST_ROOT_UID = 0
HOST_RUNTIME_GID = 10001
class HostResultError(RuntimeError):
pass
def fixed_result_directory_is_safe():
try:
reject_reparse_components(HOST_RESULT_DIRECTORY)
details = os.stat(HOST_RESULT_DIRECTORY, follow_symlinks=False)
return (
stat.S_ISDIR(details.st_mode)
and details.st_uid == HOST_ROOT_UID
and details.st_gid == HOST_RUNTIME_GID
and stat.S_IMODE(details.st_mode) == 0o750
)
except Exception:
return False
def _read_result(operation_id):
path = HOST_RESULT_DIRECTORY / f'{operation_id}.json'
descriptor = None
try:
flags = os.O_RDONLY | getattr(os, 'O_CLOEXEC', 0) | getattr(os, 'O_NOFOLLOW', 0)
descriptor = os.open(path, flags)
before = os.fstat(descriptor)
if (
not stat.S_ISREG(before.st_mode)
or before.st_nlink != 1
or before.st_uid != HOST_ROOT_UID
or before.st_gid != HOST_RUNTIME_GID
or stat.S_IMODE(before.st_mode) != 0o640
):
raise HostResultError('host result metadata is invalid')
with os.fdopen(descriptor, 'rb') as handle:
descriptor = None
payload = handle.read(MAX_STATE_BYTES + 1)
after = os.fstat(handle.fileno())
current = os.stat(path, follow_symlinks=False)
identity = lambda item: (
item.st_dev, item.st_ino, item.st_size,
getattr(item, 'st_mtime_ns', None), getattr(item, 'st_ctime_ns', None),
)
if (
len(payload) > MAX_STATE_BYTES
or identity(before) != identity(after)
or identity(after) != identity(current)
):
raise HostResultError('host result changed during read')
value = json.loads(payload.decode('ascii'))
canonical = json.dumps(
value, sort_keys=True, separators=(',', ':'), ensure_ascii=True,
allow_nan=False,
).encode('ascii')
if canonical != payload:
raise HostResultError('host result is not canonical')
return payload
except FileNotFoundError:
return None
except HostResultError:
raise
except Exception:
raise HostResultError('host result is invalid') from None
finally:
if descriptor is not None:
os.close(descriptor)
def reconcile_pending_host_results(database, *, limit=32):
if not fixed_result_directory_is_safe():
return 0
reconciled = 0
for operation in database.pending_runtime_agent_operations(limit=limit):
envelope = _read_result(operation['operation_id'])
if envelope is None:
continue
if database.reconcile_runtime_operation_result(envelope) is not None:
reconciled += 1
return reconciled