|
| 1 | +name: bughunt-bundler |
| 2 | +on: |
| 3 | + push: |
| 4 | + branches: ['bughunt/bundler/**'] |
| 5 | +permissions: |
| 6 | + contents: read |
| 7 | +jobs: |
| 8 | + hosted: |
| 9 | + runs-on: ${{ matrix.os }} |
| 10 | + timeout-minutes: 45 |
| 11 | + strategy: |
| 12 | + fail-fast: false |
| 13 | + matrix: |
| 14 | + os: [ubuntu-latest, windows-latest] |
| 15 | + ruby: ['2.7', '3.1'] |
| 16 | + defaults: |
| 17 | + run: |
| 18 | + shell: bash |
| 19 | + steps: |
| 20 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
| 21 | + with: |
| 22 | + persist-credentials: false |
| 23 | + - run: rustup show |
| 24 | + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 |
| 25 | + with: |
| 26 | + save-if: false |
| 27 | + key: bughunt-${{ matrix.os }} |
| 28 | + - uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0 |
| 29 | + with: |
| 30 | + ruby-version: ${{ matrix.ruby }} |
| 31 | + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 |
| 32 | + with: |
| 33 | + python-version: '3.12' |
| 34 | + - name: Build CLI |
| 35 | + run: cargo build --release -p socket-patch-cli |
| 36 | + - name: Probe Bundler 1.x / 2.2 hosted cycle |
| 37 | + run: | |
| 38 | + set +e |
| 39 | + W="$RUNNER_TEMP/bh"; mkdir -p "$W"; cd "$W" |
| 40 | + BIN="$GITHUB_WORKSPACE/target/release/socket-patch" |
| 41 | + R(){ echo "RESULT ${{ matrix.os }} ruby-${{ matrix.ruby }} $1 $2"; } |
| 42 | + ruby -v; gem install bundler -v 1.17.3 --no-document; gem install bundler -v 2.2.33 --no-document; gem list bundler |
| 43 | + gem fetch colorize -v 0.8.1 |
| 44 | + gem unpack colorize-0.8.1.gem --target pk |
| 45 | + cp pk/colorize-0.8.1/lib/colorize.rb orig.rb |
| 46 | + (cd pk/colorize-0.8.1 && gem spec ../../colorize-0.8.1.gem --ruby > c.gemspec && printf '# SOCKET_PATCHED\n' >> lib/colorize.rb && gem build c.gemspec && cp colorize-0.8.1.gem ../../patched.gem && cp lib/colorize.rb ../../after.rb) |
| 47 | + cat > mock.py <<'PY' |
| 48 | + import http.server, json, hashlib, sys |
| 49 | + PORT=18765; UUID="11111111-2222-4333-8444-555555555555"; TOKEN="tok"; N,V="colorize","0.8.1"; PURL=f"pkg:gem/{N}@{V}" |
| 50 | + patched=open("patched.gem","rb").read(); psha=hashlib.sha256(patched).hexdigest() |
| 51 | + B=open("orig.rb","rb").read(); A=open("after.rb","rb").read() |
| 52 | + def gh(b): return hashlib.sha256(b"blob %d\0"%len(b)+b).hexdigest() |
| 53 | + BASE=f"http://127.0.0.1:{PORT}"; REG=f"/patch-registry/gem/{TOKEN}/{UUID}" |
| 54 | + info=f"---\n{V} |checksum:{psha},ruby:>= 0\n" |
| 55 | + versions=f"created_at: 2026-01-01T00:00:00Z\n---\n{N} {V} {hashlib.md5(info.encode()).hexdigest()}\n" |
| 56 | + class H(http.server.BaseHTTPRequestHandler): |
| 57 | + def log_message(self,*a): pass |
| 58 | + def send(self,code,body,ct="application/json",etag=False): |
| 59 | + if isinstance(body,(dict,list)): body=json.dumps(body) |
| 60 | + if isinstance(body,str): body=body.encode() |
| 61 | + self.send_response(code); self.send_header("Content-Type",ct); self.send_header("Content-Length",str(len(body))) |
| 62 | + if etag: self.send_header("ETag",'"%s"'%hashlib.md5(body).hexdigest()) |
| 63 | + self.end_headers(); self.wfile.write(body) |
| 64 | + def do_POST(self): |
| 65 | + self.rfile.read(int(self.headers.get("Content-Length",0))); p=self.path.split("?")[0] |
| 66 | + if p.endswith("/patches/batch"): |
| 67 | + return self.send(200,{"packages":[{"purl":PURL,"patches":[{"uuid":UUID,"purl":PURL,"tier":"free","cveIds":[],"ghsaIds":["GHSA-aaaa-bbbb-cccc"],"severity":"high","title":"t"}]}],"canAccessPaidPatches":False}) |
| 68 | + if p.endswith("/patches/package"): |
| 69 | + u=f"{BASE}/patch/gem/{N}/{V}/{TOKEN}/{UUID}/{N}-{V}.gem" |
| 70 | + return self.send(200,{"results":{UUID:{"status":"granted","url":u,"purl":PURL,"artifacts":[{"kind":"tarball","url":u,"integrity":{"sha256":psha}}],"registryOverride":{"kind":"rubygems-compact-index","indexUrl":f"{BASE}{REG}/","identifiers":{"name":N,"version":V,"gemChecksumSha256":psha}}}}}) |
| 71 | + self.send(404,{}) |
| 72 | + def do_GET(self): |
| 73 | + p=self.path.split("?")[0] |
| 74 | + if "/patches/by-package/" in p: |
| 75 | + return self.send(200,{"patches":[{"uuid":UUID,"purl":PURL,"publishedAt":"2026-01-01T00:00:00Z","description":"x","license":"MIT","tier":"free","vulnerabilities":{}}],"canAccessPaidPatches":False}) |
| 76 | + if p.endswith(f"/patches/view/{UUID}"): |
| 77 | + return self.send(200,{"uuid":UUID,"purl":PURL,"publishedAt":"2026-01-01T00:00:00Z","files":{"lib/colorize.rb":{"beforeHash":gh(B),"afterHash":gh(A)}},"vulnerabilities":{"GHSA-aaaa-bbbb-cccc":{"cves":["CVE-2026-1"],"summary":"s","severity":"high","description":"d"}},"description":"x","license":"MIT","tier":"free"}) |
| 78 | + if p.startswith(REG): |
| 79 | + r=p[len(REG):] |
| 80 | + if r=="/versions": return self.send(200,versions,"text/plain",True) |
| 81 | + if r==f"/info/{N}": return self.send(200,info,"text/plain",True) |
| 82 | + if r=="/names": return self.send(200,f"---\n{N}\n","text/plain",True) |
| 83 | + if p.endswith(f"/{N}-{V}.gem"): return self.send(200,patched,"application/octet-stream") |
| 84 | + self.send(404,"nf","text/plain") |
| 85 | + http.server.ThreadingHTTPServer(("127.0.0.1",PORT),H).serve_forever() |
| 86 | + PY |
| 87 | + python mock.py & |
| 88 | + for i in $(seq 1 30); do curl -sf http://127.0.0.1:18765/patch-registry/gem/tok/11111111-2222-4333-8444-555555555555/names >/dev/null && break; sleep 1; done |
| 89 | + API=http://127.0.0.1:18765; AA="--api-url $API --org org --api-token fake" |
| 90 | + cycle(){ # BV SHAPE(Gemfile|gems.rb|twin) |
| 91 | + local BV=$1 SH=$2 TAG="$1-$2"; local D="$W/c-$TAG"; mkdir -p "$D/proj"; cd "$D/proj" |
| 92 | + local DECL='source "https://rubygems.org"\n\ngem "rake"\ngem "colorize", "~> 0.8.1"\n' |
| 93 | + bundle _${BV}_ config --local path vendor/bundle >/dev/null |
| 94 | + if [ $SH = twin ]; then printf "$DECL" > Gemfile; printf "$DECL" > gems.rb |
| 95 | + BUNDLE_GEMFILE=Gemfile bundle _${BV}_ lock >/dev/null 2>&1; BUNDLE_GEMFILE=gems.rb bundle _${BV}_ lock >/dev/null 2>&1 |
| 96 | + else printf "$DECL" > $SH; bundle _${BV}_ lock >/dev/null 2>&1; fi |
| 97 | + ls; cp -r . "$D/orig" |
| 98 | + "$BIN" scan --mode hosted --json --yes --cwd . $AA --vex out.vex.json --vex-product pkg:generic/app@1 > "$D/scan.json" 2>"$D/scan.err"; local sc=$? |
| 99 | + cat "$D/scan.err" | grep -v api-token | grep -v 'Got: fake' | head -5 |
| 100 | + local S=$(python -c "import json;e=json.load(open(r'$D/scan.json'));r=e.get('redirect') or {};print('status=%s redirected=%s files=%s warn=%s vex=%s'%(e.get('status'),r.get('redirected'),r.get('rewrittenFiles'),[w['code'] for w in r.get('warnings',[])],(e.get('vex') or {}).get('statements')))" 2>/dev/null) |
| 101 | + for f in Gemfile gems.rb; do [ -f $f ] && { echo "--- $f"; cat $f; }; done |
| 102 | + mkdir "$D/fresh"; cp -r . "$D/fresh/"; rm -rf "$D/fresh/vendor" |
| 103 | + (cd "$D/fresh" && bundle _${BV}_ install 2>&1 | tail -3; echo "installexit=${PIPESTATUS[0]}") |
| 104 | + local P=$(cat "$D"/fresh/vendor/bundle/ruby/*/gems/colorize-0.8.1/lib/colorize.rb 2>/dev/null | grep -c SOCKET_PATCHED) |
| 105 | + local LS=$(cd "$D/fresh" && bundle _${BV}_ exec ruby -e 'print Bundler.default_gemfile.basename' 2>/dev/null) |
| 106 | + "$BIN" vex --output post.json --product pkg:generic/app@1 --cwd "$D/fresh" --patch-server-url $API $AA > "$D/vex.out" 2>&1; local VX=$? |
| 107 | + R $TAG "scan=$sc $S | bundler_loads=$LS installed_patched=$P post_vex_exit=$VX" |
| 108 | + cd "$W" |
| 109 | + } |
| 110 | + for BV in 1.17.3 2.2.33; do for SH in Gemfile gems.rb twin; do cycle $BV $SH; done; done |
| 111 | + grep -h RESULT /dev/null; true |
0 commit comments