95 lines
3.1 KiB
Python
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
|