"""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-.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), }