Add endpoint monitor catalog and warn-only drift reconciler

Operators need to see when a machine's running services or processes fall outside the desired state defined by policy. This adds a reusable monitor catalog (shipped read-only rows plus user-built local rows), computes drift server-side on agent check-in, and surfaces a warning on the endpoint panels. Remediation stays manual for now; automation can follow once demand is proven. Includes migrations 0006-0008 and the monitors API client.
This commit is contained in:
benjamin.baker
2026-09-01 15:44:03 -04:00
parent 1a1bf0640f
commit 42db980ca9
23 changed files with 2054 additions and 51 deletions

View File

@@ -185,6 +185,36 @@ def requires_target(action):
return action_targetkind(action) is not None
# Monitor attachment as a policy action. A monitor definition is bound to a PC
# type by carrying a ConfigurationPolicyAction whose name is 'monitor:<id>'.
# Distinct names let one policy attach several monitors and fold naturally
# (first-match-wins per name), and they are validated in the policy layer but
# never enqueued as commands because ALLOWED_ACTIONS_FOR intersects with
# COMMAND_ALLOWLIST, which carries no monitor: entries.
MONITOR_ACTION_PREFIX = 'monitor:'
def monitor_action_name(endpointmonitorid):
"""Build the policy action name that attaches a monitor definition."""
return f'{MONITOR_ACTION_PREFIX}{endpointmonitorid}'
def is_monitor_action(action):
"""True when action names a monitor attachment (monitor:<id>)."""
return isinstance(action, str) and action.startswith(MONITOR_ACTION_PREFIX)
def monitor_id_from_action(action):
"""The monitor id an action attaches, or None when it is not a monitor action."""
if not is_monitor_action(action):
return None
raw = action[len(MONITOR_ACTION_PREFIX):]
try:
return int(raw)
except (TypeError, ValueError):
return None
def ALLOWED_ACTIONS_FOR(pctype=None, assetid=None):
"""The action names an endpoint may run.
@@ -211,6 +241,44 @@ def ALLOWED_ACTIONS_FOR(pctype=None, assetid=None):
# behavior). This is intentionally permissive; deny-by-default would be
# a separate, explicit setting.
return base
# Attaching a monitor expresses intent to watch a process/service AND to
# control it: the panel renders that row's Start/Stop/Status buttons, so the
# gate must permit the matching generic primitives or every click 409s
# "not permitted by policy". Expand each monitor:<id> to its kind's control
# actions before intersecting.
allowed = allowed | _monitor_control_actions(allowed)
# Never widen past the code-level allowlist: policy can only gate within it.
return base & allowed
def _monitor_control_actions(action_names):
"""Control primitives granted by the monitor: attachments in a policy.
A process monitor grants process_start/stop/status; a service monitor grants
service_start/stop/status. The monitor's kind is read from the catalog, so a
definition's kind is the single source of truth for what its row can do.
"""
ids = set()
for name in action_names:
if isinstance(name, str) and name.startswith(MONITOR_ACTION_PREFIX):
monitor_id = monitor_id_from_action(name)
if monitor_id is not None:
ids.add(monitor_id)
if not ids:
return set()
try:
from .models import EndpointMonitor
except Exception:
return set()
granted = set()
for monitor in EndpointMonitor.query.filter(
EndpointMonitor.endpointmonitorid.in_(ids)).all():
kind = (monitor.kind or '').lower()
if kind == 'process':
granted |= {'process_start', 'process_stop', 'process_status'}
elif kind == 'service':
granted |= {'service_start', 'service_stop', 'service_status'}
return granted