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:
@@ -238,6 +238,44 @@ def elevated_permission_for(action):
|
||||
return ELEVATED_ACTION_PERMISSIONS.get(action)
|
||||
|
||||
|
||||
# Shipped scripts that endpoints.corptools may push without holding
|
||||
# endpoints.runscript.
|
||||
#
|
||||
# This is an allowance on ONE NAMED SCRIPT, never on the run_script action. The
|
||||
# distinction is the whole point: relaxing the action would let any corptools
|
||||
# holder push ANY script in the library - including the agent self-upgrade, and
|
||||
# anything an operator uploads later - because the script id rides in the
|
||||
# caller-controlled request body. That would silently rebuild the endpoints.deploy
|
||||
# bundle the permission split took apart, which is the one outcome this must not
|
||||
# produce.
|
||||
#
|
||||
# Granting on a name is only defensible because these names are MANAGED: each is
|
||||
# seeded from a source-controlled template, pinned by SHA-256 on delivery, and
|
||||
# re-asserted on every seed (see reportipscript.py). The name therefore cannot be
|
||||
# made to mean something else by editing a row. An entry here is a statement that
|
||||
# the reviewed body is operationally equivalent to something the user could
|
||||
# already do unaided - Report IP is an icon on every Public Desktop in the
|
||||
# company, so pushing it grants no capability the person did not already have.
|
||||
#
|
||||
# Do NOT add a name here because a denial is inconvenient. The test for
|
||||
# membership is that a non-privileged employee can already trigger the same work
|
||||
# by hand on the machine in front of them. Anything else needs endpoints.runscript.
|
||||
CORPTOOLS_PUSHABLE_SCRIPTS = frozenset({
|
||||
'Report IP - Desktop Shortcut',
|
||||
})
|
||||
|
||||
|
||||
def corptools_may_push_script(scriptname):
|
||||
"""True when endpoints.corptools alone is enough to push this script.
|
||||
|
||||
Matched on the exact managed name. Callers must resolve the target to a real
|
||||
Script row FIRST and pass the stored scriptname, never a caller-supplied
|
||||
string: matching what the requester typed would let anyone name their way
|
||||
past the grant.
|
||||
"""
|
||||
return scriptname in CORPTOOLS_PUSHABLE_SCRIPTS
|
||||
|
||||
|
||||
def minimum_agent_version_for(action):
|
||||
"""The lowest agent version that implements an action, or None if any will do."""
|
||||
return COMMAND_ALLOWLIST.get(action, {}).get('minagentversion')
|
||||
@@ -449,6 +487,19 @@ POLICY_EXEMPT_ACTIONS = frozenset({
|
||||
# still requires endpoints.updateapp via ELEVATED_ACTION_PERMISSIONS, and
|
||||
# the agent still refuses UNC, traversal, directories and non-text files.
|
||||
'collect_log',
|
||||
# Script push is gated by WHO the caller is, not by what a PC type exposes.
|
||||
# Configuration Policy describes machine shape; it has no view on whether a
|
||||
# given person may execute code, so a policy that never mentions run_script
|
||||
# must not be read as withdrawing it. Leaving it policy-gated forced sites
|
||||
# to keep an all-PCs "allow run_script" policy row alive purely to avoid
|
||||
# losing the action everywhere - a rule that encoded no real intent and
|
||||
# whose deletion silently disarmed Script Push on every policied endpoint.
|
||||
# Authority lives on the caller axis instead: run_script maps to
|
||||
# endpoints.runscript in ELEVATED_ACTION_PERMISSIONS, checked with
|
||||
# caller_holds at the enqueue route BEFORE policy is consulted, so a scoped
|
||||
# personal token cannot inherit it from an admin owner. This exemption
|
||||
# widens WHERE the action may run, never WHO may run it.
|
||||
'run_script',
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user