mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-17 22:33:06 +08:00
security: add node-ipc IOC coverage (#1924)
This commit is contained in:
@@ -64,17 +64,22 @@ Project documents added in Linear:
|
|||||||
| Surface | Evidence |
|
| Surface | Evidence |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| PR #1921 | Merged supply-chain IOC expansion for Mini Shai-Hulud/TanStack follow-up |
|
| 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` |
|
| 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 |
|
| Unicode safety | `node scripts/ci/check-unicode-safety.js` passed |
|
||||||
| IOC scan | `npm run security:ioc-scan` 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 |
|
| 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/
|
The May 15 IOC expansion added coverage for OpenSearch/Mistral/Guardrails/
|
||||||
UiPath/Squawk-style campaign variants, `opensearch_init.js`, `vite_setup.mjs`,
|
UiPath/Squawk-style campaign variants, `opensearch_init.js`, `vite_setup.mjs`,
|
||||||
dead-drop/session protocol strings, and AI-tooling persistence surfaces without
|
dead-drop/session protocol strings, and AI-tooling persistence surfaces without
|
||||||
committing full high-entropy indicators that trip secret scanners.
|
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
|
## Current Publication Blockers
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ credentials:
|
|||||||
- Follow-on reporting from StepSecurity, Socket, Aikido, and Wiz describes the
|
- Follow-on reporting from StepSecurity, Socket, Aikido, and Wiz describes the
|
||||||
same campaign expanding into packages associated with Mistral AI, UiPath,
|
same campaign expanding into packages associated with Mistral AI, UiPath,
|
||||||
OpenSearch, Guardrails AI, Squawk, and other npm/PyPI packages.
|
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
|
- The live IOC set includes persistence through Claude Code
|
||||||
`.claude/settings.json`, VS Code `.vscode/tasks.json`, and OS-level
|
`.claude/settings.json`, VS Code `.vscode/tasks.json`, and OS-level
|
||||||
`gh-token-monitor` LaunchAgent/systemd services. Some variants add a
|
`gh-token-monitor` LaunchAgent/systemd services. Some variants add a
|
||||||
@@ -35,6 +39,12 @@ credentials:
|
|||||||
`opensearch_init.js`, `vite_setup.mjs`, campaign salt `svksjrhjkcejg`,
|
`opensearch_init.js`, `vite_setup.mjs`, campaign salt `svksjrhjkcejg`,
|
||||||
Session protocol strings, `claude@users.noreply.github.com` dead-drop
|
Session protocol strings, `claude@users.noreply.github.com` dead-drop
|
||||||
commits, `dependabout/` branch names, and `OhNoWhatsGoingOnWithGitHub`.
|
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
|
- The attack chain combined `pull_request_target`, GitHub Actions cache
|
||||||
poisoning across a fork/base trust boundary, and OIDC token extraction from a
|
poisoning across a fork/base trust boundary, and OIDC token extraction from a
|
||||||
GitHub Actions runner.
|
GitHub Actions runner.
|
||||||
@@ -47,6 +57,7 @@ Primary references:
|
|||||||
- <https://tanstack.com/blog/npm-supply-chain-compromise-postmortem>
|
- <https://tanstack.com/blog/npm-supply-chain-compromise-postmortem>
|
||||||
- <https://github.com/advisories/GHSA-g7cv-rxg3-hmpx>
|
- <https://github.com/advisories/GHSA-g7cv-rxg3-hmpx>
|
||||||
- <https://tanstack.com/blog/incident-followup>
|
- <https://tanstack.com/blog/incident-followup>
|
||||||
|
- <https://socket.dev/blog/node-ipc-package-compromised>
|
||||||
- <https://docs.npmjs.com/trusted-publishers/>
|
- <https://docs.npmjs.com/trusted-publishers/>
|
||||||
- <https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem>
|
- <https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const crypto = require('crypto');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
@@ -204,6 +205,7 @@ const MALICIOUS_PACKAGE_VERSIONS = {
|
|||||||
'mbt': ['1.2.48'],
|
'mbt': ['1.2.48'],
|
||||||
'mistralai': ['2.4.6'],
|
'mistralai': ['2.4.6'],
|
||||||
'ml-toolkit-ts': ['1.0.4', '1.0.5'],
|
'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'],
|
'nextmove-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.7'],
|
||||||
'safe-action': ['0.8.3', '0.8.4'],
|
'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'],
|
'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',
|
'PUSH UR T3MPRR',
|
||||||
'codeql_analysis.yml',
|
'codeql_analysis.yml',
|
||||||
'shai-hulud-workflow.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([
|
const DEPENDENCY_FILENAMES = new Set([
|
||||||
'package.json',
|
'package.json',
|
||||||
'package-lock.json',
|
'package-lock.json',
|
||||||
@@ -279,6 +333,13 @@ const DEPENDENCY_FILENAMES = new Set([
|
|||||||
'requirements.txt',
|
'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([
|
const PERSISTENCE_FILENAMES = new Set([
|
||||||
'settings.json',
|
'settings.json',
|
||||||
'tasks.json',
|
'tasks.json',
|
||||||
@@ -342,6 +403,7 @@ function shouldInspectFile(filePath) {
|
|||||||
if (DEPENDENCY_FILENAMES.has(base)) return true;
|
if (DEPENDENCY_FILENAMES.has(base)) return true;
|
||||||
if (PERSISTENCE_FILENAMES.has(base) && isInSpecialConfigPath(filePath)) 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 (PAYLOAD_FILENAMES.has(base) && filePath.includes(`${path.sep}node_modules${path.sep}`)) return true;
|
||||||
|
if (INSPECT_ONLY_FILENAMES.has(base)) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +454,13 @@ function walkNodeModules(nodeModulesDir, files) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function inspectPackageDir(packageDir, 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);
|
const candidate = path.join(packageDir, filename);
|
||||||
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
||||||
files.push(candidate);
|
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) {
|
function lineForIndex(text, index) {
|
||||||
return text.slice(0, index).split(/\r?\n/).length;
|
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 relativePath = path.relative(rootDir, filePath) || filePath;
|
||||||
const text = readText(filePath);
|
const text = readText(filePath);
|
||||||
const lowerText = normalizeForMatch(text);
|
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)) {
|
if (PAYLOAD_FILENAMES.has(base)) {
|
||||||
addFinding(
|
addFinding(
|
||||||
@@ -492,8 +580,14 @@ function runtimeTargets() {
|
|||||||
return [
|
return [
|
||||||
'/tmp/transformers.pyz',
|
'/tmp/transformers.pyz',
|
||||||
'/tmp/pgmonitor.py',
|
'/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/transformers.pyz',
|
||||||
'/private/tmp/pgmonitor.py',
|
'/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 = {
|
module.exports = {
|
||||||
CRITICAL_TEXT_INDICATORS,
|
CRITICAL_TEXT_INDICATORS,
|
||||||
|
MALICIOUS_FILE_HASHES,
|
||||||
MALICIOUS_PACKAGE_VERSIONS,
|
MALICIOUS_PACKAGE_VERSIONS,
|
||||||
scanSupplyChainIocs,
|
scanSupplyChainIocs,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -104,6 +104,41 @@ function run() {
|
|||||||
});
|
});
|
||||||
})) passed++; else failed++;
|
})) 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', () => {
|
if (test('passes clean versions of watched packages', () => {
|
||||||
withFixture({
|
withFixture({
|
||||||
'package-lock.json': JSON.stringify({
|
'package-lock.json': JSON.stringify({
|
||||||
|
|||||||
Reference in New Issue
Block a user