|
| 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 | + python: ['3.8', '3.13'] |
| 14 | + pip: ['20.3.4', 'latest'] |
| 15 | + runs-on: ${{ matrix.os }} |
| 16 | + timeout-minutes: 45 |
| 17 | + defaults: |
| 18 | + run: |
| 19 | + shell: bash |
| 20 | + steps: |
| 21 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
| 22 | + - run: rustup show |
| 23 | + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 |
| 24 | + with: |
| 25 | + save-if: false |
| 26 | + - run: cargo build --release -p socket-patch-cli |
| 27 | + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 |
| 28 | + with: |
| 29 | + python-version: ${{ matrix.python }} |
| 30 | + - name: probe |
| 31 | + run: | |
| 32 | + set -x |
| 33 | + W="$RUNNER_TEMP/w"; mkdir -p "$W"; cd "$W" |
| 34 | + BIN="$GITHUB_WORKSPACE/target/release/socket-patch"; [ -f "$BIN.exe" ] && BIN="$BIN.exe" |
| 35 | + python -m venv pv |
| 36 | + if [ -d pv/Scripts ]; then PY=pv/Scripts/python; else PY=pv/bin/python; fi |
| 37 | + if [ "${{ matrix.pip }}" = latest ]; then $PY -m pip install -q -U pip; else $PY -m pip install -q "pip==${{ matrix.pip }}"; fi |
| 38 | + $PY -m pip --version |
| 39 | + $PY -m pip download -q six==1.16.0 --no-deps -d dl |
| 40 | + cat > mock.py <<'PYEOF' |
| 41 | + import base64, hashlib, json, re, sys, zipfile, io |
| 42 | + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer |
| 43 | + PORT=int(sys.argv[1]) if len(sys.argv)>1 else 8765 |
| 44 | + ORG="test-org"; PURL="pkg:pypi/six@1.16.0"; UUID="5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d" |
| 45 | + TOKEN="11111111-2222-4333-8444-555555555555"; WHEEL="six-1.16.0-py2.py3-none-any.whl" |
| 46 | + GHSA="GHSA-real-pypi-0001"; CVE="CVE-2026-7201" |
| 47 | + orig=zipfile.ZipFile("dl/"+WHEEL).read("six.py") |
| 48 | + patched=orig+b"\n# SOCKET-PATCHED\nSOCKET_PATCHED = 1\n" |
| 49 | + def d(b): return base64.urlsafe_b64encode(hashlib.sha256(b).digest()).rstrip(b"=").decode() |
| 50 | + meta=b"Metadata-Version: 2.1\nName: six\nVersion: 1.16.0\nSummary: x\n" |
| 51 | + wh=b"Wheel-Version: 1.0\nGenerator: t\nRoot-Is-Purelib: true\nTag: py2-none-any\nTag: py3-none-any\n" |
| 52 | + mem=[("six.py",patched),("six-1.16.0.dist-info/METADATA",meta),("six-1.16.0.dist-info/WHEEL",wh)] |
| 53 | + rec="".join(f"{n},sha256={d(b)},{len(b)}\n" for n,b in mem)+"six-1.16.0.dist-info/RECORD,,\n" |
| 54 | + buf=io.BytesIO(); z=zipfile.ZipFile(buf,"w") |
| 55 | + for n,b in mem+[("six-1.16.0.dist-info/RECORD",rec.encode())]: z.writestr(n,b) |
| 56 | + z.close(); wheel=buf.getvalue() |
| 57 | + sha256=hashlib.sha256(wheel).hexdigest(); sha512="sha512-"+base64.b64encode(hashlib.sha512(wheel).digest()).decode() |
| 58 | + def g(b): return hashlib.sha256(b"blob %d\0"%len(b)+b).hexdigest() |
| 59 | + BASE=f"http://127.0.0.1:{PORT}" |
| 60 | + APATH=f"/patch/pypi/six/1.16.0/{TOKEN}/{UUID}/{WHEEL}" |
| 61 | + view={"uuid":UUID,"purl":PURL,"publishedAt":"Fri, 27 Mar 2026 00:00:00 GMT","files":{"six.py":{"beforeHash":g(orig),"afterHash":g(patched),"blobContent":base64.b64encode(patched).decode()}}, |
| 62 | + "vulnerabilities":{GHSA:{"cves":[CVE],"summary":"s","severity":"high","description":"d"}},"description":"x","license":"MIT","tier":"free"} |
| 63 | + class H(BaseHTTPRequestHandler): |
| 64 | + def log_message(self,*a): sys.stderr.write("REQ "+self.command+" "+self.path+"\n") |
| 65 | + def j(self,o): |
| 66 | + 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) |
| 67 | + def do_HEAD(self): |
| 68 | + p=self.path.split("?")[0] |
| 69 | + if p==APATH: self.send_response(200); self.send_header("content-length",str(len(wheel))); self.end_headers() |
| 70 | + else: self.send_response(404); self.end_headers() |
| 71 | + def do_GET(self): |
| 72 | + p=self.path.split("?")[0] |
| 73 | + if p==APATH: |
| 74 | + 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) |
| 75 | + elif p.endswith("/patches/view/"+UUID) or p=="/patch/view/"+UUID: self.j(view) |
| 76 | + elif re.match(f"^/v0/orgs/{ORG}/patches/by-package/.+$",p): |
| 77 | + self.j({"patches":[{"uuid":UUID,"purl":PURL,"publishedAt":"2026-03-27T00:00:00Z","description":"x","license":"MIT","tier":"free","vulnerabilities":{}}],"canAccessPaidPatches":False}) |
| 78 | + else: self.send_response(404); self.end_headers() |
| 79 | + def do_POST(self): |
| 80 | + n=int(self.headers.get("content-length",0)); body=self.rfile.read(n) |
| 81 | + p=self.path.split("?")[0] |
| 82 | + if p.endswith("/patches/batch"): |
| 83 | + want=PURL in body.decode(errors="replace") |
| 84 | + self.j({"packages":[{"purl":PURL,"patches":[{"uuid":UUID,"purl":PURL,"tier":"free","cveIds":[CVE],"ghsaIds":[GHSA],"severity":"high","title":"t"}]}] if want else [],"canAccessPaidPatches":False}) |
| 85 | + elif p.endswith("/patches/package"): |
| 86 | + u=BASE+APATH |
| 87 | + self.j({"results":{UUID:{"status":"granted","url":u,"purl":PURL,"artifacts":[{"kind":"tarball","url":u,"integrity":{"sha256":sha256,"sha512":sha512}}],"registryOverride":None}}}) |
| 88 | + else: self.send_response(404); self.end_headers() |
| 89 | + print("wheel sha256",sha256,flush=True) |
| 90 | + ThreadingHTTPServer(("127.0.0.1",PORT),H).serve_forever() |
| 91 | + PYEOF |
| 92 | + python mock.py 8765 > mock.log 2>&1 & |
| 93 | + sleep 3; cat mock.log |
| 94 | + export NO_PROXY=127.0.0.1 SOCKET_TELEMETRY_DISABLED=1 SOCKET_NO_CONFIG=1 |
| 95 | + A="--api-url http://127.0.0.1:8765 --api-token fake --org test-org --patch-server-url http://127.0.0.1:8765" |
| 96 | + echo "#### C: venv --system-site-packages; six 1.16.0 in the BASE interpreter" |
| 97 | + python -m pip install -q six==1.16.0 |
| 98 | + for mode in hosted vendored; do |
| 99 | + d="c-$mode"; rm -rf "$d"; mkdir "$d"; cd "$d"; printf 'six==1.16.0\n' > requirements.txt |
| 100 | + ../$PY -m venv --system-site-packages .venv |
| 101 | + if [ -d .venv/Scripts ]; then VP=.venv/Scripts/python; else VP=.venv/bin/python; fi |
| 102 | + if [ "${{ matrix.pip }}" = latest ]; then $VP -m pip install -q -U pip; else $VP -m pip install -q "pip==${{ matrix.pip }}"; fi |
| 103 | + "$BIN" scan --mode $mode --json $A --cwd . > scan.json 2> scan.err; src=$? |
| 104 | + warns=$(python -c "import json;d=json.load(open('scan.json'));print([w.get('code') for w in d.get('redirect',{}).get('warnings',[])], d.get('status'))") |
| 105 | + $VP -m pip install -q --disable-pip-version-check -r requirements.txt > pip.txt 2>&1; prc=$? |
| 106 | + six=$($VP -c "import six;print(six.__file__, 'PATCHED' if getattr(six,'SOCKET_PATCHED',0) else 'UNPATCHED')") |
| 107 | + "$BIN" vex $A --cwd . --product pkg:pypi/app@1 --output vex.json > vex.out 2>&1; vrc=$? |
| 108 | + st=$(python -c "import json;print([s['status'] for s in json.load(open('vex.json'))['statements']])" 2>/dev/null) |
| 109 | + oos=$(grep -c 'does not match' vex.out) |
| 110 | + echo "RESULT C-$mode scan_rc=$src warnings=$warns pip_rc=$prc six=$six vex_rc=$vrc vex=$st out_of_sync_warn=$oos" |
| 111 | + cat scan.err | grep -v -i token | tail -3 |
| 112 | + cd .. |
| 113 | + done |
0 commit comments