|
| 1 | +name: bughunt yarn-classic probe |
| 2 | +on: |
| 3 | + push: |
| 4 | + branches: ['bughunt/yarn-classic/**'] |
| 5 | +permissions: |
| 6 | + contents: read |
| 7 | +jobs: |
| 8 | + probe: |
| 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 | + yarn: ['1.10.1', '1.22.22'] |
| 16 | + defaults: |
| 17 | + run: |
| 18 | + shell: bash |
| 19 | + steps: |
| 20 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
| 21 | + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 |
| 22 | + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 |
| 23 | + with: |
| 24 | + node-version: '22.x' |
| 25 | + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 |
| 26 | + with: |
| 27 | + python-version: '3.12' |
| 28 | + - name: Build socket-patch |
| 29 | + run: cargo build --release -p socket-patch-cli |
| 30 | + - name: Install yarn |
| 31 | + run: npm install --prefix "$RUNNER_TEMP/y" yarn@${{ matrix.yarn }} && node "$RUNNER_TEMP/y/node_modules/yarn/bin/yarn.js" --version |
| 32 | + - name: Write harness |
| 33 | + run: | |
| 34 | + mkdir -p "$RUNNER_TEMP/h" |
| 35 | + cat > "$RUNNER_TEMP/h/mock.py" <<'MOCKEOF' |
| 36 | + #!/usr/bin/env python3 |
| 37 | + """Minimal Socket patch API mock: left-pad@1.3.0 patch (prepends a marker to index.js).""" |
| 38 | + import base64, gzip, hashlib, io, json, os, sys, tarfile, urllib.request |
| 39 | + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer |
| 40 | + |
| 41 | + PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8787 |
| 42 | + SRC = sys.argv[2] if len(sys.argv) > 2 else None # optional local left-pad-1.3.0.tgz |
| 43 | + BASE = f"http://127.0.0.1:{PORT}" |
| 44 | + NAME, VER = "left-pad", "1.3.0" |
| 45 | + PURL = f"pkg:npm/{NAME}@{VER}" |
| 46 | + UUID = "11111111-2222-4333-8444-555555555555" |
| 47 | + MARKER = b"/* SOCKET-PATCHED-MOCK */\n" |
| 48 | + |
| 49 | + raw = open(SRC, "rb").read() if SRC else urllib.request.urlopen( |
| 50 | + f"https://registry.npmjs.org/{NAME}/-/{NAME}-{VER}.tgz").read() |
| 51 | + files = {} |
| 52 | + with tarfile.open(fileobj=io.BytesIO(raw), mode="r:gz") as t: |
| 53 | + for m in t.getmembers(): |
| 54 | + if m.isfile(): |
| 55 | + files[m.name] = t.extractfile(m).read() |
| 56 | + orig = files["package/index.js"] |
| 57 | + patched = MARKER + orig |
| 58 | + files["package/index.js"] = patched |
| 59 | + buf = io.BytesIO() |
| 60 | + with tarfile.open(fileobj=buf, mode="w", format=tarfile.USTAR_FORMAT) as t: |
| 61 | + for n in sorted(files): |
| 62 | + ti = tarfile.TarInfo(n); ti.size = len(files[n]); ti.mtime = 0; ti.mode = 0o644 |
| 63 | + t.addfile(ti, io.BytesIO(files[n])) |
| 64 | + gz = io.BytesIO() |
| 65 | + with gzip.GzipFile(fileobj=gz, mode="wb", mtime=0) as g: |
| 66 | + g.write(buf.getvalue()) |
| 67 | + TGZ = gz.getvalue() |
| 68 | + |
| 69 | + def gsha(b): return hashlib.sha256(b"blob %d\0" % len(b) + b).hexdigest() |
| 70 | + BLOBS = {gsha(orig): orig, gsha(patched): patched} |
| 71 | + SRI = "sha512-" + base64.b64encode(hashlib.sha512(TGZ).digest()).decode() |
| 72 | + SHA1 = hashlib.sha1(TGZ).hexdigest() |
| 73 | + ART = f"{BASE}/artifacts/{UUID}/{NAME}-{VER}.tgz" |
| 74 | + SUMMARY = {"uuid": UUID, "purl": PURL, "tier": "free", "cveIds": ["CVE-2026-0001"], |
| 75 | + "ghsaIds": ["GHSA-aaaa-bbbb-cccc"], "severity": "high", "title": "mock"} |
| 76 | + VIEW = {"uuid": UUID, "purl": PURL, "publishedAt": "2026-01-01T00:00:00Z", |
| 77 | + "files": {"package/index.js": {"beforeHash": gsha(orig), "afterHash": gsha(patched), |
| 78 | + "blobContent": base64.b64encode(patched).decode()}}, |
| 79 | + "vulnerabilities": {"GHSA-aaaa-bbbb-cccc": {"cves": ["CVE-2026-0001"], "summary": "s", |
| 80 | + "severity": "high", "description": "d"}}, |
| 81 | + "description": "x", "license": "MIT", "tier": "free"} |
| 82 | + GRANT = {"status": "granted", "purl": PURL, "url": ART, |
| 83 | + "artifacts": [{"kind": "tarball", "url": ART, "integrity": {"sha512": SRI, "sha1": SHA1}}], |
| 84 | + "registryOverride": None} |
| 85 | + |
| 86 | + class H(BaseHTTPRequestHandler): |
| 87 | + def log_message(self, *a): sys.stderr.write("MOCK %s\n" % (a[0] % a[1:])) |
| 88 | + def send(self, code, body, ctype="application/json"): |
| 89 | + if not isinstance(body, bytes): body = json.dumps(body).encode() |
| 90 | + self.send_response(code); self.send_header("Content-Type", ctype) |
| 91 | + self.send_header("Content-Length", str(len(body))); self.end_headers(); self.wfile.write(body) |
| 92 | + def do_GET(self): |
| 93 | + p = self.path.split("?")[0] |
| 94 | + if p.startswith(f"/artifacts/{UUID}/"): return self.send(200, TGZ, "application/octet-stream") |
| 95 | + if "/by-package/" in p: |
| 96 | + return self.send(200, {"patches": [dict(SUMMARY, publishedAt="2026-01-01T00:00:00Z", |
| 97 | + description="x", license="MIT", vulnerabilities={})], "canAccessPaidPatches": False}) |
| 98 | + if p.endswith(f"/view/{UUID}"): return self.send(200, VIEW) |
| 99 | + if "/blob/" in p: |
| 100 | + h = p.rsplit("/", 1)[1] |
| 101 | + return self.send(200, BLOBS[h], "application/octet-stream") if h in BLOBS else self.send(404, {}) |
| 102 | + self.send(404, {"error": "nf"}) |
| 103 | + def do_POST(self): |
| 104 | + p = self.path.split("?")[0] |
| 105 | + body = self.rfile.read(int(self.headers.get("Content-Length") or 0)).decode("utf-8", "replace") |
| 106 | + if p.endswith("/patches/batch") or p.endswith("/patch/batch"): |
| 107 | + pk = [{"purl": PURL, "patches": [SUMMARY]}] if (NAME + "@" + VER) in body.replace("%40", "@") else [] |
| 108 | + return self.send(200, {"packages": pk, "canAccessPaidPatches": False}) |
| 109 | + if p.endswith("/package"): |
| 110 | + return self.send(200, {"results": {UUID: GRANT} if UUID in body else {}}) |
| 111 | + self.send(404, {"error": "nf"}) |
| 112 | + |
| 113 | + if __name__ == "__main__": |
| 114 | + print(json.dumps({"sri": SRI, "sha1": SHA1, "after": gsha(patched)}), flush=True) |
| 115 | + ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever() |
| 116 | + MOCKEOF |
| 117 | + cat > "$RUNNER_TEMP/h/probe.sh" <<'PROBEEOF' |
| 118 | + #!/usr/bin/env bash |
| 119 | + set -u |
| 120 | + BIN="$1"; Y="node $2" |
| 121 | + T="$(cd "$RUNNER_TEMP" && pwd -W 2>/dev/null || pwd)"; W="$T/w"; rm -rf "$W"; mkdir -p "$W" |
| 122 | + API="--api-url http://127.0.0.1:8787 --org o --api-token x" |
| 123 | + export SOCKET_PATCH_SERVER_URL=http://127.0.0.1:8787 SOCKET_API_URL=http://127.0.0.1:8787 SOCKET_PROXY_URL=http://127.0.0.1:8787 SOCKET_TELEMETRY_DISABLED=1 |
| 124 | + NODEBIN="$(node -p 'require("path").dirname(process.execPath)')" |
| 125 | + case "$RUNNER_OS" in Windows) PFX="$NODEBIN";; *) PFX="$(dirname "$NODEBIN")";; esac |
| 126 | + UH="$(node -p 'require("os").homedir()')" |
| 127 | + echo "prefix=$PFX userhome=$UH" |
| 128 | + cleanup(){ rm -f "$UH/.yarnrc" "$UH/.npmrc" "$PFX/etc/yarnrc" "$PFX/etc/npmrc"; [ "$(id -u)" = 0 ] && rm -f /usr/local/share/.yarnrc /usr/local/share/.npmrc; unset YARN_YARN_OFFLINE_MIRROR npm_config_yarn_offline_mirror; } |
| 129 | + cell(){ local N="$1" SETUP="$2"; cleanup; local D="$W/$N"; local M="$W/$N-mirror"; mkdir -p "$D/p" "$M"; cd "$D/p" |
| 130 | + echo '{"name":"p","version":"1.0.0","private":true,"dependencies":{"left-pad":"1.3.0"}}' > package.json |
| 131 | + eval "$SETUP" |
| 132 | + $Y install --no-progress --cache-folder "$D/c0" > "$D/i0.log" 2>&1 || { echo "RESULT $N BLOCKED initial-install"; tail -5 "$D/i0.log"; cleanup; return; } |
| 133 | + local ACTIVE; ACTIVE=$(ls "$M" | wc -l | tr -d ' ') |
| 134 | + "$BIN" scan --mode hosted --json --yes $API > "$D/scan.json" 2> "$D/scan.err"; local EX=$? |
| 135 | + local PIN; PIN=$(grep -c 8787 yarn.lock) |
| 136 | + local CODES; CODES=$(python -c "import json,sys;d=json.load(open(sys.argv[1]));r=d.get('redirect') or {};print(d.get('status'),[w['code'] for w in r.get('warnings',[])])" "$D/scan.json" 2>&1 | tr -d '\r') |
| 137 | + rm -rf node_modules; $Y install --frozen-lockfile --no-progress --cache-folder "$D/c1" > "$D/i1.log" 2>&1; local IE=$? |
| 138 | + local PT=UNPATCHED; grep -q SOCKET-PATCHED-MOCK node_modules/left-pad/index.js 2>/dev/null && PT=PATCHED |
| 139 | + local V="pass" |
| 140 | + if [ "$ACTIVE" != 0 ] && [ "$PIN" != 0 ]; then V="FAIL(pinned-under-active-mirror)"; fi |
| 141 | + if [ "$ACTIVE" = 0 ] && [ "$PIN" = 0 ]; then V="FAIL(refused-without-mirror)"; fi |
| 142 | + [ $IE -ne 0 ] && V="$V+install-failed" |
| 143 | + echo "RESULT $N $V mirror_active=$ACTIVE scan_exit=$EX pinned=$PIN [$CODES] frozen=$IE $PT" |
| 144 | + [ $IE -ne 0 ] && grep -i error "$D/i1.log" | head -3 |
| 145 | + cd "$W"; cleanup; } |
| 146 | + MM='"$(cygpath -m "$M" 2>/dev/null || echo "$M")"' |
| 147 | + cell control ':' |
| 148 | + cell user-yarnrc-config-set '$Y config set yarn-offline-mirror "$(cygpath -m "$M" 2>/dev/null || echo "$M")" >/dev/null' |
| 149 | + cell user-npmrc 'echo "yarn-offline-mirror=$(cygpath -m "$M" 2>/dev/null || echo "$M")" > "$UH/.npmrc"' |
| 150 | + cell prefix-etc-yarnrc 'mkdir -p "$PFX/etc"; echo "yarn-offline-mirror \"$(cygpath -m "$M" 2>/dev/null || echo "$M")\"" > "$PFX/etc/yarnrc"' |
| 151 | + cell parent-yarnrc 'echo "yarn-offline-mirror \"$(cygpath -m "$M" 2>/dev/null || echo "$M")\"" > ../.yarnrc' |
| 152 | + cell env-yarn 'export YARN_YARN_OFFLINE_MIRROR="$(cygpath -m "$M" 2>/dev/null || echo "$M")"' |
| 153 | + cell env-npm 'export npm_config_yarn_offline_mirror="$(cygpath -m "$M" 2>/dev/null || echo "$M")"' |
| 154 | + cell bom-project-yarnrc 'printf "\xef\xbb\xbfyarn-offline-mirror \"%s\"\r\n" "$(cygpath -m "$M" 2>/dev/null || echo "$M")" > .yarnrc' |
| 155 | + if [ "$RUNNER_OS" = Windows ]; then |
| 156 | + cell ps51-bom-project-yarnrc 'powershell -NoProfile -Command "Set-Content -Path .yarnrc -Encoding utf8 -Value (\"yarn-offline-mirror \" + [char]34 + \"$(cygpath -m "$M")\" + [char]34)"; od -c .yarnrc | head -2' |
| 157 | + cell ps51-bom-user-yarnrc 'powershell -NoProfile -Command "Set-Content -Path \"$(cygpath -w "$UH")\\.yarnrc\" -Encoding utf8 -Value (\"yarn-offline-mirror \" + [char]34 + \"$(cygpath -m "$M")\" + [char]34)"' |
| 158 | + cell backslash-path-yarnrc 'echo "yarn-offline-mirror \"$(cygpath -w "$M" | sed "s/\\\\/\\\\\\\\/g")\"" > .yarnrc; cat .yarnrc' |
| 159 | + fi |
| 160 | + cell npm-false-yarn-path 'echo "yarn-offline-mirror \"$(cygpath -m "$M" 2>/dev/null || echo "$M")\"" > .yarnrc; echo "yarn-offline-mirror=false" > .npmrc' |
| 161 | + echo DONE |
| 162 | + PROBEEOF |
| 163 | + - name: Probe |
| 164 | + run: | |
| 165 | + python "$RUNNER_TEMP/h/mock.py" 8787 > "$RUNNER_TEMP/mock.log" 2>&1 & |
| 166 | + for i in $(seq 1 60); do curl -s -o /dev/null -XPOST http://127.0.0.1:8787/x/package -d '{}' && break; sleep 1; done |
| 167 | + for i in 1 2 3 4 5; do curl -s -o /dev/null -XPOST http://127.0.0.1:8787/x/package -d '{}'; done |
| 168 | + BIN="$GITHUB_WORKSPACE/target/release/socket-patch"; [ -f "$BIN.exe" ] && BIN="$BIN.exe" |
| 169 | + cd "$RUNNER_TEMP" && bash "$RUNNER_TEMP/h/probe.sh" "$BIN" "$RUNNER_TEMP/y/node_modules/yarn/bin/yarn.js" 2>&1 | grep -v '^Warning: --api-token\|Got: x' | tee "$RUNNER_TEMP/probe.log" |
| 170 | + echo "===== SUMMARY ${{ matrix.os }} yarn ${{ matrix.yarn }}"; grep '^RESULT' "$RUNNER_TEMP/probe.log" |
0 commit comments