diff --git a/scripts/hardening-inventory.js b/scripts/hardening-inventory.js index f3ba4946..549fe35a 100644 --- a/scripts/hardening-inventory.js +++ b/scripts/hardening-inventory.js @@ -66,6 +66,22 @@ const SYNC_CALL_CAPTURE_REGEX = new RegExp( ); const LEGACY_MARKER_REGEX = /(?:\blegacy\b|\bshim\b|backward compatibility|backwards compatibility|compatibility layer|deprecated.*re-export|re-export.*compatibility)/i; +const REGEX_LITERAL_KEYWORDS = new Set([ + 'return', + 'throw', + 'case', + 'else', + 'do', + 'delete', + 'void', + 'typeof', + 'instanceof', + 'in', + 'of', + 'yield', + 'await', + 'new', +]); function toPosixPath(filePath) { return filePath.split(path.sep).join('/'); @@ -124,9 +140,11 @@ function summarize(items, limit = 10) { })); } -function isRegexLiteralStart(previousSignificantChar) { +function isRegexLiteralStart(previousSignificantChar, previousIdentifier) { return ( - previousSignificantChar === '' || '([{:;,=!?+-*%^&|~<>'.includes(previousSignificantChar) + previousSignificantChar === '' || + '([{:;,=!?+-*%^&|~<>'.includes(previousSignificantChar) || + REGEX_LITERAL_KEYWORDS.has(previousIdentifier) ); } @@ -141,6 +159,7 @@ function stripComments(sourceText) { let inRegexLiteral = false; let inRegexCharClass = false; let previousSignificantChar = ''; + let previousIdentifier = ''; while (index < sourceText.length) { const current = sourceText[index]; @@ -207,6 +226,7 @@ function stripComments(sourceText) { } inRegexLiteral = false; previousSignificantChar = 'r'; + previousIdentifier = ''; continue; } @@ -224,6 +244,7 @@ function stripComments(sourceText) { if (current === "'") { inSingleQuote = false; previousSignificantChar = 's'; + previousIdentifier = ''; } index += 1; continue; @@ -239,6 +260,7 @@ function stripComments(sourceText) { if (current === '"') { inDoubleQuote = false; previousSignificantChar = 's'; + previousIdentifier = ''; } index += 1; continue; @@ -254,6 +276,7 @@ function stripComments(sourceText) { if (current === '`') { inTemplateLiteral = false; previousSignificantChar = 's'; + previousIdentifier = ''; } index += 1; continue; @@ -273,7 +296,7 @@ function stripComments(sourceText) { continue; } - if (current === '/' && isRegexLiteralStart(previousSignificantChar)) { + if (current === '/' && isRegexLiteralStart(previousSignificantChar, previousIdentifier)) { output += ' '; index += 1; inRegexLiteral = true; @@ -281,6 +304,19 @@ function stripComments(sourceText) { continue; } + if (/[A-Za-z_$]/.test(current)) { + let tokenEnd = index + 1; + while (tokenEnd < sourceText.length && /[A-Za-z0-9_$]/.test(sourceText[tokenEnd])) { + tokenEnd += 1; + } + const token = sourceText.slice(index, tokenEnd); + output += token; + previousSignificantChar = 'i'; + previousIdentifier = token; + index = tokenEnd; + continue; + } + if (current === "'") { inSingleQuote = true; output += ' '; @@ -305,6 +341,7 @@ function stripComments(sourceText) { output += current; if (!/\s/.test(current)) { previousSignificantChar = current; + previousIdentifier = ''; } index += 1; } @@ -477,4 +514,13 @@ function main() { console.log(`[hardening-inventory] wrote ${relMd}`); } -main(); +if (require.main === module) { + main(); +} + +module.exports = { + buildReport, + collectSyncCallSites, + renderMarkdown, + stripComments, +}; diff --git a/tests/unit/scripts/hardening-inventory.test.ts b/tests/unit/scripts/hardening-inventory.test.ts new file mode 100644 index 00000000..b5f02281 --- /dev/null +++ b/tests/unit/scripts/hardening-inventory.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from 'bun:test'; + +const { collectSyncCallSites } = require('../../../scripts/hardening-inventory.js'); + +describe('hardening-inventory sync call scanning', () => { + test('ignores sync-call names inside regex literals after else', () => { + const source = [ + 'if (enabled) {', + ' run();', + '} else /fs\\.readFileSync\\(/.test("pattern");', + ].join('\n'); + + const result = collectSyncCallSites(source); + expect(result.count).toBe(0); + }); + + test('ignores sync-call names inside regex literals after do', () => { + const source = 'do /fs\\.writeFileSync\\(/.test("pattern"); while (false);'; + const result = collectSyncCallSites(source); + + expect(result.count).toBe(0); + }); + + test('still counts real sync fs call sites', () => { + const source = [ + 'if (enabled) {', + ' run();', + '} else /fs\\.readFileSync\\(/.test("pattern");', + 'fs.readFileSync("file.txt", "utf8");', + ].join('\n'); + + const result = collectSyncCallSites(source); + expect(result.count).toBe(1); + expect(result.calls).toEqual(['readFileSync']); + }); +});