|
| 1 | +name: bughunt pip probe |
| 2 | + |
| 3 | +on: |
| 4 | + push: |
| 5 | + branches: ['bughunt/pip/**'] |
| 6 | + |
| 7 | +permissions: |
| 8 | + contents: read |
| 9 | + |
| 10 | +jobs: |
| 11 | + probe: |
| 12 | + strategy: |
| 13 | + fail-fast: false |
| 14 | + matrix: |
| 15 | + os: [ubuntu-latest, macos-latest, windows-latest] |
| 16 | + python: ['3.8', '3.13'] |
| 17 | + pip: ['20.3.4', '23.3.2', 'latest'] |
| 18 | + exclude: |
| 19 | + - python: '3.13' |
| 20 | + pip: '20.3.4' |
| 21 | + runs-on: ${{ matrix.os }} |
| 22 | + timeout-minutes: 45 |
| 23 | + steps: |
| 24 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
| 25 | + with: |
| 26 | + persist-credentials: false |
| 27 | + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 |
| 28 | + with: |
| 29 | + python-version: ${{ matrix.python }} |
| 30 | + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 |
| 31 | + with: |
| 32 | + key: bughunt-pip |
| 33 | + save-if: false |
| 34 | + - run: cargo build --locked -p socket-patch-cli |
| 35 | + - name: Probe |
| 36 | + shell: bash |
| 37 | + run: | |
| 38 | + mkdir -p "$RUNNER_TEMP/bh" |
| 39 | + cat > "$RUNNER_TEMP/bh/mock.py" <<'MOCK_EOF' |
| 40 | + import base64, hashlib, io, json, sys, zipfile |
| 41 | + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer |
| 42 | + PORT=int(sys.argv[1]); RAW=open(sys.argv[2],'rb').read() |
| 43 | + UUID="3c5e7a9b-1d3f-4b5d-8f7a-9b1d3f5a7c9e" |
| 44 | + before=zipfile.ZipFile(io.BytesIO(RAW)).read("six.py"); after=before+b"\n# SOCKET-PATCHED\nSOCKET_PATCHED = 1\n" |
| 45 | + def build(): |
| 46 | + zin=zipfile.ZipFile(io.BytesIO(RAW)); out=io.BytesIO(); zout=zipfile.ZipFile(out,"w",zipfile.ZIP_DEFLATED) |
| 47 | + rec=[n for n in zin.namelist() if n.endswith(".dist-info/RECORD")][0]; lines=[] |
| 48 | + for n in zin.namelist(): |
| 49 | + if n==rec: continue |
| 50 | + d=after if n=="six.py" else zin.read(n); zout.writestr(n,d) |
| 51 | + lines.append("%s,sha256=%s,%d"%(n,base64.urlsafe_b64encode(hashlib.sha256(d).digest()).rstrip(b"=").decode(),len(d))) |
| 52 | + lines.append(rec+",,"); zout.writestr(rec,"\n".join(lines)+"\n"); zout.close(); return out.getvalue() |
| 53 | + WHEEL=build() |
| 54 | + g=lambda b: hashlib.sha256(b"blob %d\0"%len(b)+b).hexdigest() |
| 55 | + API_PURL="pkg:pypi/six@1.16.0?artifact_id=py2-py3-none-any-whl" |
| 56 | + VIEW={"uuid":UUID,"purl":API_PURL,"publishedAt":"2026-09-01T00:00:00Z","files":{"six.py":{"beforeHash":g(before),"afterHash":g(after),"blobContent":base64.b64encode(after).decode()}}, |
| 57 | + "vulnerabilities":{"GHSA-aaaa-bbbb-cccc":{"cves":["CVE-2026-0001"],"summary":"s","severity":"high","description":"d"}},"description":"x","license":"MIT","tier":"free"} |
| 58 | + PATCH={"uuid":UUID,"purl":API_PURL,"tier":"free","cveIds":["CVE-2026-0001"],"ghsaIds":["GHSA-aaaa-bbbb-cccc"],"severity":"HIGH","title":"t"} |
| 59 | + API="http://127.0.0.1:%d"%PORT |
| 60 | + class H(BaseHTTPRequestHandler): |
| 61 | + def log_message(self,*a): sys.stderr.write("REQ %s %s\n"%(self.command,self.path)) |
| 62 | + def out(self,obj): |
| 63 | + b=json.dumps(obj).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) |
| 64 | + def do_GET(self): |
| 65 | + if "/patches/view/" in self.path: return self.out(VIEW) |
| 66 | + if "/blob/" in self.path: |
| 67 | + h=self.path.rsplit('/',1)[1] |
| 68 | + for d in (before,after): |
| 69 | + if g(d)==h: self.send_response(200); self.send_header("Content-Length",str(len(d))); self.end_headers(); self.wfile.write(d); return |
| 70 | + if self.path.startswith("/patch/pypi/"): |
| 71 | + self.send_response(200); self.send_header("Content-Length",str(len(WHEEL))); self.end_headers(); self.wfile.write(WHEEL); return |
| 72 | + if "/by-package/" in self.path: return self.out({"patches":[dict(PATCH,publishedAt="2026-09-01T00:00:00Z",description="x",license="MIT",vulnerabilities={})],"canAccessPaidPatches":False}) |
| 73 | + self.send_response(404); self.end_headers() |
| 74 | + def do_POST(self): |
| 75 | + raw=self.rfile.read(int(self.headers.get("Content-Length") or 0)).decode() |
| 76 | + sys.stderr.write("BODY %s\n"%raw[:300]) |
| 77 | + if self.path.endswith("/patches/batch"): |
| 78 | + has="pkg:pypi/six@1.16.0" in raw |
| 79 | + return self.out({"packages":[{"purl":"pkg:pypi/six@1.16.0","patches":[PATCH]}] if has else [],"canAccessPaidPatches":False}) |
| 80 | + if self.path.endswith("/patches/package"): |
| 81 | + url=API+"/patch/pypi/six/1.16.0/11111111-2222-4333-8444-555555555555/"+UUID+"/six-1.16.0-py2.py3-none-any.whl" |
| 82 | + return self.out({"results":{UUID:{"status":"granted","url":url,"purl":"pkg:pypi/six@1.16.0","artifacts":[{"kind":"tarball","url":url,"integrity":{"sha256":hashlib.sha256(WHEEL).hexdigest()}}],"registryOverride":None}}}) |
| 83 | + self.out({}) |
| 84 | + ThreadingHTTPServer(("127.0.0.1",PORT),H).serve_forever() |
| 85 | + MOCK_EOF |
| 86 | + cat > "$RUNNER_TEMP/bh/probe.py" <<'PROBE_EOF' |
| 87 | + import json, os, subprocess, sys, shutil, threading, runpy, tempfile, urllib.request |
| 88 | + CLI = os.path.abspath(sys.argv[1]); PIPV = sys.argv[2]; PY = sys.executable |
| 89 | + WHL = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl" |
| 90 | + tmp = os.path.realpath(os.path.join(os.environ.get("RUNNER_TEMP", tempfile.gettempdir()), "bhpip")); os.makedirs(tmp, exist_ok=True) |
| 91 | + raw = os.path.join(tmp, "six.whl"); open(raw, "wb").write(urllib.request.urlopen(WHL).read()) |
| 92 | + PORT = 18080 |
| 93 | + threading.Thread(target=lambda: (sys.argv.__setitem__(slice(1, None), [str(PORT), raw]), runpy.run_path(os.path.join(os.path.dirname(__file__), "mock.py"))), daemon=True).start() |
| 94 | + import time; time.sleep(2) |
| 95 | + env = dict(os.environ, SOCKET_API_URL="http://127.0.0.1:%d" % PORT, SOCKET_API_TOKEN="fake", SOCKET_ORG_SLUG="test-org", SOCKET_NO_CONFIG="1", SOCKET_NO_UPDATE_CHECK="1", SOCKET_TELEMETRY_DISABLED="1", PIP_DISABLE_PIP_VERSION_CHECK="1") |
| 96 | + env.pop("VIRTUAL_ENV", None) |
| 97 | + def run(a, d): return subprocess.run(a, cwd=d, env=env, capture_output=True, text=True) |
| 98 | + def vpy(v): return os.path.join(v, "Scripts", "python.exe") if os.name == "nt" else os.path.join(v, "bin", "python") |
| 99 | + HASHED = "six==1.16.0 \\\n --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254\n" |
| 100 | + rows = [] |
| 101 | + def pipinstall(d): |
| 102 | + v = os.path.join(d, "fresh"); run([PY, "-m", "venv", v], d) |
| 103 | + run([vpy(v), "-m", "pip", "install", "-q", "pip==" + PIPV], d) |
| 104 | + r = run([vpy(v), "-m", "pip", "install", "-r", "requirements.txt"], d) |
| 105 | + err = [l for l in (r.stdout + r.stderr).splitlines() if l.startswith("ERROR")] |
| 106 | + st = run([vpy(v), "-c", "import six;print('PATCHED' if hasattr(six,'SOCKET_PATCHED') else 'UNPATCHED')"], d).stdout.strip() or "absent" |
| 107 | + return "pip-exit=%d %s %s" % (r.returncode, st, (err[0][:110] if err else "")) |
| 108 | + for case, content, mode in [("hosted-unhashed", "six==1.16.0\nidna==3.7\n", "hosted"), ("vendored-unhashed", "six==1.16.0\nidna==3.7\n", "vendored"), ("hosted-control-single", "six==1.16.0\n", "hosted"), ("setup-hashed", HASHED, "setup")]: |
| 109 | + d = os.path.join(tmp, case); shutil.rmtree(d, ignore_errors=True); os.makedirs(d) |
| 110 | + run(["git", "init", "-q", "."], d) |
| 111 | + open(os.path.join(d, "requirements.txt"), "w", newline="").write(content) |
| 112 | + if mode == "vendored": |
| 113 | + run([PY, "-m", "venv", ".venv"], d); run([vpy(os.path.join(d, ".venv")), "-m", "pip", "install", "-q", "-r", "requirements.txt"], d) |
| 114 | + if mode == "setup": |
| 115 | + s = run([CLI, "setup", "--yes", "--json"], d) |
| 116 | + else: |
| 117 | + s = run([CLI, "scan", "--mode", mode, "--json", "--yes"], d) |
| 118 | + try: st = json.loads(s.stdout[s.stdout.find("{"):]).get("status") |
| 119 | + except Exception: st = "nojson" |
| 120 | + req = open(os.path.join(d, "requirements.txt")).read().replace("\n", "\\n") |
| 121 | + rows.append("%s | cli-exit=%d %s | %s | %s" % (case, s.returncode, st, pipinstall(d), req[-70:])) |
| 122 | + print("== pip %s | python %s | %s" % (PIPV, sys.version.split()[0], sys.platform)) |
| 123 | + for r in rows: print(r) |
| 124 | + PROBE_EOF |
| 125 | + CLI="$PWD/target/debug/socket-patch"; [ -f "$CLI.exe" ] && CLI="$CLI.exe" |
| 126 | + python "$RUNNER_TEMP/bh/probe.py" "$CLI" "${{ matrix.pip }}" |
0 commit comments