Skip to content

Commit 9a29064

Browse files
committed
bughunt probe: Bundler 1.17 / 2.2 hosted cycle
1 parent 045d7ec commit 9a29064

1 file changed

Lines changed: 111 additions & 0 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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

Comments
 (0)