Merge origin/main (West Jeff) into wilm
Bring in the natural-key import fix, Metrics->Monitors rename, PC detail tabbing and panel groups, guest search grants, and the 2026-08-30 security fixes. Reconcile endpoint packaging/installer, migrations, contract version, and tests; keep the delivery uninstaller and 6-step installer alongside main's permission-based RBAC and pin-drift protection.
This commit is contained in:
@@ -78,6 +78,20 @@ def _as_json(value):
|
||||
return json.dumps(value, separators=(',', ':'), sort_keys=True)
|
||||
|
||||
|
||||
def _clamp(value, length):
|
||||
"""A string bounded to its column width, or None.
|
||||
|
||||
Ingest writes whatever an endpoint reports. A registry value longer than its
|
||||
column is not a data-quality nuisance on MySQL: in strict mode it raises
|
||||
Data too long and fails the whole check-in transaction, so one machine with
|
||||
a malformed Uninstall entry stops reporting telemetry and events entirely.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value).strip()
|
||||
return text[:length] or None
|
||||
|
||||
|
||||
class EndpointsPlugin(BasePlugin):
|
||||
"""Endpoint RMM telemetry ingest plus read/dashboard surface."""
|
||||
|
||||
@@ -131,10 +145,11 @@ class EndpointsPlugin(BasePlugin):
|
||||
# including every logged-in username on the floor, because the
|
||||
# panels and the list were gated on the same string.
|
||||
#
|
||||
# It is deliberately NOT named '.view'. guest_grantable_permissions
|
||||
# opens any '.view' or '.read' permission to the guest role, so this
|
||||
# name is what keeps the fleet roster off the public surface while
|
||||
# per-asset telemetry can still be granted.
|
||||
# The name avoids '.view' for readability, but the NAME no longer
|
||||
# decides anything: guest_grantable_permissions is a declared list
|
||||
# now, and this plugin blesses endpoints.view alone below. Adding
|
||||
# endpoints.fleet there would be what opens the roster, and the
|
||||
# docstring on that hook says why not to.
|
||||
('endpoints.fleet',
|
||||
'View the fleet-wide endpoint list and dashboards', 'endpoints'),
|
||||
# Issue an allowlisted command to an endpoint agent (for example the
|
||||
@@ -152,8 +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'),
|
||||
# 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
|
||||
@@ -162,6 +188,21 @@ class EndpointsPlugin(BasePlugin):
|
||||
'Submit endpoint agent telemetry (fleet reporting)', 'collector'),
|
||||
]
|
||||
|
||||
def get_guest_grantable_permissions(self) -> List[str]:
|
||||
"""Declared because a site chose it: live status on a PC page.
|
||||
|
||||
endpoints.view only. NOT endpoints.fleet - that is the roster, and the
|
||||
whole reason the two were split.
|
||||
|
||||
KNOWN GAP, tracked separately: the per-asset panels this opens are
|
||||
keyed on a small integer asset id and the status panel emits the
|
||||
logged-in username, so walking ids reconstructs most of the roster the
|
||||
split exists to protect. Bounding or rate-limiting the per-asset panels
|
||||
is the fix. Until then a site enabling guest access should know the
|
||||
split is a speed bump here, not a wall.
|
||||
"""
|
||||
return ['endpoints.view']
|
||||
|
||||
def get_settings_defaults(self) -> List[Dict]:
|
||||
"""Settings owned by the endpoints agent-key kill switch.
|
||||
|
||||
@@ -310,6 +351,9 @@ class EndpointsPlugin(BasePlugin):
|
||||
'permission': 'endpoints.command',
|
||||
'empty': 'No monitored services reported.',
|
||||
'position': 12,
|
||||
# Service state is what is being watched; the Start/Stop
|
||||
# controls belong beside it, not in a tab of their own.
|
||||
'group': 'monitors',
|
||||
},
|
||||
{
|
||||
'id': 'endpoints-tracked-processes',
|
||||
@@ -348,6 +392,8 @@ class EndpointsPlugin(BasePlugin):
|
||||
'permission': 'endpoints.command',
|
||||
'empty': 'No monitored processes reported.',
|
||||
'position': 13,
|
||||
# observed process state, not something you act on here
|
||||
'group': 'monitors',
|
||||
},
|
||||
{
|
||||
# One feed instead of two cards. Events and commands answer the
|
||||
@@ -391,6 +437,8 @@ class EndpointsPlugin(BasePlugin):
|
||||
},
|
||||
'empty': 'Nothing recorded for this endpoint yet.',
|
||||
'position': 20,
|
||||
# the event and command feed IS the activity tab
|
||||
'group': 'activity',
|
||||
},
|
||||
{
|
||||
'id': 'endpoints-interfaces',
|
||||
@@ -410,6 +458,33 @@ class EndpointsPlugin(BasePlugin):
|
||||
],
|
||||
'empty': 'No network interfaces reported.',
|
||||
'position': 16,
|
||||
# adapter state is watched, not something you act on
|
||||
'group': 'monitors',
|
||||
},
|
||||
{
|
||||
# Installed-software (SBOM) inventory. Raw discovered list for
|
||||
# now: no managed-application linkage is surfaced yet.
|
||||
#
|
||||
# NOT collapsible. It used to be, and started collapsed, because
|
||||
# it was low-priority reference data sitting near the bottom of
|
||||
# one long page. It is now the Software tab's whole reason to
|
||||
# exist, and somebody who clicked that tab has already said what
|
||||
# they came for - making them click again to see it is a step
|
||||
# that asks a question already answered.
|
||||
'id': 'endpoints-applications',
|
||||
'title': 'Installed software',
|
||||
'assettypes': ['computer'],
|
||||
'endpoint': '/api/endpoints/asset/{assetid}/applications',
|
||||
'render': 'table',
|
||||
'columns': [
|
||||
{'key': 'displayname', 'label': 'Name'},
|
||||
{'key': 'displayversion', 'label': 'Version'},
|
||||
{'key': 'publisher', 'label': 'Publisher'},
|
||||
],
|
||||
'empty': 'No installed software reported.',
|
||||
'position': 30,
|
||||
# the installed-software inventory
|
||||
'group': 'software',
|
||||
},
|
||||
{
|
||||
# Installed-software (SBOM) inventory. Low-priority reference
|
||||
@@ -641,11 +716,18 @@ class EndpointsPlugin(BasePlugin):
|
||||
if not displayname:
|
||||
warnings.append('skipped application with no displayname')
|
||||
continue
|
||||
# TRUNCATE EVERY STRING, not just the name. On MySQL in strict
|
||||
# mode an over-long value raises Data too long and takes down the
|
||||
# WHOLE check-in transaction, so one machine with a malformed
|
||||
# Uninstall registry entry stops reporting entirely - telemetry,
|
||||
# events and all - until somebody uninstalls the offender. SQLite
|
||||
# does not enforce declared lengths, so no test here would ever see
|
||||
# it. displayname was already clamped; these two were not.
|
||||
db.session.add(EndpointApplication(
|
||||
assetid=assetid,
|
||||
displayname=displayname[:255],
|
||||
displayversion=(item.get('displayversion') or None),
|
||||
publisher=(item.get('publisher') or None),
|
||||
displayversion=_clamp(item.get('displayversion'), 128),
|
||||
publisher=_clamp(item.get('publisher'), 255),
|
||||
))
|
||||
stored += 1
|
||||
return stored
|
||||
|
||||
Reference in New Issue
Block a user