|
| 1 | +name: bughunt-pip |
| 2 | +on: |
| 3 | + push: |
| 4 | + branches: ['bughunt/pip/**'] |
| 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 | + combo: |
| 14 | + - { py: '3.8', pip: '20.3.4' } |
| 15 | + - { py: '3.13', pip: '26.2.1' } |
| 16 | + runs-on: ${{ matrix.os }} |
| 17 | + timeout-minutes: 45 |
| 18 | + steps: |
| 19 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
| 20 | + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 |
| 21 | + with: |
| 22 | + python-version: ${{ matrix.combo.py }} |
| 23 | + - run: rustup show |
| 24 | + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 |
| 25 | + - run: cargo build --locked -p socket-patch-cli |
| 26 | + - name: probe |
| 27 | + shell: bash |
| 28 | + run: | |
| 29 | + cat > probe.py <<'PROBE_EOF' |
| 30 | + import base64, hashlib, http.server, io, json, os, shutil, subprocess, sys, threading, zipfile, re, urllib.parse |
| 31 | + BIN = os.path.abspath(sys.argv[1]); PIPV = sys.argv[2] |
| 32 | + W = os.path.abspath("probe-work"); shutil.rmtree(W, ignore_errors=True); os.makedirs(W) |
| 33 | + WIN = os.name == "nt" |
| 34 | + UUID = "a1a1a1a1-a1a1-4a1a-8a1a-a1a1a1a1a1a1" |
| 35 | + VER = "1.15.0" |
| 36 | + def run(cmd, cwd=None, env=None): |
| 37 | + p = subprocess.run(cmd, cwd=cwd, env=env, capture_output=True, text=True) |
| 38 | + return p.returncode, p.stdout + p.stderr |
| 39 | + def vpy(v): return os.path.join(v, "Scripts" if WIN else "bin", "python.exe" if WIN else "python") |
| 40 | + def mkvenv(path, pipv=PIPV): |
| 41 | + rc, out = run([sys.executable, "-m", "venv", path]); assert rc == 0, out |
| 42 | + rc, out = run([vpy(path), "-m", "pip", "install", "-q", f"pip=={pipv}"]); assert rc == 0, out |
| 43 | + return vpy(path) |
| 44 | + rc, out = run([sys.executable, "-m", "pip", "download", "--no-deps", f"six=={VER}", "-d", W, "-q"]); assert rc == 0, out |
| 45 | + WN = f"six-{VER}-py2.py3-none-any.whl" |
| 46 | + zin = zipfile.ZipFile(os.path.join(W, WN)); buf = io.BytesIO(); zout = zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) |
| 47 | + for i in zin.infolist(): |
| 48 | + d = zin.read(i.filename) |
| 49 | + if i.filename == "six.py": before = d; d = d + b"# SOCKET-PATCHED\n"; after = d |
| 50 | + zout.writestr(i, d) |
| 51 | + zout.close(); WHL = buf.getvalue() |
| 52 | + SHA = hashlib.sha256(WHL).hexdigest(); SRI = "sha512-" + base64.b64encode(hashlib.sha512(WHL).digest()).decode() |
| 53 | + g = lambda d: hashlib.sha256(b"blob %d\0" % len(d) + d).hexdigest() |
| 54 | + WPATH = f"/patch/pypi/six/{VER}/tok/{UUID}/{WN}" |
| 55 | + PORT = 18765; BASE = f"http://127.0.0.1:{PORT}"; URL = BASE + WPATH |
| 56 | + MATCH = f"pkg:pypi/six@{VER}"; SEEN = [] |
| 57 | + def key(p): |
| 58 | + p = urllib.parse.unquote(p).split("?")[0].lower() |
| 59 | + if "@" not in p: return p |
| 60 | + n, v = p.split("@", 1); return re.sub(r"[_.-]+", "-", n) + "@" + v |
| 61 | + VULN = {"GHSA-test-aaaa-bbbb": {"cves": ["CVE-2024-0001"], "summary": "s", "severity": "high", "description": "d"}} |
| 62 | + class H(http.server.BaseHTTPRequestHandler): |
| 63 | + def log_message(self, *a): pass |
| 64 | + def send(self, b, ct="application/octet-stream", code=200): |
| 65 | + self.send_response(code); self.send_header("content-type", ct); self.send_header("content-length", str(len(b))); self.end_headers(); self.wfile.write(b) |
| 66 | + def j(self, o, code=200): self.send(json.dumps(o).encode(), "application/json", code) |
| 67 | + def do_POST(self): |
| 68 | + n = int(self.headers.get("content-length") or 0); b = json.loads(self.rfile.read(n) or b"null") |
| 69 | + if self.path.endswith("/patches/batch"): |
| 70 | + SEEN.extend(c["purl"] for c in b.get("components", []) if "six" in c["purl"]) |
| 71 | + pk = [{"purl": c["purl"], "patches": [{"uuid": UUID, "purl": c["purl"], "tier": "free", "cveIds": [], "ghsaIds": ["GHSA-test-aaaa-bbbb"], "severity": "high", "title": "fixture"}]} for c in b.get("components", []) if key(c["purl"]) == MATCH] |
| 72 | + return self.j({"packages": pk, "canAccessPaidPatches": False}) |
| 73 | + if self.path.endswith("/patches/package"): |
| 74 | + r = {u: {"status": "granted", "url": URL, "purl": None, "artifacts": [{"kind": "tarball", "url": URL, "integrity": {"sha256": SHA, "sha512": SRI}}], "registryOverride": None} for u in b.get("uuids", []) if u == UUID} |
| 75 | + return self.j({"results": r}) |
| 76 | + self.j({}, 404) |
| 77 | + def do_GET(self): |
| 78 | + if self.path == WPATH: return self.send(WHL) |
| 79 | + if "/by-package/" in self.path: |
| 80 | + p = urllib.parse.unquote(self.path.split("/by-package/")[1]) |
| 81 | + ps = [{"uuid": UUID, "purl": p, "publishedAt": "2024-01-01T00:00:00Z", "description": "fixture", "license": "MIT", "tier": "free", "vulnerabilities": VULN}] if key(p) == MATCH else [] |
| 82 | + return self.j({"patches": ps, "canAccessPaidPatches": False}) |
| 83 | + if "/view/" in self.path: |
| 84 | + return self.j({"uuid": UUID, "purl": MATCH, "publishedAt": "2024-01-01T00:00:00Z", "files": {"six.py": {"beforeHash": g(before), "afterHash": g(after)}}, "vulnerabilities": VULN, "description": "fixture", "license": "MIT", "tier": "free"}) |
| 85 | + self.j({}, 404) |
| 86 | + srv = http.server.ThreadingHTTPServer(("127.0.0.1", PORT), H); threading.Thread(target=srv.serve_forever, daemon=True).start() |
| 87 | + ENV = dict(os.environ, SOCKET_NO_CONFIG="1", SOCKET_TELEMETRY_DISABLED="1", SOCKET_NO_UPDATE_CHECK="1") |
| 88 | + ENV.pop("VIRTUAL_ENV", None) |
| 89 | + A = ["--org", "test-org", "--api-token", "fake-token", "--api-url", BASE, "--patch-server-url", BASE] |
| 90 | + def sp(args, cwd): return run([BIN] + args + ["--cwd", cwd] + A, env=ENV) |
| 91 | + def patched(py): |
| 92 | + rc, out = run([py, "-c", "import six;print('PATCHED' if 'SOCKET-PATCHED' in open(six.__file__).read() else 'UNPATCHED')"]) |
| 93 | + return out.strip().splitlines()[-1] if rc == 0 else "NOTINSTALLED" |
| 94 | + results = [] |
| 95 | + FORMS = [f"six=={VER}", f"six == {VER}", f"six =={VER}", f"six== {VER}", f"six[x] == {VER}", f"six (=={VER})"] |
| 96 | + for i, form in enumerate(FORMS): |
| 97 | + p = os.path.join(W, f"p{i}"); os.makedirs(os.path.join(p, ".git")) |
| 98 | + open(os.path.join(p, "requirements.txt"), "w", newline="").write(form + "\nidna==3.7\n") |
| 99 | + del SEEN[:] |
| 100 | + rc, out = sp(["scan", "--mode", "hosted", "--yes"], p) |
| 101 | + txt = open(os.path.join(p, "requirements.txt")).read() |
| 102 | + rewritten = "#sha256=" in txt |
| 103 | + py = mkvenv(os.path.join(W, f"v{i}")) |
| 104 | + rc2, out2 = run([py, "-m", "pip", "install", "-q", "--no-cache-dir", "-r", os.path.join(p, "requirements.txt")]) |
| 105 | + st = patched(py) |
| 106 | + results.append((form, rc, rewritten, sorted(set(SEEN)), rc2, st)) |
| 107 | + print(f"== [{form}] scan rc={rc} rewritten={rewritten} discovered={sorted(set(SEEN))} pip rc={rc2} {st}\n{out.strip()[-400:]}\n{out2.strip()[-300:]}", flush=True) |
| 108 | + print("\n| pin | scan exit | rewritten | six purl sent to batch | pip install -r | six |\n|---|---|---|---|---|---|") |
| 109 | + for r in results: print("| `%s` | %s | %s | %s | %s | %s |" % r) |
| 110 | + PROBE_EOF |
| 111 | + BIN=target/debug/socket-patch |
| 112 | + if [ "$RUNNER_OS" = Windows ]; then BIN=target/debug/socket-patch.exe; fi |
| 113 | + python probe.py "$BIN" "${{ matrix.combo.pip }}" 2>&1 | tee probe.log |
0 commit comments