|
| 1 | +name: bughunt uv probe |
| 2 | +on: |
| 3 | + push: |
| 4 | + branches: ["bughunt/uv/**"] |
| 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 | + defaults: |
| 16 | + run: |
| 17 | + shell: bash |
| 18 | + steps: |
| 19 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
| 20 | + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 |
| 21 | + with: |
| 22 | + python-version: "3.11" |
| 23 | + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 |
| 24 | + - name: Build CLI |
| 25 | + run: cargo build --release -p socket-patch-cli |
| 26 | + - name: Write probe files |
| 27 | + run: | |
| 28 | + mkdir -p "$RUNNER_TEMP/mock" "$RUNNER_TEMP/work" |
| 29 | + cat > "$RUNNER_TEMP/mock/mock.py" <<'PYEOF' |
| 30 | + #!/usr/bin/env python3 |
| 31 | + """Local mock of the Socket patch API serving one patched pypi package (six 1.16.0).""" |
| 32 | + import base64, hashlib, io, json, os, re, sys, zipfile |
| 33 | + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer |
| 34 | +
|
| 35 | + HERE = os.path.dirname(os.path.abspath(__file__)) |
| 36 | + PORT = int(os.environ.get("MOCK_PORT", "18080")) |
| 37 | + ORG = "test-org" |
| 38 | + UUID = "0f6e7c1a-1111-4222-8333-444455556666" |
| 39 | + TOKEN = "11111111-2222-4333-8444-555555555555" |
| 40 | + SUFFIX = b"\n# SOCKET-PATCHED\nSOCKET_PATCHED = 1\n" |
| 41 | + NAME, VER = "six", "1.16.0" |
| 42 | + PURL = f"pkg:pypi/{NAME}@{VER}" |
| 43 | + LEAF = f"{NAME}-{VER}-py2.py3-none-any.whl" |
| 44 | + LOG = os.path.join(HERE, "requests.log") |
| 45 | +
|
| 46 | +
|
| 47 | + def git_sha256(b): |
| 48 | + return hashlib.sha256(b"blob %d\0" % len(b) + b).hexdigest() |
| 49 | +
|
| 50 | +
|
| 51 | + def build(): |
| 52 | + src = zipfile.ZipFile(os.path.join(HERE, LEAF)) |
| 53 | + members = [] |
| 54 | + dist = f"{NAME}-{VER}.dist-info" |
| 55 | + for info in src.infolist(): |
| 56 | + if info.filename == f"{dist}/RECORD": |
| 57 | + continue |
| 58 | + data = src.read(info.filename) |
| 59 | + if info.filename == "six.py": |
| 60 | + orig = data |
| 61 | + data = data + SUFFIX |
| 62 | + members.append((info.filename, data)) |
| 63 | + rec = "" |
| 64 | + for n, d in members: |
| 65 | + dig = base64.urlsafe_b64encode(hashlib.sha256(d).digest()).rstrip(b"=").decode() |
| 66 | + rec += f"{n},sha256={dig},{len(d)}\n" |
| 67 | + rec += f"{dist}/RECORD,,\n" |
| 68 | + members.append((f"{dist}/RECORD", rec.encode())) |
| 69 | + buf = io.BytesIO() |
| 70 | + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z: |
| 71 | + for n, d in members: |
| 72 | + zi = zipfile.ZipInfo(n, date_time=(2020, 1, 1, 0, 0, 0)); zi.compress_type = zipfile.ZIP_DEFLATED |
| 73 | + z.writestr(zi, d) |
| 74 | + return orig, orig + SUFFIX, buf.getvalue() |
| 75 | +
|
| 76 | +
|
| 77 | + ORIG, PATCHED, WHEEL = build() |
| 78 | + SHA = hashlib.sha256(WHEEL).hexdigest() |
| 79 | + BASE = f"http://127.0.0.1:{PORT}" |
| 80 | + ART_PATH = f"/patch/pypi/{NAME}/{VER}/{TOKEN}/{UUID}/{LEAF}" |
| 81 | + ART = BASE + ART_PATH |
| 82 | + VIEW = { |
| 83 | + "uuid": UUID, "purl": PURL, "publishedAt": "Fri, 27 Mar 2026 00:00:00 GMT", |
| 84 | + "files": {"six.py": {"beforeHash": git_sha256(ORIG), "afterHash": git_sha256(PATCHED), |
| 85 | + "blobContent": base64.b64encode(PATCHED).decode()}}, |
| 86 | + "vulnerabilities": {"GHSA-uvbu-ildv-ex01": {"cves": ["CVE-2026-7101"], "summary": "s", |
| 87 | + "severity": "high", "description": "d"}}, |
| 88 | + "description": "uv bughunt patch", "license": "MIT", "tier": "free", |
| 89 | + } |
| 90 | +
|
| 91 | +
|
| 92 | + class H(BaseHTTPRequestHandler): |
| 93 | + def log_message(self, *a): |
| 94 | + pass |
| 95 | +
|
| 96 | + def send(self, code, body, ctype="application/json"): |
| 97 | + if not isinstance(body, bytes): |
| 98 | + body = json.dumps(body).encode() |
| 99 | + self.send_response(code) |
| 100 | + self.send_header("Content-Type", ctype) |
| 101 | + self.send_header("Content-Length", str(len(body))) |
| 102 | + self.end_headers() |
| 103 | + self.wfile.write(body) |
| 104 | +
|
| 105 | + def route(self): |
| 106 | + with open(LOG, "a") as f: |
| 107 | + f.write(f"{self.command} {self.path}\n") |
| 108 | + p = self.path.split("?")[0] |
| 109 | + n = int(self.headers.get("Content-Length") or 0) |
| 110 | + body = self.rfile.read(n) if n else b"" |
| 111 | + if p == ART_PATH: |
| 112 | + if self.command == "HEAD": |
| 113 | + self.send_response(200); self.send_header("Content-Length", str(len(WHEEL))); self.end_headers(); return |
| 114 | + return self.send(200, WHEEL, "application/octet-stream") |
| 115 | + if re.fullmatch(rf"/v0/orgs/{ORG}/patches/batch", p) or p.endswith("/patch/batch"): |
| 116 | + try: |
| 117 | + comps = json.loads(body or b"{}").get("components", []) |
| 118 | + except Exception: |
| 119 | + comps = [] |
| 120 | + purls = [c.get("purl", "") for c in comps] if comps else [PURL] |
| 121 | + if not any(pu.lower().startswith(f"pkg:pypi/{NAME}@{VER}") for pu in purls): |
| 122 | + return self.send(200, {"packages": [], "canAccessPaidPatches": False}) |
| 123 | + return self.send(200, {"packages": [{"purl": PURL, "patches": [{ |
| 124 | + "uuid": UUID, "purl": PURL, "tier": "free", "cveIds": ["CVE-2026-7101"], |
| 125 | + "ghsaIds": ["GHSA-uvbu-ildv-ex01"], "severity": "HIGH", "title": "uv bughunt"}]}], |
| 126 | + "canAccessPaidPatches": False}) |
| 127 | + if "/patches/by-package/" in p or "/patch/by-package/" in p: |
| 128 | + return self.send(200, {"patches": [{"uuid": UUID, "purl": PURL, |
| 129 | + "publishedAt": "2026-09-01T00:00:00Z", "description": "uv bughunt", |
| 130 | + "license": "MIT", "tier": "free", "vulnerabilities": {}}], |
| 131 | + "canAccessPaidPatches": False}) |
| 132 | + if p.endswith("/patches/package") or p.endswith("/patch/package"): |
| 133 | + return self.send(200, {"results": {UUID: {"status": "granted", "url": ART, "purl": PURL, |
| 134 | + "artifacts": [{"kind": "tarball", "url": ART, "integrity": {"sha256": SHA}}], |
| 135 | + "registryOverride": None}}}) |
| 136 | + if p.endswith(f"/view/{UUID}"): |
| 137 | + return self.send(200, VIEW) |
| 138 | + if "/blob/" in p: |
| 139 | + h = p.rsplit("/", 1)[1] |
| 140 | + if h == git_sha256(PATCHED): |
| 141 | + return self.send(200, PATCHED, "application/octet-stream") |
| 142 | + if h == git_sha256(ORIG): |
| 143 | + return self.send(200, ORIG, "application/octet-stream") |
| 144 | + return self.send(404, {"error": "not found", "path": p}) |
| 145 | +
|
| 146 | + do_GET = route |
| 147 | + do_POST = route |
| 148 | + do_HEAD = route |
| 149 | +
|
| 150 | +
|
| 151 | + if __name__ == "__main__": |
| 152 | + print(json.dumps({"art": ART, "sha": SHA, "patched_py_sha": hashlib.sha256(PATCHED).hexdigest(), |
| 153 | + "orig_py_sha": hashlib.sha256(ORIG).hexdigest()}), flush=True) |
| 154 | + ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever() |
| 155 | + PYEOF |
| 156 | + cat > "$RUNNER_TEMP/driver.py" <<'PYEOF' |
| 157 | + #!/usr/bin/env python3 |
| 158 | + """uv vendored repair probe (lock-only checkout, committed wheel deleted). usage: driver.py SOCKET_PATCH UV WORKDIR""" |
| 159 | + import glob, json, os, shutil, subprocess, sys, hashlib |
| 160 | +
|
| 161 | + SP, UV, WORK = sys.argv[1], sys.argv[2], sys.argv[3] |
| 162 | + API = ["--api-url", "http://127.0.0.1:18080", "--api-token", "fake-token", "--org", "test-org"] |
| 163 | + ENV = dict(os.environ, SOCKET_TELEMETRY_DISABLED="1", SOCKET_NO_CONFIG="1") |
| 164 | + PYPROJECT = '[project]\nname = "uvp"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0"]\n' |
| 165 | + PATCHED = "c0c1c71ca455" |
| 166 | +
|
| 167 | +
|
| 168 | + def run(cmd, cwd, **kw): |
| 169 | + return subprocess.run(cmd, cwd=cwd, env=kw.get("env", ENV), capture_output=True, text=True, encoding="utf-8", errors="replace") |
| 170 | +
|
| 171 | +
|
| 172 | + def sp(args, cwd): |
| 173 | + p = run([SP] + args + API, cwd) |
| 174 | + try: |
| 175 | + return p.returncode, json.loads(p.stdout) |
| 176 | + except Exception: |
| 177 | + return p.returncode, {"raw": p.stdout[-500:], "stderr": p.stderr[-500:]} |
| 178 | +
|
| 179 | +
|
| 180 | + def six_sha(d): |
| 181 | + hits = glob.glob(os.path.join(d, ".venv", "**", "six.py"), recursive=True) |
| 182 | + return hashlib.sha256(open(hits[0], "rb").read()).hexdigest()[:12] if hits else None |
| 183 | +
|
| 184 | +
|
| 185 | + def install(d, tag, *flags): |
| 186 | + shutil.rmtree(os.path.join(d, ".venv"), ignore_errors=True) |
| 187 | + env = dict(ENV, UV_CACHE_DIR=os.path.join(WORK, "cache-" + tag)) |
| 188 | + p = run([UV, "sync"] + list(flags), d, env=env) |
| 189 | + return {"rc": p.returncode, "six": six_sha(d), "err": p.stderr[-300:] if p.returncode else ""} |
| 190 | +
|
| 191 | +
|
| 192 | + def git(d, *a): |
| 193 | + return run(["git"] + list(a), d) |
| 194 | +
|
| 195 | +
|
| 196 | + shutil.rmtree(WORK, ignore_errors=True) |
| 197 | + proj = os.path.join(WORK, "proj") |
| 198 | + os.makedirs(proj) |
| 199 | + with open(os.path.join(proj, "pyproject.toml"), "w", newline="\n") as f: |
| 200 | + f.write(PYPROJECT) |
| 201 | + with open(os.path.join(proj, ".gitignore"), "w", newline="\n") as f: |
| 202 | + f.write(".venv\n") |
| 203 | + r = {} |
| 204 | + for c in (["lock", "-q"], ["sync", "-q"]): |
| 205 | + run([UV] + c, proj) |
| 206 | + rc, s = sp(["scan", "--mode", "vendored", "--json", "--yes"], proj) |
| 207 | + r["vendorScan"] = [rc, s.get("vendor", {}).get("summary", {}).get("applied")] |
| 208 | + git(proj, "init", "-q", ".") |
| 209 | + git(proj, "add", "-A") |
| 210 | + git(proj, "commit", "-qm", "vendored") |
| 211 | + r["committed"] = git(proj, "ls-files", ".socket").stdout.split() |
| 212 | + def repair_case(tag, with_venv): |
| 213 | + clone = os.path.join(WORK, "clone-" + tag) |
| 214 | + git(WORK, "clone", "-q", proj, clone) |
| 215 | + c = {} |
| 216 | + if with_venv: |
| 217 | + c["preInstall"] = install(clone, tag + "0", "--locked") |
| 218 | + wheels = glob.glob(os.path.join(clone, ".socket", "vendor", "pypi", "*", "*.whl")) |
| 219 | + for w in wheels: |
| 220 | + os.remove(w) |
| 221 | + rc, rep = sp(["repair", "--json"], clone) |
| 222 | + c["repair"] = {"rc": rc, "status": rep.get("status"), |
| 223 | + "events": [(e.get("action"), e.get("errorCode"), (e.get("error") or "")[:260]) for e in rep.get("events", [])]} |
| 224 | + c["wheelBack"] = bool(glob.glob(os.path.join(clone, ".socket", "vendor", "pypi", "*", "*.whl"))) |
| 225 | + c["afterRepairLocked"] = install(clone, tag + "1", "--locked") |
| 226 | + c["gitStatus"] = git(clone, "status", "--short").stdout.strip().splitlines() |
| 227 | + ok = rc == 0 and c["wheelBack"] and c["afterRepairLocked"]["six"] == PATCHED and c["afterRepairLocked"]["rc"] == 0 |
| 228 | + c["result"] = "PASS" if ok else "FAIL" |
| 229 | + return c |
| 230 | +
|
| 231 | +
|
| 232 | + r["lockOnly"] = repair_case("lockonly", False) |
| 233 | + r["withPatchedVenv"] = repair_case("venv", True) |
| 234 | + r["uv"] = subprocess.run([UV, "--version"], capture_output=True, text=True).stdout.strip() |
| 235 | + print("RESULT " + json.dumps(r)) |
| 236 | + PYEOF |
| 237 | + python -m pip download --no-deps six==1.16.0 -d "$RUNNER_TEMP/mock" |
| 238 | + - name: Run probe |
| 239 | + run: | |
| 240 | + python "$RUNNER_TEMP/mock/mock.py" > "$RUNNER_TEMP/mock.out" 2>&1 & |
| 241 | + sleep 3; cat "$RUNNER_TEMP/mock.out" |
| 242 | + SP="$PWD/target/release/socket-patch"; [ -f "$SP.exe" ] && SP="$SP.exe" |
| 243 | + git config --global user.email probe@example.com; git config --global user.name probe |
| 244 | + for v in 0.2.37 0.12.21; do |
| 245 | + python -m venv "$RUNNER_TEMP/uv-$v" |
| 246 | + if [ -d "$RUNNER_TEMP/uv-$v/Scripts" ]; then B="$RUNNER_TEMP/uv-$v/Scripts"; else B="$RUNNER_TEMP/uv-$v/bin"; fi |
| 247 | + "$B/python" -m pip install -q "uv==$v" |
| 248 | + UVB="$B/uv"; [ -f "$UVB.exe" ] && UVB="$UVB.exe" |
| 249 | + python "$RUNNER_TEMP/driver.py" "$SP" "$UVB" "$RUNNER_TEMP/work/$v" || true |
| 250 | + done |
0 commit comments