Merge the wilm work, act on the five-agent review, verify the launch-path policy on Windows
This commit is contained in:
0
plugins/endpoints/services/__init__.py
Normal file
0
plugins/endpoints/services/__init__.py
Normal file
158
plugins/endpoints/services/reconciler.py
Normal file
158
plugins/endpoints/services/reconciler.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""Warn-only monitor reconciler.
|
||||
|
||||
The reconciler answers one question on every agent check-in: for the monitors a
|
||||
site policy (or the shipped bundle) attaches to this endpoint, is each one the
|
||||
endpoint is EXPECTED to be running actually reported as running in the latest
|
||||
snapshot? Every monitor that is expected-running but not observed-running is
|
||||
DRIFT, and drift is surfaced to an operator for MANUAL intervention.
|
||||
|
||||
This is warn-only. The reconciler NEVER starts, stops, or enqueues anything - it
|
||||
compares observed state against expected state and records the difference. Whether
|
||||
to later automate remediation (a keep-alive that starts a process when it drifts)
|
||||
is a separate, deliberate opt-in the product does not ship switched on.
|
||||
|
||||
Two seams are kept deliberately narrow so this stays easy to change:
|
||||
|
||||
monitor_expects_running(monitor)
|
||||
The single place that decides "should this monitor be up?". Today every
|
||||
attached monitor is treated as expected-running (attachment implies intent
|
||||
to run). When an observe-only monitor kind arrives - one that only collects
|
||||
general state and must never be flagged as drift - this is the one function
|
||||
that branches on it. Nothing else in the reconciler encodes that policy.
|
||||
|
||||
compute_drift(asset_id, reported_services)
|
||||
Pure given its inputs: it resolves the attached monitors and diffs them
|
||||
against the reported running set. It reads no request state and writes no
|
||||
rows, so the ingest path can call it inside its own transaction and tests
|
||||
can call it directly.
|
||||
"""
|
||||
|
||||
# The running-state vocabulary a watched process/service can report. A watched
|
||||
# service reports a STATE STRING, not a boolean, and the vocabulary varies by
|
||||
# source, so what counts as running is a single rule rather than two.
|
||||
#
|
||||
# It lives here, and api/routes.py imports it, because the panel and this
|
||||
# reconciler MUST agree: a service the panel paints green that this flags as
|
||||
# drift is a contradiction shown to the same person on the same page. It was
|
||||
# two copies with a comment asking a human to keep them in lockstep, which is
|
||||
# not a mechanism.
|
||||
RUNNING_STATES = ('running', 'started', 'up', 'ok', 'active')
|
||||
|
||||
|
||||
def monitor_expects_running(monitor):
|
||||
"""Whether an attached monitor is expected to be running on an endpoint.
|
||||
|
||||
The seam for the "should be up" decision. Today attachment implies intent to
|
||||
run, so every active monitor returns True. A future observe-only monitor kind
|
||||
(general state collection, never keep-alive) branches HERE and returns False
|
||||
so it is never reported as drift - no other part of the reconciler needs to
|
||||
change.
|
||||
"""
|
||||
return bool(getattr(monitor, 'isactive', True))
|
||||
|
||||
|
||||
def _monitor_identity(monitor):
|
||||
"""The (kind, osname) a monitor watches, or None when it has no identity.
|
||||
|
||||
Mirrors how the services panel reads a monitor's OS identity: a process
|
||||
monitor is keyed by its processname, a service monitor by its servicename.
|
||||
"""
|
||||
kind = (getattr(monitor, 'kind', '') or '').lower() or None
|
||||
if kind == 'process':
|
||||
osname = getattr(monitor, 'processname', None)
|
||||
elif kind == 'service':
|
||||
osname = getattr(monitor, 'servicename', None)
|
||||
else:
|
||||
osname = None
|
||||
osname = (osname or '').strip()
|
||||
if not kind or not osname:
|
||||
return None
|
||||
return kind, osname
|
||||
|
||||
|
||||
def _reported_running_keys(reported_services):
|
||||
"""Set of (kind, lowercased-name) the snapshot reports as RUNNING.
|
||||
|
||||
Tolerant of the two shapes the agent snapshot uses: dict entries with
|
||||
name/service and state/status (and an optional kind), or bare strings.
|
||||
Only running entries land in the set; a stopped or unknown entry is absent,
|
||||
which is what makes an expected-running monitor read as drift.
|
||||
"""
|
||||
running = set()
|
||||
for entry in reported_services or ():
|
||||
if isinstance(entry, dict):
|
||||
name = entry.get('name') or entry.get('service') or ''
|
||||
raw = (entry.get('state') or entry.get('status') or '').strip().lower()
|
||||
kind = (entry.get('kind') or '').lower() or None
|
||||
else:
|
||||
name, raw, kind = str(entry), '', None
|
||||
name = name.strip().lower()
|
||||
if not name or raw not in RUNNING_STATES:
|
||||
continue
|
||||
running.add((kind, name))
|
||||
return running
|
||||
|
||||
|
||||
def _is_running(kind, osname, running_keys):
|
||||
"""Whether a monitor's identity appears running in the snapshot.
|
||||
|
||||
A snapshot entry may omit kind (older agents, or a plain string list), so a
|
||||
match on name alone under an unknown kind still counts as running - the
|
||||
reconciler must not invent drift just because the agent did not label a row.
|
||||
"""
|
||||
needle = osname.strip().lower()
|
||||
if (kind, needle) in running_keys:
|
||||
return True
|
||||
if (None, needle) in running_keys:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def compute_drift(asset_id, reported_services):
|
||||
"""Compute the current drift list for an endpoint. Pure, no writes.
|
||||
|
||||
Resolves the monitors attached to the asset's PC type (shipped defaults plus
|
||||
site policy), keeps only those that are expected-running, and returns one
|
||||
entry per monitor whose identity is not reported as running. Returns an empty
|
||||
list when nothing is attached or nothing has drifted, so the caller can store
|
||||
it as "computed, no drift" distinct from a null "never computed".
|
||||
|
||||
Isolated from Flask request state: it reads the monitor catalog and the
|
||||
passed-in snapshot only, so the ingest path calls it inside its own
|
||||
transaction and tests call it directly.
|
||||
"""
|
||||
try:
|
||||
from plugins.endpoints.api.routes import _resolved_monitors_for_asset
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
monitors = _resolved_monitors_for_asset(asset_id) or []
|
||||
if not monitors:
|
||||
return []
|
||||
|
||||
running_keys = _reported_running_keys(reported_services)
|
||||
|
||||
drift = []
|
||||
seen = set()
|
||||
for monitor in monitors:
|
||||
if not monitor_expects_running(monitor):
|
||||
continue
|
||||
identity = _monitor_identity(monitor)
|
||||
if identity is None:
|
||||
continue
|
||||
kind, osname = identity
|
||||
key = (kind, osname.lower())
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
if _is_running(kind, osname, running_keys):
|
||||
continue
|
||||
drift.append({
|
||||
'monitorid': getattr(monitor, 'endpointmonitorid', None),
|
||||
'kind': kind,
|
||||
'name': osname,
|
||||
'displayname': getattr(monitor, 'displayname', None) or osname,
|
||||
'expected': 'running',
|
||||
'observed': 'notrunning',
|
||||
})
|
||||
return drift
|
||||
Reference in New Issue
Block a user