// dep-token-overlap.test.mjs — B7 (v7.2.0) — typosquat token-overlap integration // // Verifies that dep-auditor's checkTyposquatting now flags packages with // edit distance >= 3 that contain all tokens of a popular package plus a // suspicious suffix (e.g. `lodash-utils` vs `lodash`). // // Builds an in-memory fixture in tmpdir per test instead of mutating the // shared `tests/fixtures/dep-test/` fixture (which other tests assert // exact finding counts against). import { describe, it, before, after, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtemp, writeFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { resetCounter } from '../../scanners/lib/output.mjs'; import { scan } from '../../scanners/dep-auditor.mjs'; async function makeFixture(deps) { const dir = await mkdtemp(join(tmpdir(), 'llm-security-dep-token-')); await writeFile( join(dir, 'package.json'), JSON.stringify({ name: 'token-overlap-fixture', version: '1.0.0', dependencies: deps }, null, 2), 'utf8', ); return dir; } describe('dep-auditor B7 — token-overlap typosquat heuristic', () => { beforeEach(() => { resetCounter(); }); it('flags lodash-utils as token-overlap typosquat of lodash', async () => { const dir = await makeFixture({ 'lodash-utils': '^1.0.0' }); try { const result = await scan(dir, { files: [] }); const finding = result.findings.find( f => f.title.toLowerCase().includes('token-overlap') && f.title.includes('lodash-utils') && f.title.includes('lodash') ); assert.ok( finding, `expected token-overlap finding for lodash-utils. Got: ${result.findings.map(f => f.title).join('; ')}`, ); assert.equal(finding.severity, 'medium'); } finally { await rm(dir, { recursive: true, force: true }); } }); it('flags react-helper as token-overlap typosquat of react', async () => { const dir = await makeFixture({ 'react-helper': '^1.0.0' }); try { const result = await scan(dir, { files: [] }); const finding = result.findings.find( f => f.title.toLowerCase().includes('token-overlap') && f.title.includes('react-helper') ); assert.ok(finding, `expected react-helper to be flagged. Got: ${result.findings.map(f => f.title).join('; ')}`); } finally { await rm(dir, { recursive: true, force: true }); } }); it('flags express-wrapper as token-overlap typosquat of express', async () => { const dir = await makeFixture({ 'express-wrapper': '^1.0.0' }); try { const result = await scan(dir, { files: [] }); const finding = result.findings.find( f => f.title.toLowerCase().includes('token-overlap') && f.title.includes('express-wrapper') ); assert.ok(finding, `expected express-wrapper to be flagged. Got: ${result.findings.map(f => f.title).join('; ')}`); } finally { await rm(dir, { recursive: true, force: true }); } }); it('does NOT flag legitimate package lodash (exact match)', async () => { const dir = await makeFixture({ 'lodash': '^4.17.0' }); try { const result = await scan(dir, { files: [] }); const typosquatFindings = result.findings.filter( f => f.title.toLowerCase().includes('typosquat') ); assert.equal(typosquatFindings.length, 0, `lodash must not be flagged. Got: ${typosquatFindings.map(f => f.title).join('; ')}`); } finally { await rm(dir, { recursive: true, force: true }); } }); it('does NOT flag legitimate short-name tools from allowlist', async () => { // These are in knowledge/typosquat-allowlist.json (22 npm + 5 PyPI from v7.0.0). // The allowlist check fires BEFORE Levenshtein and BEFORE token-overlap. const dir = await makeFixture({ 'knip': '^5.0.0', 'tsx': '^4.0.0', 'nx': '^17.0.0', 'rimraf': '^5.0.0', }); try { const result = await scan(dir, { files: [] }); const typosquatFindings = result.findings.filter( f => f.title.toLowerCase().includes('typosquat') ); assert.equal( typosquatFindings.length, 0, `allowlisted short-name tools must not be flagged. Got: ${typosquatFindings.map(f => f.title).join('; ')}`, ); } finally { await rm(dir, { recursive: true, force: true }); } }); it('does NOT flag packages with no suspicious suffix even with token overlap', async () => { // `react-router-dom` contains `react` token + `router` + `dom` — but neither // `router` nor `dom` is in TYPOSQUAT_SUSPICIOUS_TOKENS. This is a legitimate // ecosystem name and must pass. const dir = await makeFixture({ 'react-router-dom': '^6.0.0' }); try { const result = await scan(dir, { files: [] }); const tokenOverlapFindings = result.findings.filter( f => f.title.toLowerCase().includes('token-overlap') ); assert.equal( tokenOverlapFindings.length, 0, `react-router-dom must not be flagged as token-overlap typosquat`, ); } finally { await rm(dir, { recursive: true, force: true }); } }); it('does NOT flag packages whose all tokens match a popular package (subset, not superset)', async () => { // `react` itself has tokens {react} which equal the popular `react` token set. // The heuristic requires declared ⊃ popular (strict superset), not equal. const dir = await makeFixture({ 'react': '^18.0.0' }); try { const result = await scan(dir, { files: [] }); const tokenOverlapFindings = result.findings.filter( f => f.title.toLowerCase().includes('token-overlap') ); assert.equal(tokenOverlapFindings.length, 0); } finally { await rm(dir, { recursive: true, force: true }); } }); });