|
| 1 | +name: bughunt yarn-berry probe |
| 2 | +on: |
| 3 | + push: |
| 4 | + branches: ['bughunt/yarn-berry/**'] |
| 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 | + steps: |
| 16 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
| 17 | + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 |
| 18 | + with: |
| 19 | + node-version: '22' |
| 20 | + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 |
| 21 | + with: |
| 22 | + python-version: '3.12' |
| 23 | + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 |
| 24 | + with: |
| 25 | + save-if: false |
| 26 | + - name: Build CLI |
| 27 | + shell: bash |
| 28 | + run: cargo build --release -p socket-patch-cli |
| 29 | + - name: Write probe |
| 30 | + shell: bash |
| 31 | + run: | |
| 32 | + cat > "$RUNNER_TEMP/probe.sh" <<'EOS' |
| 33 | + #!/bin/bash |
| 34 | + # self-contained probe: SP=<socket-patch bin> YV=<yarn version> WORK=<dir> |
| 35 | + set -u |
| 36 | + PY=$(command -v python3 || command -v python) |
| 37 | + W=${WORK:-$PWD/work}; mkdir -p $W/yarn; cd $W/yarn |
| 38 | + [ -f package/bin/yarn.js ] || { npm pack @yarnpkg/cli-dist@$YV --silent >/dev/null 2>&1 && tar xzf yarnpkg-cli-dist-$YV.tgz; } |
| 39 | + YJ=$W/yarn/package/bin/yarn.js |
| 40 | + y() { node "$YJ" "$@"; } |
| 41 | + export NO_PROXY="127.0.0.1,localhost" no_proxy="127.0.0.1,localhost" YARN_ENABLE_TELEMETRY=0 |
| 42 | + cat > $W/mock.py <<'EOF' |
| 43 | + import os, json |
| 44 | + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer |
| 45 | + E=os.environ; PURL=E['PURL']; UUID=E['UUID']; PORT=int(E['PORT']); ORG='test-org'; TOKEN='22222222-2222-4222-8222-222222222222' |
| 46 | + name,ver=PURL[len('pkg:npm/'):].rsplit('@',1); leaf=name.split('/')[-1] |
| 47 | + HOSTED=f"http://127.0.0.1:{PORT}/patch/npm/{name}/{ver}/{TOKEN}/{UUID}/{leaf}-{ver}.tgz" |
| 48 | + TGZ=open('patched.tgz','rb').read() |
| 49 | + class H(BaseHTTPRequestHandler): |
| 50 | + def j(self,o): |
| 51 | + b=json.dumps(o).encode(); self.send_response(200); self.send_header('content-type','application/json'); self.send_header('content-length',str(len(b))); self.end_headers(); self.wfile.write(b) |
| 52 | + def do_POST(self): |
| 53 | + self.rfile.read(int(self.headers.get('content-length') or 0)); p=self.path.split('?')[0] |
| 54 | + if p.endswith('/patches/batch'): return self.j({"packages":[{"purl":PURL,"patches":[{"uuid":UUID,"purl":PURL,"tier":"free","cveIds":[],"ghsaIds":[],"severity":"high","title":"t"}]}],"canAccessPaidPatches":False}) |
| 55 | + if p.endswith('/patches/package'): return self.j({"results":{UUID:{"status":"granted","url":HOSTED,"purl":PURL,"artifacts":[{"kind":"tarball","url":HOSTED,"integrity":{"sha512":"sha512-unused=="}},{"kind":"yarn-berry-zip","url":HOSTED,"integrity":{"yarnBerry10c0":E['CHECKSUM']}}],"registryOverride":None}}}) |
| 56 | + self.send_response(404); self.end_headers() |
| 57 | + def do_GET(self): |
| 58 | + p=self.path.split('?')[0] |
| 59 | + if '/patches/by-package/' in p: return self.j({"patches":[{"uuid":UUID,"purl":PURL,"publishedAt":"2026-01-01T00:00:00Z","description":"x","license":"MIT","tier":"free","vulnerabilities":{}}],"canAccessPaidPatches":False}) |
| 60 | + if p.endswith('/patches/view/'+UUID): return self.j({"uuid":UUID,"purl":PURL,"publishedAt":"2026-01-01T00:00:00Z","files":{E['FILEKEY']:{"beforeHash":E['BEFORE'],"afterHash":E['AFTER']}},"vulnerabilities":{"GHSA-aaaa-bbbb-cccc":{"cves":["CVE-2099-1"],"summary":"s","severity":"high","description":"d"}},"description":"x","license":"MIT","tier":"free"}) |
| 61 | + if p.startswith('/patch/npm/'): |
| 62 | + self.send_response(200); self.send_header('content-type','application/octet-stream'); self.send_header('content-length',str(len(TGZ))); self.end_headers(); self.wfile.write(TGZ); return |
| 63 | + self.send_response(404); self.end_headers() |
| 64 | + def log_message(self,*a): pass |
| 65 | + ThreadingHTTPServer(('127.0.0.1',PORT),H).serve_forever() |
| 66 | + EOF |
| 67 | + gs() { $PY -c "import hashlib,sys;c=open(sys.argv[1],'rb').read();print(hashlib.sha256(b'blob %d\0'%len(c)+c).hexdigest())" "$1"; } |
| 68 | + # hosted CASE PKG VER FILE PORT ROOTPKGJSON [MEMBERPKGJSON] |
| 69 | + hosted() { |
| 70 | + local C=$1 PKG=$2 VER=$3 FILE=$4 PORT=$5 RP=$6 MP=${7:-} |
| 71 | + local D=$W/$C; rm -rf $D; mkdir -p $D/proj $D/boot $D/tgz; cd $D/proj |
| 72 | + echo "$RP" > package.json; [ -n "$MP" ] && mkdir -p pkgs/a && echo "$MP" > pkgs/a/package.json |
| 73 | + printf 'nodeLinker: node-modules\nenableGlobalCache: false\nunsafeHttpWhitelist:\n - "127.0.0.1"\n' > .yarnrc.yml; touch yarn.lock |
| 74 | + y install > i1.log 2>&1 || { echo "[$C] install1 FAIL"; tail -5 i1.log; } |
| 75 | + echo "[$C] yarn.lock EOL: $(grep -c $'\r' yarn.lock) CR lines; cacheKey $(grep cacheKey yarn.lock)" |
| 76 | + cd $D/tgz && npm pack $PKG@$VER --silent >/dev/null 2>&1 && tar xzf *.tgz && cp package/$FILE $D/before && $PY -c " |
| 77 | + p='package/$FILE';b=open(p,'rb').read();open(p,'wb').write(b'/* SOCKET-PATCHED */\n'+b)" && cp package/$FILE $D/after && tar czf $D/patched.tgz package |
| 78 | + cd $D/boot && cp $D/patched.tgz . && echo "{\"name\":\"boot\",\"version\":\"0.0.0\",\"private\":true,\"dependencies\":{\"$PKG\":\"$VER\"},\"resolutions\":{\"$PKG\":\"file:./patched.tgz\"}}" > package.json && printf 'nodeLinker: node-modules\nenableGlobalCache: false\n' > .yarnrc.yml && touch yarn.lock && y install > b.log 2>&1 || tail -3 b.log |
| 79 | + local CK=$($PY -c " |
| 80 | + import re |
| 81 | + t=open('yarn.lock').read().replace('\r\n','\n') |
| 82 | + for blk in t.split('\n\n'): |
| 83 | + if blk.startswith('\"$PKG@file:'): |
| 84 | + c=re.search(r'checksum: (\S+)',blk).group(1); print(c if '/' in c else '10c0/'+c)") |
| 85 | + cd $D |
| 86 | + PURL="pkg:npm/$PKG@$VER" UUID=5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d CHECKSUM=$CK BEFORE=$(gs before) AFTER=$(gs after) FILEKEY=package/$FILE PORT=$PORT $PY mock.py 2>/dev/null & |
| 87 | + MOCK=$!; cp $W/mock.py $D/ 2>/dev/null; sleep 3 |
| 88 | + cd $D/proj; cp yarn.lock $D/lock.orig |
| 89 | + "$SP" scan --mode hosted --json --yes --api-url http://127.0.0.1:$PORT --org test-org --api-token fake --vex out.vex.json --vex-product pkg:npm/app@1.0.0 > scan.json 2> scan.err |
| 90 | + echo "[$C] scan exit=$? $($PY -c "import json;d=json.load(open('scan.json'));r=d.get('redirect') or {};print('status',d.get('status'),'redirected',r.get('redirected'),[w['code'] for w in r.get('warnings',[])])")" |
| 91 | + $PY -c "import json;d=json.load(open('out.vex.json'));print('[$C] in-run VEX:',[s.get('status') for s in d.get('statements',[])])" 2>/dev/null |
| 92 | + echo "[$C] archiveUrl entries in lock: $(grep -c __archiveUrl yarn.lock)" |
| 93 | + } |
| 94 | + fresh() { # CASE -> fresh immutable install |
| 95 | + local C=$1 PKG=$2 FILE=$3; local D=$W/$C; rm -rf $D/fresh; mkdir -p $D/fresh; cd $D/proj; cp -r package.json yarn.lock .yarnrc.yml .socket $D/fresh/ 2>/dev/null; [ -d pkgs ] && cp -r pkgs $D/fresh/ |
| 96 | + cd $D/fresh; y install --immutable > i2.log 2>&1; echo "[$C] fresh 'yarn install --immutable' exit=$?"; grep 'YN0028' i2.log | head -4 |
| 97 | + [ -f node_modules/$PKG/$FILE ] && echo "[$C] node_modules/$PKG/$FILE starts: $(head -c 20 node_modules/$PKG/$FILE | tr -d '\r\n')" |
| 98 | + } |
| 99 | + echo "===== yarn $(y --version) on $(uname -s) =====" |
| 100 | + # Case A: hosted redirect of a yarn builtin-compat-patched package (resolve) |
| 101 | + hosted A-resolve resolve 1.22.8 index.js 18801 '{"name":"app","version":"1.0.0","dependencies":{"resolve":"1.22.8"}}' |
| 102 | + [ -n "${MOCK:-}" ] ; fresh A-resolve resolve index.js; kill %1 2>/dev/null; kill $MOCK 2>/dev/null; wait 2>/dev/null |
| 103 | + # Case A control: left-pad |
| 104 | + hosted A-leftpad left-pad 1.3.0 index.js 18802 '{"name":"app","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' |
| 105 | + fresh A-leftpad left-pad index.js; kill %1 2>/dev/null; wait 2>/dev/null |
| 106 | + # Case B: hosted -> vendored takeover where vendoring refuses per-target (second left-pad version) |
| 107 | + hosted B-takeover left-pad 1.3.0 index.js 18803 '{"name":"app","version":"1.0.0","private":true,"workspaces":["pkgs/*"],"dependencies":{"left-pad":"1.3.0"}}' '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.1.3"}}' |
| 108 | + fresh B-takeover left-pad index.js |
| 109 | + cd $W/B-takeover/proj |
| 110 | + $PY - "$(gs $W/B-takeover/before)" "$(gs $W/B-takeover/after)" <<'EOF' |
| 111 | + import json,os,sys |
| 112 | + b,a=sys.argv[1:3] |
| 113 | + os.makedirs('.socket/blobs',exist_ok=True) |
| 114 | + m={"patches":{"pkg:npm/left-pad@1.3.0":{"uuid":"5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d","exportedAt":"2026-01-01T00:00:00Z","files":{"package/index.js":{"beforeHash":b,"afterHash":a}},"vulnerabilities":{"GHSA-aaaa-bbbb-cccc":{"cves":["CVE-2099-1"],"summary":"s","severity":"high","description":"d"}},"description":"x","license":"MIT","tier":"free"}}} |
| 115 | + json.dump(m,open('.socket/manifest.json','w'),indent=2) |
| 116 | + EOF |
| 117 | + cp $W/B-takeover/after .socket/blobs/$(gs $W/B-takeover/after) |
| 118 | + "$SP" vendor --json --offline > vt.json 2>&1 |
| 119 | + echo "[B-takeover] vendor exit=$? $($PY -c "import json;d=json.load(open('vt.json'));print(d['status'],[(e.get('action'),e.get('errorCode')) for e in d.get('events',[])])")" |
| 120 | + echo "[B-takeover] archiveUrl entries in lock after vendor: $(grep -c __archiveUrl yarn.lock); lock == pre-hosted registry lock: $(cmp -s yarn.lock $W/B-takeover/lock.orig && echo yes || echo no)" |
| 121 | + kill %1 2>/dev/null; wait 2>/dev/null |
| 122 | + exit 0 |
| 123 | + EOS |
| 124 | + - name: Probe |
| 125 | + shell: bash |
| 126 | + run: | |
| 127 | + SP="$GITHUB_WORKSPACE/target/release/socket-patch" |
| 128 | + [ -f "$SP.exe" ] && SP="$SP.exe" |
| 129 | + for v in 4.12.0 4.18.1; do |
| 130 | + SP="$SP" YV=$v WORK="$RUNNER_TEMP/w-$v" bash "$RUNNER_TEMP/probe.sh" 2>&1 | tee -a "$RUNNER_TEMP/probe.log" |
| 131 | + done |
0 commit comments