Skip to content

Commit 975f9a8

Browse files
committed
bughunt uv probe: vendored dependency-group drift after uv add --dev
1 parent 045d7ec commit 975f9a8

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

‎.github/workflows/bughunt-uv.yml‎

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
name: bughunt-uv
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+
uv: ['0.8.17', '0.12.23']
14+
runs-on: ${{ matrix.os }}
15+
timeout-minutes: 45
16+
defaults:
17+
run:
18+
shell: bash
19+
steps:
20+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
21+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
22+
with:
23+
python-version: '3.11'
24+
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
25+
- run: cargo build --release -p socket-patch-cli
26+
- name: tools
27+
run: |
28+
python -m pip install -q "uv==${{ matrix.uv }}"
29+
mkdir -p "$RUNNER_TEMP/h/wh"
30+
cd "$RUNNER_TEMP/h"
31+
for spec in six==1.16.0 idna==3.7; do python -m pip download -q --no-deps -d wh "$spec"; done
32+
python -m pip download -q --no-deps -d wh --only-binary=:all: --python-version 3.11 --platform manylinux_2_17_x86_64 MarkupSafe==2.1.5
33+
cat > mock.py <<'MOCKEOF'
34+
import base64, hashlib, http.server, io, json, os, re, sys, urllib.request, zipfile, ssl
35+
S = os.path.dirname(os.path.abspath(__file__))
36+
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8765
37+
BASE = f"http://127.0.0.1:{PORT}"
38+
TOKEN = "11111111-2222-4333-8444-555555555555"
39+
SUFFIX = b"\n# SOCKET-PATCHED\nSOCKET_PATCHED = 1\n"
40+
def gsha(b): return hashlib.sha256(b"blob %d\0" % len(b) + b).hexdigest()
41+
def patch_wheel(src, target):
42+
zin = zipfile.ZipFile(src); out = io.BytesIO(); members = []
43+
rec_name = [n for n in zin.namelist() if n.endswith(".dist-info/RECORD")][0]
44+
before = after = None
45+
for n in sorted(zin.namelist()):
46+
if n == rec_name: continue
47+
b = zin.read(n)
48+
if n == target: before = b; b = b + SUFFIX; after = b
49+
members.append((n, b))
50+
rec = ""
51+
for n, b in members:
52+
d = base64.urlsafe_b64encode(hashlib.sha256(b).digest()).rstrip(b"=").decode()
53+
rec += f"{n},sha256={d},{len(b)}\n"
54+
rec += f"{rec_name},,\n"; members.append((rec_name, rec.encode()))
55+
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
56+
for n, b in members:
57+
zi = zipfile.ZipInfo(n, (2020,1,1,0,0,0)); zi.compress_type = zipfile.ZIP_DEFLATED; zi.external_attr = 0o644 << 16
58+
z.writestr(zi, b)
59+
return out.getvalue(), before, after
60+
PATCHES = {}
61+
def add(uuid, name, ver, whl, target):
62+
data, before, after = patch_wheel(os.path.join(S, "wh", whl), target)
63+
purl = f"pkg:pypi/{name}@{ver}"
64+
PATCHES[uuid] = dict(uuid=uuid, name=name, ver=ver, purl=purl, whl=whl, data=data,
65+
url=f"{BASE}/patch/pypi/{name}/{ver}/{TOKEN}/{uuid}/{whl}",
66+
files={target: {"beforeHash": gsha(before), "afterHash": gsha(after)}}, blobs={gsha(after): after, gsha(before): before})
67+
add("aaaaaaaa-0000-4000-8000-000000000001", "six", "1.16.0", "six-1.16.0-py2.py3-none-any.whl", "six.py")
68+
add("aaaaaaaa-0000-4000-8000-000000000002", "markupsafe", "2.1.5", "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", "markupsafe/__init__.py")
69+
add("aaaaaaaa-0000-4000-8000-000000000003", "idna", "3.7", "idna-3.7-py3-none-any.whl", "idna/__init__.py")
70+
ENABLED = set(os.environ.get("MOCK_PATCHES", "six").split(","))
71+
def canon(n): return re.sub(r"[-_.]+", "-", n).lower()
72+
def live(): return [p for p in PATCHES.values() if p["name"] in ENABLED]
73+
def find_purl(purl):
74+
m = re.match(r"pkg:pypi/([^@]+)@([^?#]+)", purl)
75+
if not m: return None
76+
for p in live():
77+
if canon(m.group(1)) == canon(p["name"]) and m.group(2) == p["ver"]: return p
78+
def view(p):
79+
return {"uuid": p["uuid"], "purl": p["purl"], "publishedAt": "2026-09-01T00:00:00Z", "files": p["files"],
80+
"vulnerabilities": {"GHSA-xxxx-yyyy-zzzz": {"cves": ["CVE-2026-0001"], "summary": "s", "severity": "high", "description": "d"}},
81+
"description": "bughunt patch", "license": "MIT", "tier": "free"}
82+
def grant(p):
83+
sri = "sha512-" + base64.b64encode(hashlib.sha512(p["data"]).digest()).decode()
84+
return {"status": "granted", "purl": p["purl"], "url": p["url"], "artifacts": [
85+
{"kind": "tarball", "url": p["url"], "integrity": {"sha512": sri, "sha256": hashlib.sha256(p["data"]).hexdigest()}}], "registryOverride": None}
86+
CTX = ssl.create_default_context(cafile="/root/.ccr/ca-bundle.crt") if os.path.exists("/root/.ccr/ca-bundle.crt") else ssl.create_default_context()
87+
class H(http.server.BaseHTTPRequestHandler):
88+
def log_message(self, *a):
89+
with open(os.path.join(S, "mock.log"), "a") as f: f.write(f"{self.command} {self.path}\n")
90+
def send(self, code, body=b"", ctype="application/json", head=False):
91+
if isinstance(body, (dict, list)): body = json.dumps(body).encode()
92+
self.send_response(code); self.send_header("Content-Type", ctype); self.send_header("Content-Length", str(len(body))); self.end_headers()
93+
if not head: self.wfile.write(body)
94+
def body(self):
95+
n = int(self.headers.get("Content-Length") or 0); raw = self.rfile.read(n) if n else b""
96+
try: return json.loads(raw or b"{}")
97+
except Exception: return {}
98+
def do_HEAD(self): self.do_GET(head=True)
99+
def do_GET(self, head=False):
100+
path = self.path.split("?")[0]
101+
for p in PATCHES.values():
102+
if path.endswith("/" + p["uuid"] + "/" + p["whl"]) or path.endswith("/artifacts/" + p["uuid"] + "/" + p["whl"]):
103+
return self.send(200, p["data"], "application/octet-stream", head)
104+
m = re.search(r"/view/([0-9a-f-]+)$", path)
105+
if m and m.group(1) in PATCHES and PATCHES[m.group(1)]["name"] in ENABLED: return self.send(200, view(PATCHES[m.group(1)]), head=head)
106+
m = re.search(r"/by-package/(.+)$", path)
107+
if m:
108+
from urllib.parse import unquote
109+
p = find_purl(unquote(m.group(1)))
110+
pl = [dict(view(p), vulnerabilities={})] if p else []
111+
return self.send(200, {"patches": pl, "canAccessPaidPatches": False}, head=head)
112+
m = re.search(r"/blob/([0-9a-f]+)$", path)
113+
if m:
114+
for p in PATCHES.values():
115+
if m.group(1) in p["blobs"]: return self.send(200, p["blobs"][m.group(1)], "application/octet-stream", head)
116+
if path.startswith("/pypi/"):
117+
try:
118+
with urllib.request.urlopen("https://pypi.org" + path, context=CTX, timeout=30) as r: return self.send(200, r.read(), head=head)
119+
except urllib.error.HTTPError as e: return self.send(e.code, b"", head=head)
120+
return self.send(404, {"error": "not found"}, head=head)
121+
def do_POST(self):
122+
path = self.path.split("?")[0]; b = self.body()
123+
with open(os.path.join(S, "mock.log"), "a") as f: f.write(f" body {json.dumps(b)[:300]}\n")
124+
if path.endswith("/patches/batch"):
125+
pk = []
126+
for c in b.get("components", []):
127+
p = find_purl(c.get("purl", ""))
128+
if p: pk.append({"purl": c["purl"], "patches": [{"uuid": p["uuid"], "purl": p["purl"], "tier": "free", "cveIds": ["CVE-2026-0001"], "ghsaIds": ["GHSA-xxxx-yyyy-zzzz"], "severity": "HIGH", "title": "bughunt"}]})
129+
return self.send(200, {"packages": pk, "canAccessPaidPatches": False})
130+
if path.endswith("/package"):
131+
res = {}
132+
ids = set()
133+
def walk(o):
134+
if isinstance(o, dict): [walk(v) for v in o.values()]
135+
elif isinstance(o, list): [walk(v) for v in o]
136+
elif isinstance(o, str):
137+
if o in PATCHES: ids.add(o)
138+
p = find_purl(o)
139+
if p: ids.add(p["uuid"])
140+
walk(b)
141+
for u in ids: res[u] = grant(PATCHES[u])
142+
return self.send(200, {"results": res})
143+
return self.send(404, {"error": "not found"})
144+
http.server.ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever()
145+
MOCKEOF
146+
cat > probe.sh <<'PROBEEOF'
147+
set -u
148+
M=http://127.0.0.1:8765
149+
export SOCKET_TELEMETRY_DISABLED=1 SOCKET_NO_CONFIG=1 SOCKET_PYPI_JSON_API=$M/pypi
150+
sp() { "$SP" "$@" --api-url $M --api-token fake --org acme --patch-server-url $M --vendor-url $M; }
151+
for mode in vendored hosted; do for grp in pep735 legacy; do for cmd in "vendor --revert" "remove pkg:pypi/six@1.16.0" "rollback pkg:pypi/six@1.16.0"; do
152+
[ $mode = hosted ] && [ "$cmd" = "vendor --revert" ] && continue
153+
d=$PWD/../rp-$RANDOM; ROOT=${ROOT:-$PWD}; rm -rf $d; mkdir -p $d; cd "$d"
154+
if [ $grp = pep735 ]; then printf '[project]\nname = "app"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = []\n\n[dependency-groups]\ndev = ["six==1.16.0", "attrs>=20"]\n' > pyproject.toml
155+
else printf '[project]\nname = "app"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = []\n\n[tool.uv]\ndev-dependencies = ["six==1.16.0", "attrs>=20"]\n' > pyproject.toml; fi
156+
cp pyproject.toml p0
157+
"$UV" lock -q 2>/dev/null; "$UV" sync -q 2>/dev/null
158+
sp scan --mode $mode --yes --json >s.json 2>/dev/null; e=$?
159+
"$UV" sync -q --locked 2>/dev/null; l1=$?; pat=$(grep -c SOCKET_PATCHED $(ls .venv/lib/python*/site-packages/six.py .venv/Lib/site-packages/six.py 2>/dev/null|head -1))
160+
"$UV" add -q --dev idna==3.7 2>/dev/null; a=$?
161+
sp $cmd --yes --json >r.json 2>/dev/null; u=$?
162+
st=$(python -c "import json;print(json.load(open('r.json')).get('status'))" 2>/dev/null)
163+
lr=$(grep -c 'socket/vendor\|127.0.0.1:8765' uv.lock); pr=$(grep -c 'socket/vendor\|127.0.0.1:8765' pyproject.toml)
164+
out=$("$UV" sync --locked 2>&1); lk=$?
165+
sp vex --json >v.json 2>/dev/null; vx=$?
166+
echo "RESULT ${UVV} $mode $grp [$cmd] scan=$e locked_after_scan=$l1 patched=$pat add=$a unwind_exit=$u status=$st uvlock_refs=$lr py_refs=$pr locked_after=$lk vex=$vx | $(echo "$out"|grep -i error|head -1)"
167+
done; done; done
168+
PROBEEOF
169+
- name: probe
170+
run: |
171+
cd "$RUNNER_TEMP/h"
172+
(MOCK_PATCHES=six python mock.py 8765 > mock.out 2>&1 &)
173+
for i in $(seq 1 30); do curl -sf http://127.0.0.1:8765/v0/orgs/acme/patches/view/aaaaaaaa-0000-4000-8000-000000000001 >/dev/null && break; sleep 1; done
174+
export SP="$GITHUB_WORKSPACE/target/release/socket-patch"
175+
[ -f "$SP.exe" ] && SP="$SP.exe"
176+
export UV="$(command -v uv)"
177+
"$UV" --version
178+
export UV_CACHE_DIR="$RUNNER_TEMP/uvcache"
179+
mkdir -p wk && cd wk && UVV="${{ matrix.uv }}" bash ../probe.sh 2>&1 | tee probe.log
180+
echo '--- RESULTS'; grep '^RESULT' probe.log
181+
echo '--- mock log'; tail -30 mock.log || true; cat mock.out || true

0 commit comments

Comments
 (0)