|
| 1 | +name: bughunt pip probe (global --user) |
| 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 | + include: |
| 14 | + - python: '3.8' |
| 15 | + pipv: '20.3.4' |
| 16 | + - python: '3.13' |
| 17 | + pipv: '' |
| 18 | + python: ['3.8', '3.13'] |
| 19 | + runs-on: ${{ matrix.os }} |
| 20 | + timeout-minutes: 45 |
| 21 | + steps: |
| 22 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
| 23 | + with: |
| 24 | + persist-credentials: false |
| 25 | + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 |
| 26 | + with: |
| 27 | + python-version: ${{ matrix.python }} |
| 28 | + - name: Install Rust |
| 29 | + run: rustup show |
| 30 | + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 |
| 31 | + with: |
| 32 | + key: bughunt-pip-g |
| 33 | + save-if: false |
| 34 | + - name: Build the CLI |
| 35 | + shell: bash |
| 36 | + run: cargo build --locked -p socket-patch-cli |
| 37 | + - name: Write probe |
| 38 | + shell: bash |
| 39 | + run: | |
| 40 | + cat > "$RUNNER_TEMP/probe_g.py" <<'PYEOF' |
| 41 | + import base64, csv, hashlib, http.server, io, json, os, subprocess, sys, tempfile, threading, urllib.request, zipfile |
| 42 | + CLI = os.environ["SP_CLI"]; WIN = os.name == "nt" |
| 43 | + UUID = "4d5e6f70-8192-4a1b-8c2d-0123456789ab"; TOKEN = "11111111-1111-4111-8111-111111111111" |
| 44 | + work = tempfile.mkdtemp(prefix="bh pip ") |
| 45 | + def run(cmd, cwd=None, env=None, check=False): |
| 46 | + p = subprocess.run(cmd, cwd=cwd, env=env, capture_output=True, text=True) |
| 47 | + if check and p.returncode: print(p.stdout, p.stderr); raise SystemExit(f"failed: {cmd}") |
| 48 | + return p |
| 49 | + # patched wheel |
| 50 | + up = os.path.join(work, "six-1.16.0-py2.py3-none-any.whl") |
| 51 | + urllib.request.urlretrieve("https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", up) |
| 52 | + zin = zipfile.ZipFile(up); buf = io.BytesIO(); zout = zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) |
| 53 | + ORIG = zin.read("six.py"); patched_six = ORIG + b"\nSOCKET_PATCHED = 1\n" |
| 54 | + def gsha(b): return hashlib.sha256(b"blob %d\0" % len(b) + b).hexdigest() |
| 55 | + for item in zin.infolist(): |
| 56 | + data = zin.read(item.filename) |
| 57 | + if item.filename == "six.py": data = patched_six |
| 58 | + if item.filename.endswith("RECORD"): |
| 59 | + rows = [] |
| 60 | + for row in csv.reader(io.StringIO(data.decode())): |
| 61 | + if row and row[0] == "six.py": |
| 62 | + row = ["six.py", "sha256=" + base64.urlsafe_b64encode(hashlib.sha256(patched_six).digest()).rstrip(b"=").decode(), str(len(patched_six))] |
| 63 | + rows.append(row) |
| 64 | + s = io.StringIO(); csv.writer(s, lineterminator="\n").writerows(rows); data = s.getvalue().encode() |
| 65 | + zout.writestr(item, data) |
| 66 | + zout.close(); WHL = buf.getvalue(); SHA = hashlib.sha256(WHL).hexdigest() |
| 67 | + class H(http.server.BaseHTTPRequestHandler): |
| 68 | + def log_message(self, *a): pass |
| 69 | + def send(self, code, body, ct="application/json"): |
| 70 | + b = body if isinstance(body, bytes) else json.dumps(body).encode() |
| 71 | + self.send_response(code); self.send_header("Content-Type", ct); self.send_header("Content-Length", str(len(b))); self.end_headers() |
| 72 | + if self.command != "HEAD": self.wfile.write(b) |
| 73 | + def do_HEAD(self): self.do_GET() |
| 74 | + def do_GET(self): |
| 75 | + if self.path.endswith(".whl"): return self.send(200, WHL, "application/octet-stream") |
| 76 | + if "/patches/blob/" in self.path or "/patch/blob/" in self.path: |
| 77 | + h = self.path.rsplit("/", 1)[1] |
| 78 | + for b in (ORIG, patched_six): |
| 79 | + if gsha(b) == h: return self.send(200, b, "application/octet-stream") |
| 80 | + return self.send(404, {"error": "nf"}) |
| 81 | + if "/patches/view/" in self.path: |
| 82 | + return self.send(200, {"uuid": UUID, "purl": "pkg:pypi/six@1.16.0", "publishedAt": "2024-01-01T00:00:00Z", "files": {"six.py": {"beforeHash": gsha(ORIG), "afterHash": gsha(patched_six), "blobContent": base64.b64encode(patched_six).decode()}}, "vulnerabilities": {"GHSA-xxxx-yyyy-zzzz": {"cves": ["CVE-2024-88888"], "summary": "s", "severity": "high", "description": "d"}}, "description": "d", "license": "MIT", "tier": "free"}) |
| 83 | + if "/patches/by-package/" in self.path: |
| 84 | + hit = "six%401.16.0" in self.path |
| 85 | + return self.send(200, {"patches": [{"uuid": UUID, "purl": "pkg:pypi/six@1.16.0", "publishedAt": "2024-01-01T00:00:00Z", "description": "d", "license": "MIT", "tier": "free", "vulnerabilities": {"GHSA-xxxx-yyyy-zzzz": {"cves": ["CVE-2024-88888"], "summary": "s", "severity": "high", "description": "d"}}}] if hit else [], "canAccessPaidPatches": True}) |
| 86 | + self.send(404, {"error": "nf"}) |
| 87 | + def do_POST(self): |
| 88 | + body = self.rfile.read(int(self.headers.get("Content-Length", 0))) |
| 89 | + if self.path.endswith("/patches/package"): |
| 90 | + url = f"http://127.0.0.1:{PORT}/patch/pypi/six/1.16.0/{TOKEN}/{UUID}/six-1.16.0-py2.py3-none-any.whl" |
| 91 | + return self.send(200, {"results": {UUID: {"status": "granted", "url": url, "purl": "pkg:pypi/six@1.16.0", "artifacts": [{"kind": "tarball", "url": url, "integrity": {"sha256": SHA}}], "registryOverride": None}}}) |
| 92 | + if self.path.endswith("/patches/batch"): |
| 93 | + comps = [c["purl"] for c in json.loads(body or b"{}").get("components", [])] |
| 94 | + pk = [{"purl": "pkg:pypi/six@1.16.0", "patches": [{"uuid": UUID, "purl": "pkg:pypi/six@1.16.0", "tier": "free", "cveIds": ["CVE-2024-88888"], "ghsaIds": ["GHSA-xxxx-yyyy-zzzz"], "severity": "high", "title": "t"}]}] if "pkg:pypi/six@1.16.0" in comps else [] |
| 95 | + return self.send(200, {"packages": pk, "canAccessPaidPatches": True}) |
| 96 | + self.send(200, {}) |
| 97 | + srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), H); PORT = srv.server_address[1] |
| 98 | + threading.Thread(target=srv.serve_forever, daemon=True).start() |
| 99 | + |
| 100 | + import shutil, site |
| 101 | + env = {k: v for k, v in os.environ.items() if not k.startswith("SOCKET_") and k != "VIRTUAL_ENV"} |
| 102 | + env["SOCKET_API_TOKEN"] = "sktsec_" + "a" * 44 + "_api" |
| 103 | + API = ["--api-url", f"http://127.0.0.1:{PORT}", "--org", "test-org"] |
| 104 | + py = sys.executable |
| 105 | + if os.environ.get("PIPV"): run([py, "-m", "pip", "install", "-q", "--disable-pip-version-check", "pip==" + os.environ["PIPV"]], check=True) |
| 106 | + print("pip", run([py, "-m", "pip", "--version"]).stdout.strip()) |
| 107 | + r = run([py, "-m", "pip", "install", "--disable-pip-version-check", "--user", "--force-reinstall", "six==1.16.0"] + os.environ.get("PIP_EXTRA", "").split()) |
| 108 | + print("user install rc", r.returncode, r.stderr[-400:]) |
| 109 | + usersite = run([py, "-c", "import site;print(site.getusersitepackages())"]).stdout.strip() |
| 110 | + six_path = os.path.join(usersite, "six.py"); print("USER_SITE", usersite, os.path.exists(six_path)) |
| 111 | + before = open(six_path, "rb").read() if os.path.exists(six_path) else b"" |
| 112 | + proj = os.path.join(work, "outside"); os.makedirs(proj) |
| 113 | + def cli(*args): |
| 114 | + p = run([CLI, *args, "--json", "--cwd", proj] + API, cwd=proj, env=env) |
| 115 | + try: return p.returncode, json.loads(p.stdout) |
| 116 | + except Exception: return p.returncode, {"raw": p.stdout[-600:], "err": p.stderr[-600:]} |
| 117 | + def oracle(): |
| 118 | + o = run([py, "-c", "import six;print('PATCHED' if getattr(six,'SOCKET_PATCHED',0) else 'UNPATCHED', six.__file__)"], cwd=proj) |
| 119 | + return o.stdout.strip() or o.stderr.strip()[-200:] |
| 120 | + rc, j = cli("scan", "-g") |
| 121 | + purls = [p.get("purl") for p in j.get("packages", [])] if isinstance(j.get("packages"), list) else j |
| 122 | + print(f"CELL scan-g-report rc={rc} scanned={j.get('scannedPackages')} six_found={'pkg:pypi/six@1.16.0' in str(purls)} status={j.get('status')} file_unchanged={open(six_path,'rb').read()==before}") |
| 123 | + rc, j = cli("scan", "-g", "--mode", "hosted", "--yes") |
| 124 | + print(f"CELL scan-g-hosted-refused rc={rc} status={j.get('status')} err={str(j.get('error') or j.get('raw'))[:200]}") |
| 125 | + rc, j = cli("get", UUID, "-g", "--yes") |
| 126 | + print(f"CELL get-g rc={rc} status={j.get('status')} oracle={oracle()} detail={json.dumps(j)[:400]}") |
| 127 | + rc, j = cli("rollback", "-g", "--yes") |
| 128 | + print(f"CELL rollback-g rc={rc} status={j.get('status')} byte_exact={open(six_path,'rb').read()==before} oracle={oracle()}") |
| 129 | + print("ROLLBACK_JSON", json.dumps(j)[:1500]) |
| 130 | + PYEOF |
| 131 | + - name: Run probe |
| 132 | + shell: bash |
| 133 | + env: |
| 134 | + PIPV: ${{ matrix.pipv }} |
| 135 | + SOCKET_NO_CONFIG: '1' |
| 136 | + run: | |
| 137 | + if [ "$RUNNER_OS" = Windows ]; then export SP_CLI="$GITHUB_WORKSPACE/target/debug/socket-patch.exe"; else export SP_CLI="$GITHUB_WORKSPACE/target/debug/socket-patch"; fi |
| 138 | + python "$RUNNER_TEMP/probe_g.py" |
0 commit comments