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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user