// unicode-bom.test.mjs — Regression tests for the leading-BOM exception in // unicode-scanner (v7.8.3, #42). // // A legitimate UTF-8 BOM (U+FEFF at file position 0) was flagged as a HIGH // zero-width finding — a finding the tool's own auto-cleaner refuses to strip // (auto-cleaner.mjs preserves cp === 0xFEFF at line 0, pos 0). The scanner // must apply the same file-start exception. A BOM anywhere else in the file // is still a zero-width smuggling vector and must be flagged. import { describe, it, beforeEach, after } from 'node:test'; import assert from 'node:assert/strict'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { mkdtemp, writeFile, rm } from 'node:fs/promises'; import { resetCounter } from '../../scanners/lib/output.mjs'; import { discoverFiles } from '../../scanners/lib/file-discovery.mjs'; import { scan } from '../../scanners/unicode-scanner.mjs'; const created = []; /** Make a scan root containing a single file with the given content. */ async function makeRepo(content, fileName = 'app.mjs') { const root = await mkdtemp(join(tmpdir(), 'llmsec-bom-')); created.push(root); await writeFile(join(root, fileName), content, 'utf8'); return root; } async function scanRepo(root) { resetCounter(); const discovery = await discoverFiles(root); return scan(root, discovery); } function zeroWidthFindings(result) { return result.findings.filter(f => f.title.toLowerCase().includes('zero-width') || (f.evidence && f.evidence.toUpperCase().includes('U+FEFF')) ); } after(async () => { for (const dir of created) await rm(dir, { recursive: true, force: true }); }); describe('unicode-scanner — leading UTF-8 BOM exception (v7.8.3, #42)', () => { beforeEach(() => { resetCounter(); }); it('does not flag a legitimate BOM at file position 0', async () => { const root = await makeRepo('\uFEFFconst x = 1;\nexport default x;\n'); const result = await scanRepo(root); assert.equal(result.status, 'ok'); const hits = zeroWidthFindings(result); assert.equal( hits.length, 0, `leading BOM must not be flagged, got: ${hits.map(f => f.title).join('; ')}` ); }); it('still flags a BOM at the start of a later line', async () => { const root = await makeRepo('const x = 1;\n\uFEFFconst y = 2;\n'); const result = await scanRepo(root); assert.equal(result.status, 'ok'); const hits = zeroWidthFindings(result); assert.ok( hits.length >= 1, `mid-file BOM must be flagged, got ${hits.length} findings` ); assert.equal(hits[0].line, 2); }); it('still flags a BOM mid-line on the first line', async () => { const root = await makeRepo('const x\uFEFF = 1;\n'); const result = await scanRepo(root); assert.equal(result.status, 'ok'); const hits = zeroWidthFindings(result); assert.ok( hits.length >= 1, `first-line non-leading BOM must be flagged, got ${hits.length} findings` ); }); it('still flags other zero-width chars on the first line after a BOM', async () => { const root = await makeRepo('\uFEFFconst x\u200B = 1;\n'); const result = await scanRepo(root); assert.equal(result.status, 'ok'); const hits = result.findings.filter(f => f.evidence && f.evidence.toUpperCase().includes('U+200B') ); assert.ok( hits.length >= 1, `U+200B after a leading BOM must still be flagged, got ${hits.length} findings` ); }); });