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

@@ -90,7 +90,8 @@ services:
flask plugin upgrade-all && flask plugin upgrade-all &&
flask seed permissions && flask seed permissions &&
flask seed settings && flask seed settings &&
flask seed reference-data flask seed reference-data &&
flask seed shipped-scripts
volumes: volumes:
- ./plugins:/app/plugins:ro - ./plugins:/app/plugins:ro
- ./scripts:/app/scripts:ro - ./scripts:/app/scripts:ro

View File

@@ -0,0 +1,159 @@
"""Seed the Report IP desktop-shortcut script into the ShopDB script library.
run_script only delivers bodies that live in the script library, pinned by
SHA-256. The shortcut-equivalent tool therefore has to exist as a real Script
row with a stored body before Corporate Tools can push it.
The template is source-controlled at
plugins/endpoints/api/templates/report-ip-shortcut.ps1 and seeded from there
rather than typed into the library by hand, so the reviewed script and the one
endpoints actually run are the same artifact. Editing the row's body in the UI
is pointless - the next seed overwrites it.
That last point is load-bearing here in a way it is not for an ordinary script.
REPORT_IP_SCRIPT_NAME is named in CORPTOOLS_PUSHABLE_SCRIPTS, so holders of
endpoints.corptools may push this one script WITHOUT holding
endpoints.runscript. Granting on a name is only safe because the name maps to a
body that comes from source control and is digest-pinned on delivery: the name
cannot be made to mean something else. If this row were hand-authorable, the
allowance would become "corptools may run arbitrary code", which is exactly the
bundling that endpoints.runscript was split out to prevent.
It lives under the plugin, not under agent/, because the Docker image copies
shopdb/, plugins/, migrations/ and scripts/ but NOT agent/. A template read
from agent/ resolves fine on a developer checkout and then fails in the
deployed container, which is the worst place to discover it.
Unlike the agent self-upgrade seeder there is no placeholder to render: the
script resolves its own target on the endpoint (shortcut first, installed path
as fallback), so the body is shipped verbatim.
"""
import hashlib
import os
from shopdb.api import Script, db, script_body_location
# Reserved script name. Unique in the library, which makes seeding idempotent
# and keeps a hand-made script from colliding with the managed one. This exact
# string is what the enqueue route matches when deciding whether
# endpoints.corptools is sufficient, so it must stay in lockstep with
# CORPTOOLS_PUSHABLE_SCRIPTS in plugins/endpoints/commands.py - the test
# test_reserved_report_ip_name_matches_the_commands_constant pins them together.
REPORT_IP_SCRIPT_NAME = 'Report IP - Desktop Shortcut'
_REPORT_IP_DESCRIPTION = (
'Managed: reproduces double-clicking the Report IP icon on the Public '
'Desktop, re-reporting this computer\'s IP address. Runs in the signed-in '
'user\'s session and fails closed if nobody is logged on. '
'Seeded from source control - edits here are overwritten.'
)
_TEMPLATE_FILENAME = 'report-ip-shortcut.ps1'
def template_path():
return os.path.join(os.path.dirname(os.path.abspath(__file__)),
'templates', _TEMPLATE_FILENAME)
def _script_dir():
"""Where script bodies live. Resolved through the contract surface helper
so this cannot drift from the directory the download route serves from."""
directory, _prefix = script_body_location()
return directory
def render_template():
"""Return the script body verbatim.
No substitution: the endpoint resolves its own target at run time. Kept as
a function so the seeder reads the file through one place and a future
placeholder has an obvious home.
"""
with open(template_path(), 'r', encoding='utf-8') as handle:
return handle.read()
def seed_report_ip_script():
"""Create or refresh the managed Report IP script row and its body.
Returns a dict describing the seeded script. Idempotent: unchanged input
rewrites nothing and leaves the digest alone.
"""
body = render_template().encode('utf-8')
digest = hashlib.sha256(body).hexdigest()
script = Script.query.filter_by(scriptname=REPORT_IP_SCRIPT_NAME).first()
created = script is None
if created:
script = Script(
scriptname=REPORT_IP_SCRIPT_NAME,
scriptdescription=_REPORT_IP_DESCRIPTION,
# USER context, and this is not a preference - SYSTEM cannot do
# this job at all. Measured on a live endpoint: launched as SYSTEM
# the tool immediately re-entered the interactive session, leaving
# the work in a different process, session and account
# (pid 7240, session 1, owner ...\ShopFloor) while the SYSTEM-side
# launcher exited 0 straight away. The command then reported
# "completed (system context)" having observed nothing - a success
# that means "handed off", not "worked".
#
# This is the same session-0 boundary EndpointCollector already
# documents for mapped drives: session 0 has no access to a user's
# drive table, HKCU or profile, so a SYSTEM process cannot see -
# let alone report on - what happens in the user's session. The
# agent's user path crosses that boundary properly via
# CreateProcessAsUser and returns the child's REAL exit code.
#
# The tradeoff accepted here: user context fails closed when nobody
# is signed in. That is the correct failure - an honest "no one is
# logged on" beats a green result that observed nothing.
runcontext='user',
# Capture successful output too. The whole point of this tool is
# the response line it prints, and an operator who pushed it needs
# to read that line to know the IP was actually reported - a bare
# "succeeded" answers a different question.
captureoutput=True,
)
db.session.add(script)
db.session.flush()
# Body filename follows the library convention (script-<id>.ps1) so the
# collector download route serves it like any other script.
filename = 'script-{}.ps1'.format(script.scriptid)
path = os.path.join(_script_dir(), filename)
unchanged = (script.filename == filename and script.sha256 == digest
and os.path.isfile(path))
if not unchanged:
os.makedirs(_script_dir(), exist_ok=True)
# Temp-then-replace so an interrupted write cannot leave a truncated
# body at the path the row's digest already claims to describe.
temppath = path + '.tmp'
with open(temppath, 'wb') as handle:
handle.write(body)
os.replace(temppath, path)
script.filename = filename
script.sha256 = digest
# Re-assert the managed fields on every seed. A row edited in the UI drifts
# back to the shipped definition here, which is what "edits are
# overwritten" has to mean for the corptools allowance to stay honest: an
# operator must not be able to flip this to runcontext=system, or
# deactivate it, and leave the name still pushable. The context is part of
# what makes the result trustworthy, so it is re-asserted like the rest.
script.scriptdescription = _REPORT_IP_DESCRIPTION
script.runcontext = 'user'
script.captureoutput = True
script.isactive = True
db.session.commit()
return {
'scriptid': script.scriptid,
'scriptname': script.scriptname,
'created': created,
'updated': not unchanged,
'sha256': digest,
'bytes': len(body),
}

View File

@@ -31,7 +31,7 @@ from ..models import (
EndpointState, EndpointEvent, EndpointCommand, EndpointApplication, EndpointState, EndpointEvent, EndpointCommand, EndpointApplication,
EndpointHealthState, OperatorReport, EndpointLogCollection, 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 agentstore
from . import agentauth from . import agentauth
from ..eventcatalog import CRITICAL_EVENT_IDS, event_severity, event_title 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. # require_permission exists to prevent, reintroduced by checking by hand.
elevated = elevated_permission_for(action) elevated = elevated_permission_for(action)
if elevated and not caller_holds(elevated): if elevated and not caller_holds(elevated):
return error_response( # One narrow exception: a SHIPPED script whose reviewed body is
ErrorCodes.FORBIDDEN, # operationally equivalent to something the person could already do
f'Action {action!r} requires the {elevated} permission', # unaided at the machine (Report IP is an icon on every Public Desktop).
http_code=403) # 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 # 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 # only by asking for it explicitly and saying why. The override is scoped to

View File

@@ -0,0 +1,74 @@
# Report IP - reproduces double-clicking the "Report IP" desktop icon.
#
# Seeded from source control by plugins/endpoints/api/reportipscript.py. Edits
# made to the library row are overwritten on the next seed: the corptools
# allowance grants this script BY NAME, so the name has to keep meaning this
# reviewed body.
#
# RUNS IN THE USER SESSION, and that is a hard requirement rather than a
# preference. Measured on a live endpoint, launching this tool as SYSTEM has it
# immediately re-enter the interactive session - the work ended up in a separate
# process under the signed-in account (session 1) while the SYSTEM-side launcher
# exited 0 within a second, having observed nothing. Every attempt to capture
# output or a verdict from the SYSTEM side therefore reported success for work
# it never saw.
#
# This is the same session-0 boundary EndpointCollector documents for mapped
# drives: a SYSTEM process has no view of a user's drive table, HKCU or profile.
# The agent's user-context path crosses it properly (CreateProcessAsUser into
# the active console session) and returns the child's real exit code, so the
# verdict below is trustworthy in a way the SYSTEM path could not be.
#
# Consequently this script stays deliberately thin: the agent owns launching,
# waiting and output capture. Re-implementing any of that here is what produced
# the false results.
$ErrorActionPreference = 'Stop'
# Resolved from the shortcut when present, so the arguments stay whatever the
# icon on the desktop actually uses. The fallback matches the installed tool.
$ShortcutPath = 'C:\Users\Public\Desktop\Report IP.lnk'
$Exe = 'C:\ProgramData\ReportIP\GE_ReportIP_3_v1.EXE'
$ExeArgs = @('/ForceUpdate=True', '/S')
# Working directory comes from the vendor's own scheduled task definition
# (\ReportIP), which runs the tool from its install folder.
$WorkDir = 'C:\ProgramData\ReportIP'
if (Test-Path -LiteralPath $ShortcutPath) {
try {
$link = (New-Object -ComObject WScript.Shell).CreateShortcut($ShortcutPath)
if ($link.TargetPath) {
$Exe = $link.TargetPath
if ($link.Arguments) { $ExeArgs = @($link.Arguments) }
if ($link.WorkingDirectory) { $WorkDir = $link.WorkingDirectory }
}
} catch {
Write-Output "could not read the shortcut, using the installed path: $($_.Exception.Message)"
}
}
if (-not (Test-Path -LiteralPath $Exe)) {
Write-Output "executable not found: $Exe"
exit 2
}
if (-not (Test-Path -LiteralPath $WorkDir)) { $WorkDir = Split-Path -Parent $Exe }
# Identity is logged because it is the thing most likely to be wrong: if this
# ever reports SYSTEM, the run is meaningless for the reasons in the header.
Write-Output "running as: $([Security.Principal.WindowsIdentity]::GetCurrent().Name)"
Write-Output "session: $((Get-Process -Id $PID).SessionId)"
Write-Output "launching: $Exe $($ExeArgs -join ' ')"
# -Wait, and the tool's own exit code is the verdict. No success-string scan:
# the tool reports its outcome through its own channel, and inferring success
# from console text was what produced both false failures and false successes
# in earlier revisions.
$process = Start-Process -FilePath $Exe `
-ArgumentList $ExeArgs `
-WorkingDirectory $WorkDir `
-PassThru -Wait
Write-Output "exit code: $($process.ExitCode)"
exit $process.ExitCode

View File

@@ -238,6 +238,44 @@ def elevated_permission_for(action):
return ELEVATED_ACTION_PERMISSIONS.get(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): def minimum_agent_version_for(action):
"""The lowest agent version that implements an action, or None if any will do.""" """The lowest agent version that implements an action, or None if any will do."""
return COMMAND_ALLOWLIST.get(action, {}).get('minagentversion') return COMMAND_ALLOWLIST.get(action, {}).get('minagentversion')
@@ -449,6 +487,19 @@ POLICY_EXEMPT_ACTIONS = frozenset({
# still requires endpoints.updateapp via ELEVATED_ACTION_PERMISSIONS, and # still requires endpoints.updateapp via ELEVATED_ACTION_PERMISSIONS, and
# the agent still refuses UNC, traversal, directories and non-text files. # the agent still refuses UNC, traversal, directories and non-text files.
'collect_log', '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',
}) })

View File

@@ -859,6 +859,38 @@ def seed_settings():
click.echo(click.style(f"{created} default settings created.", fg='green')) click.echo(click.style(f"{created} default settings created.", fg='green'))
@seed_cli.command('shipped-scripts')
@with_appcontext
def seed_shipped_scripts():
"""Seed managed scripts that ship with the build into the script library.
Idempotent: an unchanged template rewrites nothing and leaves the stored
digest alone. Safe to run on every deploy, which is the point - a managed
script row edited in the UI drifts back to the shipped definition here.
Guarded: a lean build without the endpoints plugin has no shipped scripts
to seed, so the command reports that and exits rather than crashing the
seed chain it runs in.
"""
try:
from plugins.endpoints.api import reportipscript
except ImportError:
click.echo('Endpoints plugin is not installed - no shipped scripts to seed.')
return
seeded = reportipscript.seed_report_ip_script()
if seeded['created']:
state = 'created'
elif seeded['updated']:
state = 'updated'
else:
state = 'already current'
click.echo(click.style(
f"{seeded['scriptname']}: {state} "
f"(scriptid={seeded['scriptid']}, {seeded['bytes']} bytes)",
fg='green'))
# Demo assets carry this assetnumber prefix so a re-run skips what it made and # Demo assets carry this assetnumber prefix so a re-run skips what it made and
# an operator can bulk-delete them without touching imported/real rows. # an operator can bulk-delete them without touching imported/real rows.
DEMO_PREFIX = 'DEMO-' DEMO_PREFIX = 'DEMO-'

View File

@@ -12,7 +12,8 @@ from plugins.endpoints.eventcatalog import EVENT_CATALOG, CRITICAL_EVENT_IDS
from plugins.endpoints.commands import ( from plugins.endpoints.commands import (
COMMAND_ALLOWLIST, is_allowed_action, elevated_permission_for, COMMAND_ALLOWLIST, is_allowed_action, elevated_permission_for,
requires_target, minimum_agent_version_for, agent_meets_minimum, requires_target, minimum_agent_version_for, agent_meets_minimum,
parse_agent_version, parse_agent_version, corptools_may_push_script,
CORPTOOLS_PUSHABLE_SCRIPTS,
) )
@@ -168,6 +169,55 @@ def test_endpoints_declares_the_restart_permission():
assert 'endpoints.command' in permissions assert 'endpoints.command' in permissions
def test_run_script_still_requires_the_runscript_permission():
"""The corptools allowance is an exception for one named script, not a
relaxation of run_script. The action itself must keep demanding
endpoints.runscript, otherwise a corptools holder could push anything in
the library."""
assert elevated_permission_for('run_script') == 'endpoints.runscript'
def test_corptools_may_push_only_the_shipped_report_ip_script():
"""Corporate Tools can push the managed Report IP script by name, and
nothing else. A site-authored script that happens to be selected from the
same tab gets no allowance."""
assert corptools_may_push_script('Report IP - Desktop Shortcut') is True
assert corptools_may_push_script('Uninstall Everything') is False
assert corptools_may_push_script(None) is False
assert corptools_may_push_script('') is False
def test_corptools_pushable_scripts_stays_a_single_managed_entry():
"""Guards the blast radius of the exception. Growing this set widens what
endpoints.corptools can deliver without endpoints.runscript, so adding an
entry should be a deliberate change that fails this test first."""
assert CORPTOOLS_PUSHABLE_SCRIPTS == {'Report IP - Desktop Shortcut'}
def test_corptools_allowance_name_matches_the_seeder():
"""The allowance matches on the stored script name, so the seeder and the
allowlist must agree exactly. If either string is edited alone the
Corporate Tools button silently 403s."""
from plugins.endpoints.api.reportipscript import REPORT_IP_SCRIPT_NAME
assert REPORT_IP_SCRIPT_NAME in CORPTOOLS_PUSHABLE_SCRIPTS
def test_report_ip_script_body_does_not_relaunch_itself():
"""Pins the session-0 lesson. Launched as SYSTEM this tool re-enters the
interactive session, so the launcher observes nothing and exits 0 - a
success that means 'handed off', not 'worked'. The body must therefore
stay thin and let the agent's user-context path own launching and the exit
code; re-introducing a cmd.exe wrapper or a success-string scan is what
produced the false results."""
from plugins.endpoints.api.reportipscript import template_path
with open(template_path(), 'r', encoding='utf-8') as handle:
body = handle.read()
assert 'ComSpec' not in body
assert 'completed successfully' not in body
assert '-Wait' in body
assert 'exit $process.ExitCode' in body
def test_collect_now_is_allowlisted_targetless_and_routine(): def test_collect_now_is_allowlisted_targetless_and_routine():
"""collect_now forces an off-cadence telemetry report. It is allowlisted so """collect_now forces an off-cadence telemetry report. It is allowlisted so
the enqueue route accepts it, carries no wire target (fixed action), and the enqueue route accepts it, carries no wire target (fixed action), and
@@ -217,6 +267,62 @@ def test_collect_log_survives_a_policy_that_never_mentions_it():
assert elevated_permission_for('collect_log') == 'endpoints.updateapp' assert elevated_permission_for('collect_log') == 'endpoints.updateapp'
def test_run_script_survives_a_policy_that_never_mentions_it(monkeypatch):
"""Script Push is gated by caller permission, not by machine shape.
Before this exemption the action stayed reachable only because a site kept
an all-PCs "allow run_script" policy row alive. That row expressed no real
configuration intent, and deleting it silently removed Script Push from
every policied endpoint at once - a failure that reads as authorization
("not permitted by policy") while the caller's grant was never in doubt.
Authority belongs on the caller axis, so a policy that is silent about
run_script must not be read as withdrawing it.
"""
import plugins.endpoints.commands as commands
class _Effective:
def allowed_actions(self):
# A real, unrelated policy: it shapes services and says nothing
# whatsoever about running scripts.
return {'service_start', 'service_stop'}
monkeypatch.setattr(
'plugins.computers.services.policyresolver.effective_policy_for',
lambda assetid: _Effective(),
)
allowed = commands.ALLOWED_ACTIONS_FOR(assetid=1)
assert 'run_script' in allowed
# Exemption from POLICY is not exemption from PERMISSION: the grant must
# survive the exemption rather than be loosened by it. The enqueue route
# checks this with caller_holds BEFORE consulting policy, so a scoped
# personal token cannot inherit it from an admin owner.
assert elevated_permission_for('run_script') == 'endpoints.runscript'
def test_policy_exemption_never_widens_past_the_code_allowlist(monkeypatch):
"""Exempt actions are re-added, but the base allowlist still bounds them.
The exemption set is unioned in after the policy intersection, so a bug
there could hand back an action the command layer does not implement.
"""
import plugins.endpoints.commands as commands
class _Effective:
def allowed_actions(self):
return {'service_start'}
monkeypatch.setattr(
'plugins.computers.services.policyresolver.effective_policy_for',
lambda assetid: _Effective(),
)
allowed = commands.ALLOWED_ACTIONS_FOR(assetid=1)
assert allowed <= set(commands.COMMAND_ALLOWLIST.keys())
def test_collect_log_declares_the_agent_version_that_implements_it(): def test_collect_log_declares_the_agent_version_that_implements_it():
"""collect_log names its minimum agent version. """collect_log names its minimum agent version.