|
| 1 | +name: bughunt-hatch |
| 2 | +on: |
| 3 | + push: |
| 4 | + branches: ['bughunt/hatch/**'] |
| 5 | +permissions: |
| 6 | + contents: read |
| 7 | +jobs: |
| 8 | + probe: |
| 9 | + strategy: |
| 10 | + fail-fast: false |
| 11 | + matrix: |
| 12 | + os: [ubuntu-latest, macos-latest, windows-latest] |
| 13 | + runs-on: ${{ matrix.os }} |
| 14 | + timeout-minutes: 45 |
| 15 | + steps: |
| 16 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
| 17 | + with: |
| 18 | + persist-credentials: false |
| 19 | + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 |
| 20 | + with: |
| 21 | + python-version: '3.11' |
| 22 | + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 |
| 23 | + - run: cargo build -p socket-patch-cli |
| 24 | + - name: Probe hosted Hatch stale environment |
| 25 | + shell: bash |
| 26 | + run: | |
| 27 | + cat > mock.py <<'PY' |
| 28 | + import base64, hashlib, json, sys, io, zipfile, threading |
| 29 | + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer |
| 30 | + ORG="test-org"; PURL="pkg:pypi/six@1.16.0"; UUID="5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d"; TOKEN="11111111-2222-4333-8444-555555555555" |
| 31 | + WHEEL="six-1.16.0-py2.py3-none-any.whl" |
| 32 | + PORT=int(sys.argv[1]) |
| 33 | + orig=open(sys.argv[2],'rb').read(); patched=orig+b"\n# SOCKET-PATCHED\nSOCKET_PATCHED = 1\n" |
| 34 | + def gh(b): return hashlib.sha256(b"blob %d\0"%len(b)+b).hexdigest() |
| 35 | + def d(b): return base64.urlsafe_b64encode(hashlib.sha256(b).digest()).rstrip(b"=").decode() |
| 36 | + meta=b"Metadata-Version: 2.1\nName: six\nVersion: 1.16.0\nSummary: x\n" |
| 37 | + wh=b"Wheel-Version: 1.0\nGenerator: t\nRoot-Is-Purelib: true\nTag: py2-none-any\nTag: py3-none-any\n" |
| 38 | + mem=[("six.py",patched),("six-1.16.0.dist-info/METADATA",meta),("six-1.16.0.dist-info/WHEEL",wh)] |
| 39 | + rec="".join(f"{n},sha256={d(b)},{len(b)}\n" for n,b in mem)+"six-1.16.0.dist-info/RECORD,,\n" |
| 40 | + buf=io.BytesIO(); z=zipfile.ZipFile(buf,"w") |
| 41 | + for n,b in mem+[("six-1.16.0.dist-info/RECORD",rec.encode())]: z.writestr(n,b) |
| 42 | + z.close(); wheel=buf.getvalue(); SHA=hashlib.sha256(wheel).hexdigest() |
| 43 | + BASE=f"http://127.0.0.1:{PORT}"; APATH=f"/patch/pypi/six/1.16.0/{TOKEN}/{UUID}/{WHEEL}"; URL=BASE+APATH |
| 44 | + VIEW={"uuid":UUID,"purl":PURL,"publishedAt":"Fri, 27 Mar 2026 00:00:00 GMT","files":{"six.py":{"beforeHash":gh(orig),"afterHash":gh(patched),"blobContent":base64.b64encode(patched).decode()}},"vulnerabilities":{"GHSA-real-pypi-0001":{"cves":["CVE-2026-7201"],"summary":"s","severity":"high","description":"d"}},"description":"d","license":"MIT","tier":"free"} |
| 45 | + class H(BaseHTTPRequestHandler): |
| 46 | + def log_message(self,*a): sys.stderr.write("REQ "+self.command+" "+self.path+"\n") |
| 47 | + def j(self,o): |
| 48 | + b=json.dumps(o).encode(); self.send_response(200); self.send_header("content-type","application/json"); self.send_header("content-length",str(len(b))); self.end_headers(); self.wfile.write(b) |
| 49 | + def do_POST(self): |
| 50 | + self.rfile.read(int(self.headers.get("content-length",0))) |
| 51 | + if self.path==f"/v0/orgs/{ORG}/patches/batch": |
| 52 | + return self.j({"packages":[{"purl":PURL,"patches":[{"uuid":UUID,"purl":PURL,"tier":"free","cveIds":["CVE-2026-7201"],"ghsaIds":["GHSA-real-pypi-0001"],"severity":"high","title":"t"}]}],"canAccessPaidPatches":False}) |
| 53 | + if self.path==f"/v0/orgs/{ORG}/patches/package": |
| 54 | + return self.j({"results":{UUID:{"status":"granted","url":URL,"purl":PURL,"artifacts":[{"kind":"tarball","url":URL,"integrity":{"sha256":SHA}}],"registryOverride":None}}}) |
| 55 | + self.send_response(404); self.end_headers() |
| 56 | + def do_GET(self): |
| 57 | + p=self.path.split("?")[0] |
| 58 | + if p.startswith(f"/v0/orgs/{ORG}/patches/by-package/"): |
| 59 | + return self.j({"patches":[{"uuid":UUID,"purl":PURL,"publishedAt":"2026-03-27T00:00:00Z","description":"x","license":"MIT","tier":"free","vulnerabilities":{}}],"canAccessPaidPatches":False}) |
| 60 | + if p in (f"/patch/view/{UUID}",f"/v0/orgs/{ORG}/patches/view/{UUID}"): return self.j(VIEW) |
| 61 | + if p.startswith(f"/v0/orgs/{ORG}/patches/blob/") or p.startswith("/patch/blob/"): |
| 62 | + h=p.rsplit("/",1)[1] |
| 63 | + for b in (orig,patched): |
| 64 | + if gh(b)==h: |
| 65 | + self.send_response(200); self.send_header("content-length",str(len(b))); self.end_headers(); self.wfile.write(b); return |
| 66 | + if p==APATH: |
| 67 | + self.send_response(200); self.send_header("content-type","application/octet-stream"); self.send_header("content-length",str(len(wheel))); self.end_headers(); self.wfile.write(wheel); return |
| 68 | + self.send_response(404); self.end_headers() |
| 69 | + def do_HEAD(self): |
| 70 | + p=self.path.split("?")[0] |
| 71 | + if p==APATH: |
| 72 | + self.send_response(200); self.send_header("content-type","application/octet-stream"); self.send_header("content-length",str(len(wheel))); self.end_headers(); return |
| 73 | + self.send_response(404); self.end_headers() |
| 74 | + print("SHA",SHA,flush=True) |
| 75 | + ThreadingHTTPServer(("127.0.0.1",PORT),H).serve_forever() |
| 76 | + PY |
| 77 | + cat > probe.py <<'PY' |
| 78 | + import json, os, subprocess, sys, shutil |
| 79 | + HATCH, SP, W = sys.argv[1], os.path.abspath(sys.argv[2]), os.path.abspath(sys.argv[3]) |
| 80 | + API = "http://127.0.0.1:18080" |
| 81 | + shutil.rmtree(W, ignore_errors=True); app = os.path.join(W, "app"); os.makedirs(os.path.join(app, "src", "app")) |
| 82 | + open(os.path.join(app, "src", "app", "__init__.py"), "w").write("") |
| 83 | + open(os.path.join(app, "pyproject.toml"), "w").write('[build-system]\nrequires = ["hatchling"]\nbuild-backend = "hatchling.build"\n\n[project]\nname = "app"\nversion = "0.1.0"\ndependencies = ["six==1.16.0"]\n\n[tool.hatch.build.targets.wheel]\npackages = ["src/app"]\n') |
| 84 | + env = dict(os.environ, HATCH_DATA_DIR=W + "/data", HATCH_CACHE_DIR=W + "/cache", SOCKET_NO_CONFIG="1", SOCKET_TELEMETRY_DISABLED="1", SOCKET_NO_UPDATE_CHECK="1") |
| 85 | + env.pop("VIRTUAL_ENV", None) |
| 86 | + def run(cmd, extra=None): |
| 87 | + e = dict(env, **(extra or {})) |
| 88 | + return subprocess.run(cmd, cwd=app, env=e, capture_output=True, text=True) |
| 89 | + def state(): |
| 90 | + r = run([HATCH, "run", "python", "-c", "import six; print('PATCHED' if hasattr(six, 'SOCKET_PATCHED') else 'UNPATCHED')"]) |
| 91 | + return (r.stdout.strip().splitlines() or ["?" + r.stderr[-300:]])[-1] |
| 92 | + r = run([HATCH, "env", "create"]); print("env create", r.returncode, r.stderr[-300:] if r.returncode else "") |
| 93 | + print("before:", state()) |
| 94 | + scan = [SP, "scan", "--mode", "hosted", "--json", "--yes", "--api-url", API, "--api-token", "fake", "--org", "test-org", "--patch-server-url", API, "--ecosystems", "pypi"] |
| 95 | + r = run(scan); d = json.loads(r.stdout[r.stdout.find("{"):]) |
| 96 | + print("scan exit", r.returncode, "status", d.get("status"), "redirected", d.get("redirect", {}).get("redirected"), "warnings", [w["code"] for w in d.get("redirect", {}).get("warnings", [])]) |
| 97 | + print("after 1st hatch run:", state()); print("after 2nd hatch run:", state()) |
| 98 | + r = run([SP, "vex", "--product", "pkg:pypi/app@0.1.0", "--api-url", API, "--api-token", "fake", "--org", "test-org", "--patch-server-url", API]) |
| 99 | + try: st = [s["status"] for s in json.loads(r.stdout)["statements"]] |
| 100 | + except Exception: st = "none: " + r.stderr[-200:] |
| 101 | + print("vex statements:", st) |
| 102 | + venv = run([HATCH, "env", "find", "default"]).stdout.strip() |
| 103 | + r = run(scan, {"VIRTUAL_ENV": venv}); d = json.loads(r.stdout[r.stdout.find("{"):]) |
| 104 | + print("re-scan with VIRTUAL_ENV=hatch env: warnings", [w["code"] for w in d.get("redirect", {}).get("warnings", [])]) |
| 105 | + run([HATCH, "env", "remove", "default"]); print("after env remove + recreate:", state()) |
| 106 | + stale = state # noqa |
| 107 | + PY |
| 108 | + python -m pip download -q --no-deps six==1.16.0 -d sixdl |
| 109 | + python -c "import zipfile,glob;open('six.py','wb').write(zipfile.ZipFile(glob.glob('sixdl/*.whl')[0]).read('six.py'))" |
| 110 | + python mock.py 18080 six.py > mock.log 2>&1 & |
| 111 | + sleep 3 |
| 112 | + cli=$PWD/target/debug/socket-patch; [ "$RUNNER_OS" = Windows ] && cli=$PWD/target/debug/socket-patch.exe |
| 113 | + for v in 1.7.0 1.9.7 1.16.5 1.18.1; do |
| 114 | + python -m venv "h-$v" |
| 115 | + if [ "$RUNNER_OS" = Windows ]; then py="h-$v/Scripts/python.exe"; h="$PWD/h-$v/Scripts/hatch.exe"; else py="h-$v/bin/python"; h="$PWD/h-$v/bin/hatch"; fi |
| 116 | + "$py" -m pip install -q "hatch==$v" >/dev/null 2>&1 || { echo "install hatch $v failed"; continue; } |
| 117 | + echo "################ $RUNNER_OS Hatch $v" |
| 118 | + python probe.py "$h" "$cli" "$RUNNER_TEMP/w-$v" || echo "probe exit $?" |
| 119 | + done |
0 commit comments