Skip to content

Commit 9e6b4bb

Browse files
committed
bughunt uv probe: agent mode under UV_LINK_MODE symlink/hardlink/copy on 3 OS
1 parent 85105c9 commit 9e6b4bb

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.24']
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+
cat > mock.py <<'MOCKEOF'
33+
import base64, hashlib, http.server, io, json, os, re, sys, urllib.request, zipfile, ssl
34+
S = os.path.dirname(os.path.abspath(__file__))
35+
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8765
36+
BASE = f"http://127.0.0.1:{PORT}"
37+
TOKEN = "11111111-2222-4333-8444-555555555555"
38+
SUFFIX = b"\n# SOCKET-PATCHED\nSOCKET_PATCHED = 1\n"
39+
def gsha(b): return hashlib.sha256(b"blob %d\0" % len(b) + b).hexdigest()
40+
def patch_wheel(src, target):
41+
zin = zipfile.ZipFile(src); out = io.BytesIO(); members = []
42+
rec_name = [n for n in zin.namelist() if n.endswith(".dist-info/RECORD")][0]
43+
before = after = None
44+
for n in sorted(zin.namelist()):
45+
if n == rec_name: continue
46+
b = zin.read(n)
47+
if n == target: before = b; b = b + SUFFIX; after = b
48+
members.append((n, b))
49+
rec = ""
50+
for n, b in members:
51+
d = base64.urlsafe_b64encode(hashlib.sha256(b).digest()).rstrip(b"=").decode()
52+
rec += f"{n},sha256={d},{len(b)}\n"
53+
rec += f"{rec_name},,\n"; members.append((rec_name, rec.encode()))
54+
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
55+
for n, b in members:
56+
zi = zipfile.ZipInfo(n, (2020,1,1,0,0,0)); zi.compress_type = zipfile.ZIP_DEFLATED; zi.external_attr = 0o644 << 16
57+
z.writestr(zi, b)
58+
return out.getvalue(), before, after
59+
PATCHES = {}
60+
def add(uuid, name, ver, whl, target):
61+
data, before, after = patch_wheel(os.path.join(S, "wh", whl), target)
62+
purl = f"pkg:pypi/{name}@{ver}"
63+
PATCHES[uuid] = dict(uuid=uuid, name=name, ver=ver, purl=purl, whl=whl, data=data,
64+
url=f"{BASE}/patch/pypi/{name}/{ver}/{TOKEN}/{uuid}/{whl}",
65+
files={target: {"beforeHash": gsha(before), "afterHash": gsha(after)}}, blobs={gsha(after): after, gsha(before): before})
66+
add("aaaaaaaa-0000-4000-8000-000000000001", "six", "1.16.0", "six-1.16.0-py2.py3-none-any.whl", "six.py")
67+
ENABLED = set(os.environ.get("MOCK_PATCHES", "six").split(","))
68+
def canon(n): return re.sub(r"[-_.]+", "-", n).lower()
69+
def live(): return [p for p in PATCHES.values() if p["name"] in ENABLED]
70+
def find_purl(purl):
71+
m = re.match(r"pkg:pypi/([^@]+)@([^?#]+)", purl)
72+
if not m: return None
73+
for p in live():
74+
if canon(m.group(1)) == canon(p["name"]) and m.group(2) == p["ver"]: return p
75+
def view(p):
76+
return {"uuid": p["uuid"], "purl": p["purl"], "publishedAt": "2026-09-01T00:00:00Z", "files": p["files"],
77+
"vulnerabilities": {"GHSA-xxxx-yyyy-zzzz": {"cves": ["CVE-2026-0001"], "summary": "s", "severity": "high", "description": "d"}},
78+
"description": "bughunt patch", "license": "MIT", "tier": "free"}
79+
def grant(p):
80+
sri = "sha512-" + base64.b64encode(hashlib.sha512(p["data"]).digest()).decode()
81+
return {"status": "granted", "purl": p["purl"], "url": p["url"], "artifacts": [
82+
{"kind": "tarball", "url": p["url"], "integrity": {"sha512": sri, "sha256": hashlib.sha256(p["data"]).hexdigest()}}], "registryOverride": None}
83+
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()
84+
class H(http.server.BaseHTTPRequestHandler):
85+
def log_message(self, *a):
86+
with open(os.path.join(S, "mock.log"), "a") as f: f.write(f"{self.command} {self.path}\n")
87+
def send(self, code, body=b"", ctype="application/json", head=False):
88+
if isinstance(body, (dict, list)): body = json.dumps(body).encode()
89+
self.send_response(code); self.send_header("Content-Type", ctype); self.send_header("Content-Length", str(len(body))); self.end_headers()
90+
if not head: self.wfile.write(body)
91+
def body(self):
92+
n = int(self.headers.get("Content-Length") or 0); raw = self.rfile.read(n) if n else b""
93+
try: return json.loads(raw or b"{}")
94+
except Exception: return {}
95+
def do_HEAD(self): self.do_GET(head=True)
96+
def do_GET(self, head=False):
97+
path = self.path.split("?")[0]
98+
for p in PATCHES.values():
99+
if path.endswith("/" + p["uuid"] + "/" + p["whl"]) or path.endswith("/artifacts/" + p["uuid"] + "/" + p["whl"]):
100+
return self.send(200, p["data"], "application/octet-stream", head)
101+
m = re.search(r"/view/([0-9a-f-]+)$", path)
102+
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)
103+
m = re.search(r"/by-package/(.+)$", path)
104+
if m:
105+
from urllib.parse import unquote
106+
p = find_purl(unquote(m.group(1)))
107+
pl = [dict(view(p), vulnerabilities={})] if p else []
108+
return self.send(200, {"patches": pl, "canAccessPaidPatches": False}, head=head)
109+
m = re.search(r"/blob/([0-9a-f]+)$", path)
110+
if m:
111+
for p in PATCHES.values():
112+
if m.group(1) in p["blobs"]: return self.send(200, p["blobs"][m.group(1)], "application/octet-stream", head)
113+
if path.startswith("/pypi/"):
114+
try:
115+
with urllib.request.urlopen("https://pypi.org" + path, context=CTX, timeout=30) as r: return self.send(200, r.read(), head=head)
116+
except urllib.error.HTTPError as e: return self.send(e.code, b"", head=head)
117+
return self.send(404, {"error": "not found"}, head=head)
118+
def do_POST(self):
119+
path = self.path.split("?")[0]; b = self.body()
120+
with open(os.path.join(S, "mock.log"), "a") as f: f.write(f" body {json.dumps(b)[:300]}\n")
121+
if path.endswith("/patches/batch"):
122+
pk = []
123+
for c in b.get("components", []):
124+
p = find_purl(c.get("purl", ""))
125+
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"}]})
126+
return self.send(200, {"packages": pk, "canAccessPaidPatches": False})
127+
if path.endswith("/package"):
128+
res = {}
129+
ids = set()
130+
def walk(o):
131+
if isinstance(o, dict): [walk(v) for v in o.values()]
132+
elif isinstance(o, list): [walk(v) for v in o]
133+
elif isinstance(o, str):
134+
if o in PATCHES: ids.add(o)
135+
p = find_purl(o)
136+
if p: ids.add(p["uuid"])
137+
walk(b)
138+
for u in ids: res[u] = grant(PATCHES[u])
139+
return self.send(200, {"results": res})
140+
return self.send(404, {"error": "not found"})
141+
http.server.ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever()
142+
MOCKEOF
143+
cat > probe.sh <<'PROBEEOF'
144+
set -u
145+
M=http://127.0.0.1:8765
146+
export SOCKET_TELEMETRY_DISABLED=1 SOCKET_NO_CONFIG=1 SOCKET_PYPI_JSON_API=$M/pypi
147+
sp() { "$SP" "$@" --api-url $M --api-token fake --org acme --patch-server-url $M --vendor-url $M; }
148+
py() { if [ -x .venv/Scripts/python.exe ]; then .venv/Scripts/python.exe "$@"; else .venv/bin/python "$@"; fi; }
149+
flag() { py -c 'import six;print(getattr(six,"SOCKET_PATCHED",0))' 2>&1 | tr -d '\r' | tail -1; }
150+
for lm in symlink hardlink copy; do
151+
d=$PWD/../lm-$lm; rm -rf $d; mkdir -p $d; cd $d
152+
printf '[project]\nname = "demo"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0", "attrs>=20"]\n' > pyproject.toml
153+
"$UV" lock -q; UV_LINK_MODE=$lm "$UV" sync -q 2>sync.err; se=$?
154+
f=$(py -c 'import six;print(six.__file__)' | tr -d '\r'); if [ -L "$f" ]; then kind=symlink; else kind=regular; fi
155+
tgt=$(py -c "import os,six;print(os.path.realpath(six.__file__))" | tr -d '\r')
156+
h0=$(sha256sum "$tgt" | cut -c1-12)
157+
sp scan --mode agent --yes --json > scan.json 2>scan.err; ae=$?
158+
p1=$(flag); h1=$(sha256sum "$tgt" | cut -c1-12); if [ -L "$f" ]; then k1=symlink; else k1=regular; fi
159+
sp vex --output vex.out.json --json > vex.json 2>/dev/null; ve=$?; vs=$(grep -o "\"status\": *\"[a-z_]*\"" vex.out.json 2>/dev/null | sort | uniq -c | tr -s " " | tr "\n" " ")
160+
d2=$d-other; rm -rf $d2; mkdir -p $d2; cp pyproject.toml uv.lock $d2/
161+
other=$(cd $d2 && UV_LINK_MODE=$lm "$UV" sync -q && if [ -x .venv/Scripts/python.exe ]; then .venv/Scripts/python.exe -c 'import six;print(getattr(six,"SOCKET_PATCHED",0))'; else .venv/bin/python -c 'import six;print(getattr(six,"SOCKET_PATCHED",0))'; fi 2>&1 | tr -d '\r' | tail -1)
162+
sp rollback --yes --json > rb.json 2>rb.err; re=$?; p2=$(flag); h2=$(sha256sum "$tgt" | cut -c1-12)
163+
UV_LINK_MODE=$lm "$UV" sync -q --reinstall-package six; p3=$(flag)
164+
echo "RESULT ${UVV} ${RUNNER_OS} lm=$lm sync=$se six=$kind | agent-scan exit=$ae patched=$p1 after=$k1 target-hash $h0->$h1 | vex exit=$ve $vs | other-project patched=$other | rollback exit=$re patched=$p2 hash->$h2 | reinstall patched=$p3"
165+
cat sync.err scan.err rb.err 2>/dev/null | grep -iv 'api-token\|Got: fake' | head -5 | sed 's/^/ stderr: /'
166+
cd - >/dev/null
167+
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)