Import the wilm endpoint-monitor and channel-distribution work

Everything on origin/wilm that is not on the published main, applied as one
change because the published history has rewritten hashes: per-commit
cherry-pick conflicts against our main even though the squashed diff applies
clean.

Contents: endpoint monitor catalog + policy attachment + warn-only drift
reconciler, operator issue reports from the tray, agent launch-path allowlist,
and the GHCR channel-image distribution path for dev workstations.

NOT READY FOR main. Open items:
  - 10 new routes are absent from docs/api-inventory.json, so
    test_api_inventory_parity fails. That gate SKIPS on the published mirror
    (the test ships, its data file is scrubbed), which is why wilm never saw it.
  - dev-bootstrap.ps1 and Start-ShopDBDev.ps1 hardcode the ge-aero GitHub org.
  - plugins/endpoints/plugin.py carries a whitespace-only re-indent.

Scope: product
This commit is contained in:
cproudlock
2026-09-02 10:05:44 -04:00
parent 1480b30e8c
commit 27b2817fd7
76 changed files with 5930 additions and 173 deletions

View File

@@ -167,19 +167,19 @@ class EndpointsPlugin(BasePlugin):
# running a staged script or pushing an installer is a strictly
# heavier grant than a routine service/process toggle.
('endpoints.deploy',
'Run staged scripts and push application installers to endpoints', 'endpoints'),
# Packaging the agent is a heavier grant than deploying to one, and
# it is not the same job either: the generated ZIP carries a live
# collector key stamped into its install script, so whoever holds
# this can mint a distributable artefact containing the fleet-wide
# command-channel credential. It was require_role('admin'), which
# this codebase otherwise reserves for the few genuinely
# role-shaped checks - and a role cannot be granted to one person
# for one job the way a permission can.
('endpoints.agentpackage',
'Generate and download an agent or adoption-tool package '
'(contains a live collector key)', 'endpoints'),
# Collector service scope for the endpoint agent. A managed token
'Run staged scripts and push application installers to endpoints', 'endpoints'),
# Packaging the agent is a heavier grant than deploying to one, and
# it is not the same job either: the generated ZIP carries a live
# collector key stamped into its install script, so whoever holds
# this can mint a distributable artefact containing the fleet-wide
# command-channel credential. It was require_role('admin'), which
# this codebase otherwise reserves for the few genuinely
# role-shaped checks - and a role cannot be granted to one person
# for one job the way a permission can.
('endpoints.agentpackage',
'Generate and download an agent or adoption-tool package '
'(contains a live collector key)', 'endpoints'),
# Collector service scope for the endpoint agent. A managed token
# scoped to ONLY this authorizes the endpoints collector and nothing
# else, so rotating/revoking it stops endpoint-agent traffic without
# touching computers/geenforce collectors. Minted from the settings
@@ -486,6 +486,23 @@ class EndpointsPlugin(BasePlugin):
# the installed-software inventory
'group': 'software',
},
{
# Operator-submitted issue reports: a free-text note and an
# optional screenshot the operator captured from the tray. Sits
# in the Monitors tab beside the other watch-and-observe panels.
# A custom render carries the thumbnail grid and the expanding
# lightbox with left/right cycling the generic modes cannot.
'id': 'endpoints-operator-reports',
'title': 'Operator Reports',
'assettypes': ['computer'],
'endpoint': '/api/endpoints/panel/{assetid}/operatorreports',
'render': 'operatorreports',
'imagebase': '/api/endpoints/operatorreports',
'collapsible': True,
'empty': 'No operator reports for this endpoint.',
'position': 18,
'group': 'monitors',
},
]
def get_dashboard_widgets(self) -> List[Dict]:
@@ -802,6 +819,26 @@ class EndpointsPlugin(BasePlugin):
state.lastreportedat = datetime.utcnow()
# Warn-only reconciler: on this check-in, compare the monitors site
# policy expects to be running against what the snapshot reports, and
# cache the drift on the state in THIS transaction (same ledger commit as
# servicesjson). No start/stop is issued - drift is surfaced for an
# operator to act on. Reads the freshly reported services when present,
# else the last-known snapshot, so a report that omits services still
# reconciles against what we last saw. Fail-open: a reconciler error must
# never fail an otherwise-good check-in, so drift is left unchanged.
try:
if 'services' in payload:
reported_services = payload.get('services') or []
else:
reported_services = json.loads(state.servicesjson or '[]')
from .services.reconciler import compute_drift
state.driftjson = _as_json(compute_drift(assetid, reported_services))
except Exception:
logger.exception(
'reconciler failed for asset %s; leaving drift unchanged',
assetid)
# Log any keys the agent sent that the ingest path does not consume yet.
# New agent telemetry (disks, bios, memory, ...) lands here first: it is
# not persisted until the server grows columns for it, but logging the