Initial server source import
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user