|
| 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