Skip to content

Commit fb19799

Browse files
committed
bughunt: composer global-mode probe
1 parent 2463257 commit fb19799

1 file changed

Lines changed: 121 additions & 0 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
name: bughunt composer global probe
2+
on:
3+
push:
4+
branches: ['bughunt/composer/**']
5+
permissions:
6+
contents: read
7+
jobs:
8+
probe:
9+
name: ${{ matrix.os }} / composer ${{ matrix.composer }}
10+
runs-on: ${{ matrix.os }}
11+
timeout-minutes: 45
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
os: [ubuntu-latest, macos-latest, windows-latest]
16+
composer: ['1.10.28', '2.2.30', '2.10.3']
17+
steps:
18+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
19+
with:
20+
persist-credentials: false
21+
- run: rustup show
22+
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
23+
with:
24+
key: bughunt-composer-global
25+
save-if: false
26+
- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
27+
with:
28+
php-version: ${{ matrix.composer == '1.10.28' && '8.1' || '8.4' }}
29+
tools: composer:${{ matrix.composer }}
30+
extensions: zip, mbstring, curl, openssl
31+
coverage: none
32+
- run: cargo build --release -p socket-patch-cli
33+
- name: Write mock
34+
shell: bash
35+
run: |
36+
mkdir -p "$RUNNER_TEMP/m"
37+
cat > "$RUNNER_TEMP/m/mock.py" <<'EOF'
38+
import json,base64,hashlib,sys,re
39+
from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer
40+
ORIG=open(sys.argv[2],'rb').read(); PAT=open(sys.argv[3],'rb').read()
41+
FILE=sys.argv[4]; NAME=sys.argv[5]; VER=sys.argv[6]
42+
def gh(b): return hashlib.sha256(b"blob %d\0"%len(b)+b).hexdigest()
43+
UUID="11111111-2222-4333-8444-555555555555"
44+
PURL=f"pkg:composer/{NAME}@{VER}"
45+
LOG=open(sys.argv[7],'a')
46+
class H(BaseHTTPRequestHandler):
47+
def _j(self,o,c=200):
48+
b=json.dumps(o).encode(); self.send_response(c); self.send_header('content-type','application/json'); self.send_header('content-length',str(len(b))); self.end_headers(); self.wfile.write(b)
49+
def do_POST(self):
50+
n=int(self.headers.get('content-length',0)); body=self.rfile.read(n).decode()
51+
LOG.write(f"POST {self.path} {body}\n"); LOG.flush()
52+
if self.path.endswith('/batch'):
53+
req=json.loads(body); comps=[c.get('purl') for c in req.get('components',[])]
54+
pk=[{"purl":p,"patches":[{"uuid":UUID,"purl":PURL,"tier":"free","cveIds":["CVE-2026-0001"],"ghsaIds":["GHSA-aaaa-bbbb-cccc"],"severity":"high","title":"t"}]} for p in comps if p and p.split('@')[0]==PURL.split('@')[0]]
55+
return self._j({"packages":pk,"canAccessPaidPatches":False})
56+
self._j({},404)
57+
def do_GET(self):
58+
LOG.write(f"GET {self.path}\n"); LOG.flush()
59+
if '/blob/' in self.path:
60+
h=self.path.rsplit('/',1)[1]; m={gh(ORIG):ORIG,gh(PAT):PAT}
61+
if h in m:
62+
b=m[h]; self.send_response(200); self.send_header('content-length',str(len(b))); self.end_headers(); self.wfile.write(b); return
63+
if '/view/' in self.path:
64+
return self._j({"uuid":UUID,"purl":PURL,"publishedAt":"2026-01-01T00:00:00Z","files":{f"package/{FILE}":{"beforeHash":gh(ORIG),"afterHash":gh(PAT),"blobContent":base64.b64encode(PAT).decode(),"beforeBlobContent":base64.b64encode(ORIG).decode()}},"vulnerabilities":{"GHSA-aaaa-bbbb-cccc":{"cves":["CVE-2026-0001"],"summary":"s","severity":"high","description":"d"}},"description":"x","license":"MIT","tier":"free"})
65+
if '/by-package/' in self.path or '/by-' in self.path:
66+
return self._j({"patches":[{"uuid":UUID,"purl":PURL,"publishedAt":"2026-01-01T00:00:00Z","description":"x","license":"MIT","tier":"free","vulnerabilities":{"GHSA-aaaa-bbbb-cccc":{"cves":["CVE-2026-0001"],"summary":"s","severity":"high","description":"d"}}}],"canAccessPaidPatches":False})
67+
self._j({},404)
68+
def log_message(self,*a): pass
69+
ThreadingHTTPServer(('127.0.0.1',int(sys.argv[1])),H).serve_forever()
70+
EOF
71+
- name: Probe global mode
72+
shell: bash
73+
run: |
74+
set +e
75+
T="$RUNNER_TEMP"; if command -v cygpath >/dev/null; then T="$(cygpath -m "$RUNNER_TEMP")"; fi
76+
B="$PWD/target/release/socket-patch"; [ -f "$B.exe" ] && B="$B.exe"
77+
export COMPOSER_ALLOW_SUPERUSER=1 SOCKET_TELEMETRY_DISABLED=1 COMPOSER_NO_INTERACTION=1
78+
unset COMPOSER_HOME
79+
composer --version
80+
mkdir -p "$T/pkg/bin" "$T/pkg/src" "$T/work"
81+
printf '{"name":"acme/tool","version":"1.0.0","type":"library","bin":["bin/acme-tool"]}\n' > "$T/pkg/composer.json"
82+
printf '<?php\nnamespace Acme\\Tool;\nfunction msg() { return "VULNERABLE"; }\n' > "$T/pkg/src/Msg.php"
83+
printf '#!/usr/bin/env php\n<?php\nrequire __DIR__."/../src/Msg.php";\necho \\Acme\\Tool\\msg(), "\\n";\n' > "$T/pkg/bin/acme-tool"
84+
cp "$T/pkg/src/Msg.php" "$T/orig.php"; sed 's/VULNERABLE/PATCHED/' "$T/orig.php" > "$T/patched.php"
85+
python3 "$RUNNER_TEMP/m/mock.py" 18999 "$T/orig.php" "$T/patched.php" src/Msg.php acme/tool 1.0.0 "$T/mock.log" &
86+
sleep 3
87+
A="--api-url http://127.0.0.1:18999 --org o --api-token fake --ecosystems composer"
88+
R() { echo "RESULT ${{ matrix.os }} composer-${{ matrix.composer }} $1 $2"; }
89+
REPO='{"type":"path","url":"'"$T/pkg"'","options":{"symlink":false}}'
90+
# --- 1. default home
91+
GH="$(composer global config home 2>/dev/null | tail -1)"; echo "default home: $GH"
92+
composer global config repositories.local "$REPO"
93+
composer global require acme/tool:1.0.0 2>&1 | tail -3
94+
F="$GH/vendor/acme/tool/src/Msg.php"; grep -o 'VULNERABLE\|PATCHED' "$F"
95+
cd "$T/work"
96+
"$B" scan -g --json $A > s.json 2>s.err; echo "scan exit $?"
97+
N=$(python3 -c "import json;print(json.load(open('s.json')).get('packagesWithPatches'))"); [ "$N" = 1 ] && R scan-g pass || { R scan-g "fail($N)"; cat s.err; }
98+
"$B" scan -g --mode hosted $A >/dev/null 2>h.err; e=$?; [ $e = 2 ] && R hosted-refusal pass || R hosted-refusal "fail(exit $e)"
99+
"$B" scan -g --mode agent --yes $A 2>&1 | tail -2
100+
grep -q PATCHED "$F" && R apply-g pass || R apply-g fail
101+
"$B" vex -g $A --product pkg:composer/x@1 -O v.json 2>&1 | tail -1
102+
N=$(python3 -c "import json;print(len(json.load(open('v.json'))['statements']))" 2>/dev/null); [ "$N" = 1 ] && R vex-g pass || R vex-g "fail($N)"
103+
"$B" rollback -g $A --yes 2>&1 | tail -1
104+
cmp -s "$F" "$T/orig.php" && R rollback-g pass || R rollback-g fail
105+
# --- 2. composer NOT on PATH, default home (fallback resolution)
106+
CDIR="$(dirname "$(command -v composer)")"; echo "composer dir: $CDIR"
107+
NP="$(echo "$PATH" | tr ':' '\n' | grep -vxF "$CDIR" | paste -sd: -)"
108+
PATH="$NP" bash -c 'command -v composer || echo "composer not on PATH"'
109+
PATH="$NP" "$B" scan -g --json $A > s2.json 2>s2.err
110+
N=$(python3 -c "import json;print(json.load(open('s2.json')).get('packagesWithPatches'))"); [ "$N" = 1 ] && R scan-g-no-composer-on-path pass || { R scan-g-no-composer-on-path "fail($N)"; tail -2 s2.err; }
111+
# --- 3. custom global vendor-dir (explicit COMPOSER_HOME)
112+
export COMPOSER_HOME="$T/ch2"; mkdir -p "$COMPOSER_HOME"
113+
composer global config repositories.local "$REPO"
114+
composer global config vendor-dir deps
115+
composer global require acme/tool:1.0.0 2>&1 | tail -1
116+
ls "$COMPOSER_HOME"
117+
"$B" scan -g --json $A > s3.json 2>s3.err
118+
N=$(python3 -c "import json;print(json.load(open('s3.json')).get('packagesWithPatches'))"); [ "$N" = 1 ] && R scan-g-vendor-dir pass || { R scan-g-vendor-dir "fail($N)"; tail -2 s3.err; }
119+
"$B" scan -g --mode agent --yes $A 2>&1 | tail -1
120+
grep -q PATCHED "$COMPOSER_HOME/deps/acme/tool/src/Msg.php" && R apply-g-vendor-dir pass || R apply-g-vendor-dir fail
121+
exit 0

0 commit comments

Comments
 (0)