159 lines
6.4 KiB
Python
159 lines
6.4 KiB
Python
"""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
|