"""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_role 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_role('admin') 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')