diff --git a/docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md b/docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md index 638129b9..d75492f2 100644 --- a/docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md +++ b/docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md @@ -64,17 +64,22 @@ Project documents added in Linear: | Surface | Evidence | | --- | --- | | PR #1921 | Merged supply-chain IOC expansion for Mini Shai-Hulud/TanStack follow-up | +| Node IPC follow-up | Added May 14 `node-ipc` malicious-version, hash, DNS, and runtime IOC coverage | | Merge commit | `f04702bdac132662c8496e817bcd850c86e2b854` | -| Local IOC tests | `node tests/ci/scan-supply-chain-iocs.test.js` passed 11/11 | +| Local IOC tests | `node tests/ci/scan-supply-chain-iocs.test.js` passed 12/12 | | Unicode safety | `node scripts/ci/check-unicode-safety.js` passed | | IOC scan | `npm run security:ioc-scan` passed | -| Root suite | `npm test` passed 2426/2426, 0 failed | +| Root suite | `npm test` passed 2427/2427, 0 failed | | Repo sweeps | IOC scanner sweep passed for trunk, AgentShield, ECC Tools, ECC website, JARVIS, and the ECC document mirror | The May 15 IOC expansion added coverage for OpenSearch/Mistral/Guardrails/ UiPath/Squawk-style campaign variants, `opensearch_init.js`, `vite_setup.mjs`, dead-drop/session protocol strings, and AI-tooling persistence surfaces without committing full high-entropy indicators that trip secret scanners. +The May 15 node-ipc follow-up blocks `node-ipc@9.1.6`, `9.2.3`, `10.1.1`, +`10.1.2`, `11.0.0`, `11.1.0`, and `12.0.1`, plus the `node-ipc.cjs` payload +hash, malicious tarball hashes, DNS exfil domains, and runtime markers reported +by Socket. ## Current Publication Blockers diff --git a/docs/security/supply-chain-incident-response.md b/docs/security/supply-chain-incident-response.md index c2653691..383d54e1 100644 --- a/docs/security/supply-chain-incident-response.md +++ b/docs/security/supply-chain-incident-response.md @@ -21,6 +21,10 @@ credentials: - Follow-on reporting from StepSecurity, Socket, Aikido, and Wiz describes the same campaign expanding into packages associated with Mistral AI, UiPath, OpenSearch, Guardrails AI, Squawk, and other npm/PyPI packages. +- Socket's 2026-05-14 `node-ipc` report describes a separate active npm + compromise affecting `node-ipc` versions `9.1.6`, `9.2.3`, and `12.0.1`, + with historical malicious `node-ipc` versions also blocked by ECC because + they carried destructive or unauthorized file-writing behavior. - 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. Some variants add a @@ -35,6 +39,12 @@ credentials: `opensearch_init.js`, `vite_setup.mjs`, campaign salt `svksjrhjkcejg`, Session protocol strings, `claude@users.noreply.github.com` dead-drop commits, `dependabout/` branch names, and `OhNoWhatsGoingOnWithGitHub`. +- The `node-ipc` sweep watches for `node-ipc.cjs` payload hash + `96097e06...d9034144`, tarball hashes for the malicious `9.1.6`, `9.2.3`, + and `12.0.1` artifacts, `sh.azurestaticprovider.net`, `bt.node.js`, + `37.16.75.69`, DNS exfil labels `xh` / `xd` / `xf` where present in + artifacts, `__ntw`, `__ntRun`, `/nt-` temp archives, and archive entries such + as `uname.txt`, `envs.txt`, and `fixtures/_paths.txt`. - 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. @@ -47,6 +57,7 @@ Primary references: - - - +- - - diff --git a/scripts/ci/scan-supply-chain-iocs.js b/scripts/ci/scan-supply-chain-iocs.js index af79aede..7394288b 100755 --- a/scripts/ci/scan-supply-chain-iocs.js +++ b/scripts/ci/scan-supply-chain-iocs.js @@ -5,6 +5,7 @@ */ const fs = require('fs'); +const crypto = require('crypto'); const os = require('os'); const path = require('path'); @@ -204,6 +205,7 @@ const MALICIOUS_PACKAGE_VERSIONS = { 'mbt': ['1.2.48'], 'mistralai': ['2.4.6'], 'ml-toolkit-ts': ['1.0.4', '1.0.5'], + 'node-ipc': ['9.1.6', '9.2.3', '10.1.1', '10.1.2', '11.0.0', '11.1.0', '12.0.1'], 'nextmove-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.7'], 'safe-action': ['0.8.3', '0.8.4'], 'ts-dna': ['3.0.1', '3.0.2', '3.0.3', '3.0.4', '3.0.5'], @@ -266,8 +268,60 @@ const CRITICAL_TEXT_INDICATORS = [ 'PUSH UR T3MPRR', 'codeql_analysis.yml', 'shai-hulud-workflow.yml', + [ + '96097e0612d9575c', + 'b133021017fb1a5c', + '68a03b60f9f3d24e', + 'bdc0e628d9034144', + ].join(''), + [ + '449e4265979b5fdb', + '2d3446c021af437e', + '815debd66de7da2f', + 'e54f1ad93cbcc75e', + ].join(''), + [ + 'c2f4dc64aec46315', + '40a568e88932b61d', + 'aebbfb7e8281b812', + 'fa01b7215f9be9ea', + ].join(''), + [ + '78a82d93b4f58083', + '5f5823b85a3d9ee1', + 'f03a15ee6f0e01b', + '4eac86252a7002981', + ].join(''), + 'sh.azurestaticprovider.net', + '37.16.75.69', + 'bt.node.js', + '__ntw', + '__ntRun', + '/nt-', + 'uname.txt', + 'envs.txt', + 'fixtures/_paths.txt', ]; +const MALICIOUS_FILE_HASHES = { + '96097e0612d9575cb133021017fb1a5c68a03b60f9f3d24ebdc0e628d9034144': { + indicator: 'node-ipc.cjs sha256', + message: 'Known malicious node-ipc CommonJS payload hash is present', + }, + '449e4265979b5fdb2d3446c021af437e815debd66de7da2fe54f1ad93cbcc75e': { + indicator: 'node-ipc-9.1.6.tgz sha256', + message: 'Known malicious node-ipc tarball hash is present', + }, + 'c2f4dc64aec4631540a568e88932b61daebbfb7e8281b812fa01b7215f9be9ea': { + indicator: 'node-ipc-9.2.3.tgz sha256', + message: 'Known malicious node-ipc tarball hash is present', + }, + '78a82d93b4f580835f5823b85a3d9ee1f03a15ee6f0e01b4eac86252a7002981': { + indicator: 'node-ipc-12.0.1.tar.gz sha256', + message: 'Known malicious node-ipc tarball hash is present', + }, +}; + const DEPENDENCY_FILENAMES = new Set([ 'package.json', 'package-lock.json', @@ -279,6 +333,13 @@ const DEPENDENCY_FILENAMES = new Set([ 'requirements.txt', ]); +const INSPECT_ONLY_FILENAMES = new Set([ + 'node-ipc.cjs', + 'node-ipc-9.1.6.tgz', + 'node-ipc-9.2.3.tgz', + 'node-ipc-12.0.1.tar.gz', +]); + const PERSISTENCE_FILENAMES = new Set([ 'settings.json', 'tasks.json', @@ -342,6 +403,7 @@ function shouldInspectFile(filePath) { if (DEPENDENCY_FILENAMES.has(base)) return true; if (PERSISTENCE_FILENAMES.has(base) && isInSpecialConfigPath(filePath)) return true; if (PAYLOAD_FILENAMES.has(base) && filePath.includes(`${path.sep}node_modules${path.sep}`)) return true; + if (INSPECT_ONLY_FILENAMES.has(base)) return true; return false; } @@ -392,7 +454,13 @@ function walkNodeModules(nodeModulesDir, files) { } function inspectPackageDir(packageDir, files) { - for (const filename of [...DEPENDENCY_FILENAMES, ...PAYLOAD_FILENAMES, 'setup.mjs', 'execution.js']) { + for (const filename of [ + ...DEPENDENCY_FILENAMES, + ...PAYLOAD_FILENAMES, + ...INSPECT_ONLY_FILENAMES, + 'setup.mjs', + 'execution.js', + ]) { const candidate = path.join(packageDir, filename); if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) { files.push(candidate); @@ -408,6 +476,14 @@ function readText(filePath) { } } +function sha256File(filePath) { + try { + return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex'); + } catch { + return ''; + } +} + function lineForIndex(text, index) { return text.slice(0, index).split(/\r?\n/).length; } @@ -425,6 +501,18 @@ function scanFile(filePath, rootDir, findings) { const relativePath = path.relative(rootDir, filePath) || filePath; const text = readText(filePath); const lowerText = normalizeForMatch(text); + const hashFinding = MALICIOUS_FILE_HASHES[sha256File(filePath)]; + + if (hashFinding) { + addFinding( + findings, + 'critical', + relativePath, + 1, + hashFinding.indicator, + hashFinding.message, + ); + } if (PAYLOAD_FILENAMES.has(base)) { addFinding( @@ -492,8 +580,14 @@ function runtimeTargets() { return [ '/tmp/transformers.pyz', '/tmp/pgmonitor.py', + '/tmp/node-ipc-9.1.6.tgz', + '/tmp/node-ipc-9.2.3.tgz', + '/tmp/node-ipc-12.0.1.tar.gz', '/private/tmp/transformers.pyz', '/private/tmp/pgmonitor.py', + '/private/tmp/node-ipc-9.1.6.tgz', + '/private/tmp/node-ipc-9.2.3.tgz', + '/private/tmp/node-ipc-12.0.1.tar.gz', ]; } @@ -575,6 +669,7 @@ if (require.main === module) { module.exports = { CRITICAL_TEXT_INDICATORS, + MALICIOUS_FILE_HASHES, MALICIOUS_PACKAGE_VERSIONS, scanSupplyChainIocs, }; diff --git a/tests/ci/scan-supply-chain-iocs.test.js b/tests/ci/scan-supply-chain-iocs.test.js index 58975661..2bbd48d7 100755 --- a/tests/ci/scan-supply-chain-iocs.test.js +++ b/tests/ci/scan-supply-chain-iocs.test.js @@ -104,6 +104,41 @@ function run() { }); })) passed++; else failed++; + if (test('rejects node-ipc campaign package versions and CJS indicators', () => { + withFixture({ + 'package-lock.json': JSON.stringify({ + packages: { + 'node_modules/node-ipc': { + version: '12.0.1', + }, + }, + }, null, 2), + 'node_modules/node-ipc/package.json': JSON.stringify({ + name: 'node-ipc', + version: '9.2.3', + }, null, 2), + 'node_modules/node-ipc/node-ipc.cjs': [ + 'const host = "sh.azurestaticprovider.net";', + 'const zone = "bt.node.js";', + 'process.env.__ntw = "1";', + 'module.exports.__ntRun = true;', + 'const archive = "/nt-/sample.tar.gz";', + 'const entries = ["uname.txt", "envs.txt", "fixtures/_paths.txt"];', + ].join('\n'), + }, rootDir => { + const result = scanSupplyChainIocs({ rootDir }); + const indicators = result.findings.map(finding => finding.indicator); + assert.ok(indicators.includes('node-ipc@12.0.1')); + assert.ok(indicators.includes('node-ipc@9.2.3')); + assert.ok(indicators.includes('sh.azurestaticprovider.net')); + assert.ok(indicators.includes('bt.node.js')); + assert.ok(indicators.includes('__ntw')); + assert.ok(indicators.includes('__ntRun')); + assert.ok(indicators.includes('/nt-')); + assert.ok(indicators.includes('fixtures/_paths.txt')); + }); + })) passed++; else failed++; + if (test('passes clean versions of watched packages', () => { withFixture({ 'package-lock.json': JSON.stringify({