Skip to content

Commit 7cf105a

Browse files
committed
bughunt: yarn-classic cross-OS vendored probe
1 parent 045d7ec commit 7cf105a

1 file changed

Lines changed: 200 additions & 0 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
name: bughunt yarn-classic cross-OS vendored probe
2+
on:
3+
push:
4+
branches: ['bughunt/yarn-classic/**']
5+
permissions:
6+
contents: read
7+
jobs:
8+
produce:
9+
runs-on: ${{ matrix.os }}
10+
timeout-minutes: 45
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
os: [ubuntu-latest, macos-latest, windows-latest]
15+
defaults:
16+
run:
17+
shell: bash
18+
steps:
19+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
20+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
21+
with:
22+
node-version: '20'
23+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
24+
with:
25+
python-version: '3.12'
26+
- run: rustup show
27+
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
28+
- name: build
29+
run: cargo build --release -p socket-patch-cli
30+
- name: kit
31+
run: |
32+
mkdir -p kit
33+
cat > kit/mock.py <<'PYEOF'
34+
import json, sys, os, io, tarfile, hashlib, base64, uuid, gzip, urllib.request, threading, re
35+
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
36+
# usage: mock.py PORT spec.json ; spec: [{"name":..,"version":..,"file":"index.js"}]
37+
PORT=int(sys.argv[1]); spec=json.load(open(sys.argv[2]))
38+
ORG="test-org"; TOKEN="33333333-3333-4333-8333-333333333333"
39+
MARK=os.environ.get("MARK","/* SOCKET-PATCHED-%s */\n")
40+
def gsha(b): return hashlib.sha256(b"blob %d\0"%len(b)+b).hexdigest()
41+
P={}
42+
for s in spec:
43+
name,ver,f=s["name"],s["version"],s["file"]
44+
base=name.split("/")[-1]
45+
url="https://registry.npmjs.org/%s/-/%s-%s.tgz"%(name,base,ver)
46+
raw=urllib.request.urlopen(url).read()
47+
tin=tarfile.open(fileobj=io.BytesIO(raw),mode="r:gz")
48+
out=io.BytesIO(); tout=tarfile.open(fileobj=out,mode="w",format=tarfile.GNU_FORMAT)
49+
orig=patched=None
50+
for m in tin.getmembers():
51+
data=tin.extractfile(m).read() if m.isfile() else None
52+
rel=m.name.split("/",1)[1] if "/" in m.name else m.name
53+
m2=tarfile.TarInfo("package/"+rel); m2.mode=m.mode; m2.mtime=1700000000
54+
if m.isfile():
55+
if rel==f:
56+
orig=data; data=(MARK%ver).encode()+data; patched=data
57+
m2.size=len(data); tout.addfile(m2,io.BytesIO(data))
58+
tout.close()
59+
tgz=gzip.compress(out.getvalue(),mtime=0)
60+
u=str(uuid.uuid5(uuid.NAMESPACE_URL,name+"@"+ver))
61+
purl="pkg:npm/%s@%s"%(name,ver)
62+
path="/patch/npm/%s/%s/%s/%s/%s-%s.tgz"%(name,ver,TOKEN,u,base,ver)
63+
P[u]=dict(name=name,ver=ver,purl=purl,uuid=u,tgz=tgz,path=path,file=f,orig=orig,patched=patched,
64+
sha1=hashlib.sha1(tgz).hexdigest(),sri="sha512-"+base64.b64encode(hashlib.sha512(tgz).digest()).decode())
65+
print("ready",purl,u,flush=True)
66+
BYP={p["purl"]:p for p in P.values()}
67+
BLOBS={}
68+
for p in P.values(): BLOBS[gsha(p["orig"])]=p["orig"]; BLOBS[gsha(p["patched"])]=p["patched"]
69+
def norm(purl): return purl.replace("%40","@")
70+
def summ(p): return {"uuid":p["uuid"],"purl":p["purl"],"tier":"free","cveIds":["CVE-2026-1111"],"ghsaIds":["GHSA-aaaa-bbbb-cccc"],"severity":"high","title":"fixture "+p["purl"]}
71+
def view(p):
72+
return {"uuid":p["uuid"],"purl":p["purl"],"publishedAt":"2026-01-01T00:00:00Z","description":"x","license":"MIT","tier":"free",
73+
"files":{"package/"+p["file"]:{"beforeHash":gsha(p["orig"]),"afterHash":gsha(p["patched"]),"blobContent":base64.b64encode(p["patched"]).decode()}},
74+
"vulnerabilities":{"GHSA-aaaa-bbbb-cccc":{"cves":["CVE-2026-1111"],"summary":"s","severity":"high","description":"d"}}}
75+
LOG=open(os.environ.get("MOCKLOG","mock.log"),"a")
76+
class H(BaseHTTPRequestHandler):
77+
def log_message(self,*a): LOG.write(self.command+" "+self.path+"\n"); LOG.flush()
78+
def js(self,o,code=200):
79+
b=json.dumps(o).encode(); self.send_response(code); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(b))); self.end_headers(); self.wfile.write(b)
80+
def do_POST(self):
81+
body=json.loads(self.rfile.read(int(self.headers.get("Content-Length",0))) or b"{}")
82+
if self.path.endswith("/patches/batch") or self.path.endswith("/patch/batch"):
83+
pk=[]
84+
for c in body.get("components",[]):
85+
p=BYP.get(norm(c["purl"]))
86+
if p: pk.append({"purl":p["purl"],"patches":[summ(p)]})
87+
return self.js({"packages":pk,"canAccessPaidPatches":False})
88+
if self.path.endswith("/patches/package") or self.path.endswith("/patch/package"):
89+
res={}
90+
for u in body.get("uuids",[]):
91+
p=P.get(u)
92+
if not p: continue
93+
url="http://127.0.0.1:%d%s"%(PORT,p["path"])
94+
integ={"sha512":p["sri"]}
95+
if not os.environ.get("NOSHA1"): integ["sha1"]=p["sha1"]
96+
res[u]={"status":"granted","url":url,"purl":p["purl"],"artifacts":[{"kind":"tarball","url":url,"integrity":integ}],"registryOverride":None}
97+
return self.js({"results":res})
98+
self.js({"error":"nf"},404)
99+
def do_GET(self):
100+
path=self.path.split("?")[0]
101+
m=re.search(r"/by-package/(.+)$",path)
102+
if m:
103+
from urllib.parse import unquote
104+
p=BYP.get(norm(unquote(m.group(1))))
105+
return self.js({"patches":[dict(summ(p),publishedAt="2026-01-01T00:00:00Z",description="x",license="MIT",vulnerabilities={})] if p else [],"canAccessPaidPatches":False})
106+
m=re.search(r"/patches/view/([0-9a-f-]+)$",path) or re.search(r"/patch/view/([0-9a-f-]+)$",path)
107+
if m:
108+
p=P.get(m.group(1))
109+
return self.js(view(p)) if p else self.js({},404)
110+
m=re.search(r"/blob/([0-9a-f]{64})$",path)
111+
if m and m.group(1) in BLOBS:
112+
b=BLOBS[m.group(1)]; self.send_response(200); self.send_header("Content-Length",str(len(b))); self.end_headers(); return self.wfile.write(b)
113+
for p in P.values():
114+
if path==p["path"]:
115+
b=p["tgz"]; self.send_response(200); self.send_header("Content-Type","application/octet-stream"); self.send_header("Content-Length",str(len(b))); self.end_headers(); return self.wfile.write(b)
116+
self.js({"error":"nf"},404)
117+
ThreadingHTTPServer(("127.0.0.1",PORT),H).serve_forever()
118+
PYEOF
119+
cat > kit/spec.json <<'JSEOF'
120+
[{"name":"left-pad","version":"1.3.0","file":"index.js"},
121+
{"name":"ms","version":"3.0.0-canary.1","file":"dist/index.cjs"},
122+
{"name":"lodash.isequal","version":"4.5.0","file":"index.js"},
123+
{"name":"@types/left-pad","version":"1.2.0","file":"README.md"}]
124+
JSEOF
125+
npm i -g yarn@1.22.22 --force >/dev/null 2>&1; yarn --version
126+
- name: produce
127+
run: |
128+
set +e -u
129+
OS=${{ matrix.os }}
130+
SP="$PWD/target/release/socket-patch"; [ -f "$SP.exe" ] && SP="$SP.exe"
131+
(cd kit && python mock.py 8811 spec.json > mock.out 2>&1 &)
132+
for i in $(seq 1 60); do curl -sf -o /dev/null -X POST -d '{}' http://127.0.0.1:8811/v0/orgs/test-org/patches/batch && break; sleep 2; done
133+
cat kit/mock.out
134+
A="--api-url http://127.0.0.1:8811 --org test-org --api-token fake"
135+
export SOCKET_PATCH_SERVER_URL=http://127.0.0.1:8811
136+
O="$PWD/out/$OS"; mkdir -p "$O/proj"; cd "$O/proj"
137+
echo '{"name":"app","version":"1.0.0","private":true,"dependencies":{"left-pad":"1.3.0","ms":"3.0.0-canary.1","lodash.isequal":"^4.5.0","@types/left-pad":"^1.2.0"}}' > package.json
138+
YARN_CACHE_FOLDER="$RUNNER_TEMP/yc0" yarn install --no-progress
139+
cp yarn.lock ../orig.lock; rm -rf node_modules
140+
"$SP" scan --mode vendored --vendor-source service --json --yes $A > ../scan.json 2>../scan.err; echo "RESULT $OS scan exit=$?"
141+
tail -5 ../scan.err
142+
echo "--- lock"; cat -A yarn.lock | grep -E "resolved|integrity"
143+
echo "--- state"; cat .socket/vendor/state.json
144+
echo "--- tree"; find .socket -type f
145+
echo "--- vendor.json"; find .socket -name socket-patch.vendor.json -exec cat {} \; | head -80
146+
mkdir ../ctl && cp -r package.json yarn.lock .socket ../ctl/ && cd ../ctl
147+
YARN_CACHE_FOLDER="$RUNNER_TEMP/yc1" yarn install --frozen-lockfile --offline --no-progress; echo "RESULT $OS local-frozen exit=$?"
148+
head -c 30 node_modules/left-pad/index.js; echo; head -c 30 node_modules/@types/left-pad/README.md; echo
149+
cd .. && rm -rf ctl
150+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
151+
with:
152+
name: proj-${{ matrix.os }}
153+
path: out/
154+
consume:
155+
needs: produce
156+
runs-on: ${{ matrix.os }}
157+
timeout-minutes: 45
158+
strategy:
159+
fail-fast: false
160+
matrix:
161+
os: [ubuntu-latest, macos-latest, windows-latest]
162+
defaults:
163+
run:
164+
shell: bash
165+
steps:
166+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
167+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
168+
with:
169+
node-version: '20'
170+
- run: rustup show
171+
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
172+
- name: build
173+
run: cargo build --release -p socket-patch-cli
174+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
175+
with:
176+
path: in
177+
- name: consume
178+
run: |
179+
set +e -u
180+
npm i -g yarn@1.22.22 --force >/dev/null 2>&1
181+
ME=${{ matrix.os }}
182+
SP="$PWD/target/release/socket-patch"; [ -f "$SP.exe" ] && SP="$SP.exe"
183+
find in -maxdepth 3
184+
for SRC in ubuntu-latest macos-latest windows-latest; do
185+
D="$PWD/in/proj-$SRC/$SRC"; [ -d "$D" ] || { echo "RESULT $SRC->$ME missing"; continue; }
186+
W="$RUNNER_TEMP/w-$SRC"; rm -rf "$W"; cp -r "$D" "$W"; cd "$W/proj"
187+
YARN_CACHE_FOLDER="$RUNNER_TEMP/yc-$SRC" yarn install --frozen-lockfile --offline --no-progress > ../inst.log 2>&1; rc=$?
188+
tail -3 ../inst.log
189+
pat=""; for f in left-pad/index.js ms/dist/index.cjs lodash.isequal/index.js @types/left-pad/README.md; do head -c 12 node_modules/$f 2>/dev/null | grep -q SOCKET && pat="${pat}P" || pat="${pat}U"; done
190+
echo "RESULT $SRC->$ME frozen=$rc patched=$pat"
191+
"$SP" vendor --check --json > ../check.json 2>&1; echo "RESULT $SRC->$ME vendor-check=$?"; head -c 1500 ../check.json; echo
192+
"$SP" vex --offline --product pkg:npm/app@1.0.0 --output ../v.json --json > ../vex.out 2>&1; echo "RESULT $SRC->$ME vex=$? purls=$(grep -o 'pkg:npm/[^"]*' ../v.json | sort -u | tr '\n' ' ')"
193+
tail -c 800 ../vex.out; echo
194+
"$SP" list --json > ../list.json 2>&1; echo "RESULT $SRC->$ME list=$?"; head -c 800 ../list.json; echo
195+
"$SP" rollback --json --yes > ../rb.json 2>&1; echo "RESULT $SRC->$ME rollback=$?"; head -c 1500 ../rb.json; echo
196+
cmp ../orig.lock yarn.lock && echo "RESULT $SRC->$ME rollback-byte-exact" || { echo "RESULT $SRC->$ME rollback-NOT-exact"; diff ../orig.lock yarn.lock | head -20; }
197+
echo "RESULT $SRC->$ME leftover: $(find .socket -type f 2>/dev/null | tr '\n' ' ')"
198+
cd "$GITHUB_WORKSPACE"
199+
done
200+
echo "=== SUMMARY"

0 commit comments

Comments
 (0)