|
| 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