Ben Baker's installed-software collection, software delivery (run_script and
update_app), agentless adoption tool and command-queue hardening, with his
authorship preserved.
Reviewed before promotion. Fixed: the endpoints plugin tests were never collected
by pytest, the SBOM snapshot table's key would exhaust on MySQL, ingest did not
truncate two columns strict MySQL would reject, and update_app's version branch
pinned a digest for a file that route never serves. Agent packaging moved off
require_role('admin') onto a new endpoints.agentpackage permission.
Contract 0.27.0. Full suite 2231, frontend 314.
108 lines
4.1 KiB
Python
108 lines
4.1 KiB
Python
"""Endpoints adoption-tool packaging (SPIKE).
|
|
|
|
This module is deliberately self-contained so it can be cherry-picked onto
|
|
main as a single unit. It attaches ONE admin-gated route to the existing
|
|
endpoints blueprint and never edits routes.py.
|
|
|
|
The adoption tool is NOT the endpoint agent. It is a one-shot, agentless
|
|
enrollment client: given only the endpoints collector key and this server's
|
|
base URL, it creates a ShopDB PC (Computer) + Endpoint entry via the existing
|
|
POST /api/collector/endpoints adopt path, then exits. This route packages that
|
|
tool for download: a ZIP carrying the operator-supplied config, a README, and
|
|
(when a prebuilt binary is staged on the server) the shopdb-adopt.exe.
|
|
|
|
The collector key is supplied by the admin and is NOT stored or logged here.
|
|
The minted secret is unretrievable after generation, so the operator pastes it
|
|
into the generator; it only lives inside the downloaded config file.
|
|
"""
|
|
|
|
import io
|
|
import json
|
|
import os
|
|
import zipfile
|
|
|
|
from flask import request, current_app, send_file
|
|
|
|
from shopdb.api import error_response, ErrorCodes, require_permission
|
|
|
|
from .routes import endpoints_bp
|
|
|
|
# A staged, prebuilt shopdb-adopt.exe is embedded when present. The container
|
|
# has no .NET SDK, so this mirrors the endpoint agent installer model: build the
|
|
# exe out of band, drop it under instance/adoptiontool/, and it ships in the ZIP.
|
|
STAGED_EXE_RELPATH = os.path.join('adoptiontool', 'shopdb-adopt.exe')
|
|
|
|
_README = """ShopDB adoption tool (spike)
|
|
|
|
WHAT THIS IS
|
|
A one-shot, agentless enrollment tool. Run it once on a PC and it creates a
|
|
ShopDB PC and Endpoint entry using only the collector key in the config file
|
|
next to this exe. It collects identity, hardware, and installed-software (SBOM)
|
|
details, then exits. It installs nothing, runs no service, and sends no
|
|
ongoing telemetry. It is NOT the endpoint agent.
|
|
|
|
FILES
|
|
shopdb-adopt.exe The tool (present only if the server had a prebuilt copy).
|
|
adopt.config.json The base URL and collector key this tool will use.
|
|
|
|
USAGE
|
|
1. Keep shopdb-adopt.exe and adopt.config.json in the same folder.
|
|
2. Right-click shopdb-adopt.exe and choose Run as administrator. Elevation is
|
|
required so hardware and installed-software collection succeeds.
|
|
3. It reports the machine to ShopDB and exits 0 on success.
|
|
|
|
You can override config values with arguments:
|
|
shopdb-adopt.exe --baseurl https://shopdb.example --collectorkey KEY
|
|
|
|
SECURITY
|
|
adopt.config.json contains a live collector key. Treat it as a secret. The
|
|
key is not recoverable from the ShopDB server after it was generated, so keep
|
|
this download safe or regenerate the key to revoke it.
|
|
"""
|
|
|
|
|
|
def _build_readme():
|
|
return _README
|
|
|
|
|
|
@endpoints_bp.route('/adoptiontool/package', methods=['POST'])
|
|
@require_permission('endpoints.agentpackage')
|
|
def package_adoption_tool():
|
|
"""Bundle the adoption tool as a downloadable ZIP.
|
|
|
|
Body: {"baseurl": "...", "collectorkey": "..."}. Returns an in-memory ZIP
|
|
with adopt.config.json, README.txt, and the staged exe when available. The
|
|
collector key is embedded in the config only; it is never stored or logged.
|
|
"""
|
|
body = request.get_json(silent=True) or {}
|
|
baseurl = (body.get('baseurl') or '').strip()
|
|
collectorkey = (body.get('collectorkey') or '').strip()
|
|
|
|
if not baseurl or not collectorkey:
|
|
return error_response(
|
|
ErrorCodes.VALIDATION_ERROR,
|
|
'baseurl and collectorkey are required',
|
|
http_code=400)
|
|
|
|
config = {
|
|
'baseurl': baseurl.rstrip('/'),
|
|
'collectorkey': collectorkey,
|
|
}
|
|
|
|
buffer = io.BytesIO()
|
|
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as archive:
|
|
archive.writestr('adopt.config.json',
|
|
json.dumps(config, indent=2) + '\n')
|
|
archive.writestr('README.txt', _build_readme())
|
|
|
|
staged = os.path.join(current_app.instance_path, STAGED_EXE_RELPATH)
|
|
if os.path.isfile(staged):
|
|
archive.write(staged, 'shopdb-adopt.exe')
|
|
|
|
buffer.seek(0)
|
|
return send_file(
|
|
buffer,
|
|
mimetype='application/zip',
|
|
as_attachment=True,
|
|
download_name='shopdb-adoption-tool.zip')
|