Let Corporate Tools push the Report IP tool without granting script run

Report IP is an icon on every Public Desktop in the company, so a help desk
caller asking for it to be re-run is asking for something they could already
do themselves at the keyboard. Getting there through ShopDB meant holding
endpoints.runscript, which is the permission that lets someone push ANY script
in the library - the agent self-upgrade included. Handing that over to run a
desktop shortcut is the bundling the permission split was created to take
apart, so the work was simply not being done through the tool.

The allowance is on one named script, never on the run_script action. The
script id rides in the caller-controlled request body, so relaxing the action
would grant the whole library and quietly rebuild the endpoints.deploy bundle.
The enqueue route resolves the target to a real Script row first and matches
the STORED name, because matching what the requester typed would let anyone
name their way past the grant.

Granting on a name is only defensible while the name cannot be made to mean
something else. The body is source-controlled at
plugins/endpoints/api/templates/report-ip-shortcut.ps1, seeded from there,
pinned by SHA-256 on delivery, and the managed fields are re-asserted on every
seed - so an operator cannot flip the row to runcontext=system, deactivate it,
or edit the body and leave the name still pushable.

The template lives under the plugin rather than agent/ because the image copies
plugins/ but not agent/: reading it from agent/ works on a developer checkout
and fails in the deployed container.

The script runs in the signed-in user's session. This is not a preference.
Launched as SYSTEM the tool re-enters the interactive session and the launcher
exits 0 immediately, reporting success having observed nothing - the same
session-0 boundary the collector already documents for mapped drives. User
context fails closed when nobody is logged on, which is the honest answer.

The seed runs in the migrate service alongside the other seeders, so an
existing site picks the script up on the upgrade it already performs.
This commit is contained in:
benjamin.baker
2026-09-16 18:25:48 -04:00
parent 158ee0507e
commit 2ff7b7e906
7 changed files with 448 additions and 7 deletions

View File

@@ -31,7 +31,7 @@ from ..models import (
EndpointState, EndpointEvent, EndpointCommand, EndpointApplication,
EndpointHealthState, OperatorReport, EndpointLogCollection,
)
from ..commands import COMMAND_ALLOWLIST, is_allowed_action, ALLOWED_ACTIONS_FOR, row_actions_for, requires_target, action_targetkind, elevated_permission_for, minimum_agent_version_for, action_minimum_versions, parse_agent_version, agent_meets_minimum, PLACE_POLICY_HOLD, ACTIVE_POLICY_HOLDS, HOLDABLE_ACTIONS_FOR, RELEASE_POLICY_HOLD, ACTIVE_LOCK_FOR, PLACE_ENDPOINT_LOCK, RELEASE_ENDPOINT_LOCK
from ..commands import COMMAND_ALLOWLIST, is_allowed_action, ALLOWED_ACTIONS_FOR, row_actions_for, requires_target, action_targetkind, elevated_permission_for, corptools_may_push_script, minimum_agent_version_for, action_minimum_versions, parse_agent_version, agent_meets_minimum, PLACE_POLICY_HOLD, ACTIVE_POLICY_HOLDS, HOLDABLE_ACTIONS_FOR, RELEASE_POLICY_HOLD, ACTIVE_LOCK_FOR, PLACE_ENDPOINT_LOCK, RELEASE_ENDPOINT_LOCK
from . import agentstore
from . import agentauth
from ..eventcatalog import CRITICAL_EVENT_IDS, event_severity, event_title
@@ -2992,10 +2992,28 @@ def enqueue_endpoint_command(asset_id: int):
# require_permission exists to prevent, reintroduced by checking by hand.
elevated = elevated_permission_for(action)
if elevated and not caller_holds(elevated):
return error_response(
ErrorCodes.FORBIDDEN,
f'Action {action!r} requires the {elevated} permission',
http_code=403)
# One narrow exception: a SHIPPED script whose reviewed body is
# operationally equivalent to something the person could already do
# unaided at the machine (Report IP is an icon on every Public Desktop).
# Those are pushable with endpoints.corptools alone.
#
# The allowance is keyed on the script's STORED name, read off a row we
# resolve here - never on the caller-supplied target. Matching what the
# requester typed would let anyone name their way past the grant, and
# relaxing the run_script ACTION instead would hand every corptools
# holder the whole library (the script id rides in the request body),
# rebuilding the endpoints.deploy bundle the permission split removed.
allowed_by_shipped_script = False
if action == 'run_script' and caller_holds('endpoints.corptools'):
resolved, _resolve_error = _resolve_run_script(target)
if resolved is not None:
allowed_by_shipped_script = corptools_may_push_script(
resolved.get('scriptname'))
if not allowed_by_shipped_script:
return error_response(
ErrorCodes.FORBIDDEN,
f'Action {action!r} requires the {elevated} permission',
http_code=403)
# Break-glass: an admin may push past a Configuration Policy denial, but
# only by asking for it explicitly and saying why. The override is scoped to