Skip to content

Commit 8225e7e

Browse files
committed
bughunt probe: hatch hosted stale env
1 parent f6b7fb9 commit 8225e7e

1 file changed

Lines changed: 119 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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

Comments
 (0)