From e3f18d23765ef24e9d8d0cc317874cec8374353c Mon Sep 17 00:00:00 2001 From: daiki75 Date: Tue, 16 Jun 2026 02:48:50 +0900 Subject: [PATCH] fix: prevent IOC scanner false positives on hook filenames and scan .cursor configs (#2245) * fix: prevent IOC scanner false positives on hook filenames and scan .cursor configs The supply-chain IOC scanner matched CRITICAL_TEXT_INDICATORS with plain substring search, so legitimate hook filenames that merely end with a known payload name (e.g. the stock Cursor hook before-shell-execution.js vs the payload execution.js) were flagged as CRITICAL. Indicator matching now requires a non-filename character before the match. Also add .cursor/ to the special config paths so Cursor hooks.json files (a known persistence vector already listed in PERSISTENCE_FILENAMES) are actually inspected in normal checkouts - previously they were only scanned by accident when the repo path happened to contain /.claude/. * test: cover underscore-prefixed filenames in IOC boundary suppression Make explicit that '_' is treated as a filename word character, so snake_case hook names like post_execution.js are intentionally not flagged by the execution.js indicator (real payload references appear after '/', quotes, or whitespace). --- scripts/ci/scan-supply-chain-iocs.js | 18 ++++++++---- tests/ci/scan-supply-chain-iocs.test.js | 39 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/scripts/ci/scan-supply-chain-iocs.js b/scripts/ci/scan-supply-chain-iocs.js index ad8bd529..7b4c9569 100755 --- a/scripts/ci/scan-supply-chain-iocs.js +++ b/scripts/ci/scan-supply-chain-iocs.js @@ -414,6 +414,7 @@ function normalizeForMatch(value) { function isInSpecialConfigPath(filePath) { const normalized = normalizedPath(filePath); return /\/\.claude\//.test(normalized) + || /\/\.cursor\//.test(normalized) || /\/\.vscode\//.test(normalized) || /\/\.kiro\/settings\//.test(normalized) || /\/Library\/LaunchAgents\//.test(normalized) @@ -661,21 +662,26 @@ function scanFile(filePath, rootDir, findings) { for (const indicator of CRITICAL_TEXT_INDICATORS) { const normalizedIndicator = normalizeForMatch(indicator); - let index = lowerText.indexOf(normalizedIndicator); - while (index !== -1) { - if (!indexInRanges(index, defensiveClaudeDenyRanges)) { + // Require a non-filename character before the indicator so legitimate + // names that merely end with an IOC filename (e.g. the stock Cursor hook + // `before-shell-execution.js` vs the payload `execution.js`) do not match. + const indicatorPattern = new RegExp( + `(? { + withFixture({ + '.cursor/hooks.json': JSON.stringify({ + hooks: { + beforeShellExecution: [{ + command: 'node .cursor/hooks/before-shell-execution.js', + }], + afterShellExecution: [{ + command: 'node .cursor/hooks/after-shell-execution.js', + }], + beforeMCPExecution: [{ + command: 'node .cursor/hooks/before-mcp-execution.js', + }], + afterFileEdit: [{ + command: 'node .cursor/hooks/post_execution.js', + }], + }, + }, null, 2), + }, rootDir => { + const result = scanSupplyChainIocs({ rootDir }); + assert.deepStrictEqual(result.findings, []); + }); + })) passed++; else failed++; + + if (test('still flags the bare execution.js payload after a path separator', () => { + withFixture({ + '.cursor/hooks.json': JSON.stringify({ + hooks: { + sessionStart: [{ + command: 'node .cursor/hooks/execution.js', + }], + }, + }, null, 2), + }, rootDir => { + const result = scanSupplyChainIocs({ rootDir }); + assert.ok(result.findings.some(finding => finding.indicator === 'execution.js')); + }); + })) passed++; else failed++; + if (test('rejects user-level VS Code task persistence when home scan is enabled', () => { withFixture({ 'home/Library/Application Support/Code/User/tasks.json': JSON.stringify({