|
| 1 | +// @ts-check |
| 2 | +import test from 'node:test'; |
| 3 | +import assert from 'node:assert/strict'; |
| 4 | +import fs from 'node:fs'; |
| 5 | +import os from 'node:os'; |
| 6 | +import path from 'node:path'; |
| 7 | +import { detectVulnerabilities } from '../src/detector.js'; |
| 8 | + |
| 9 | +// Calibration guards driven by real user feedback: the internal detector was |
| 10 | +// "shouting fire because it saw the word match in the dictionary" — WEAK_CIPHER |
| 11 | +// matched the "des" substring inside includes/excludes/modes, IDs collided |
| 12 | +// (WEAK_CIPHER_0 repeated), and test fixtures cratered the score. |
| 13 | + |
| 14 | +function scan(files) { |
| 15 | + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'csreview-calib-')); |
| 16 | + for (const f of files) { |
| 17 | + const abs = path.join(root, f.path); |
| 18 | + fs.mkdirSync(path.dirname(abs), { recursive: true }); |
| 19 | + fs.writeFileSync(abs, f.code, 'utf8'); |
| 20 | + } |
| 21 | + return detectVulnerabilities({ |
| 22 | + root, |
| 23 | + files: files.map((f) => ({ path: f.path, language: f.language || 'javascript', kind: f.kind || 'source' })), |
| 24 | + }); |
| 25 | +} |
| 26 | + |
| 27 | +test('WEAK_CIPHER does not fire on the "des" substring in includes/excludes/modes', () => { |
| 28 | + const findings = scan([ |
| 29 | + { path: 'src/a.js', code: "export const f = (args) => args.includes('--no-update-check');\n" }, |
| 30 | + { path: 'src/b.js', code: "export const ok = (ids) => ids.includes('postgres');\n" }, |
| 31 | + { path: 'src/c.js', code: 'export const modes = (x) => x.excludes && x.decodes;\n' }, |
| 32 | + { path: 'src/d.js', code: 'export const nodes = ["a"];\nexport const provides = true;\n' }, |
| 33 | + ]); |
| 34 | + const wc = findings.filter((f) => String(f.id).startsWith('WEAK_CIPHER')); |
| 35 | + assert.equal(wc.length, 0, `unexpected WEAK_CIPHER FPs: ${wc.map((f) => `${f.file}:${f.line}`).join(', ')}`); |
| 36 | +}); |
| 37 | + |
| 38 | +test('WEAK_CIPHER still detects real weak-cipher usage (recall preserved)', () => { |
| 39 | + const findings = scan([ |
| 40 | + { |
| 41 | + path: 'src/n.js', |
| 42 | + code: 'import crypto from "crypto";\nexport const e = (k, iv) => crypto.createCipheriv("des-ede3-cbc", k, iv);\n', |
| 43 | + }, |
| 44 | + { |
| 45 | + path: 'src/cjs.js', |
| 46 | + code: 'import CryptoJS from "crypto-js";\nexport const e = (d) => CryptoJS.DES.encrypt(d, "key");\n', |
| 47 | + }, |
| 48 | + { |
| 49 | + path: 'src/ecb.js', |
| 50 | + code: 'import crypto from "crypto";\nexport const e = (k, iv) => crypto.createCipheriv("aes-128-ecb", k, iv);\n', |
| 51 | + }, |
| 52 | + { |
| 53 | + path: 'src/J.java', |
| 54 | + language: 'java', |
| 55 | + code: 'class A { void f() throws Exception { Cipher.getInstance("DES"); } }\n', |
| 56 | + }, |
| 57 | + ]); |
| 58 | + const wc = findings.filter((f) => f.cwe === 'CWE-327'); |
| 59 | + assert.ok(wc.length >= 4, `expected >= 4 weak-cipher detections, got ${wc.length}`); |
| 60 | +}); |
| 61 | + |
| 62 | +test('detector assigns globally-unique finding IDs across files (no WEAK_CIPHER_0 collision)', () => { |
| 63 | + const findings = scan([ |
| 64 | + { |
| 65 | + path: 'src/a.js', |
| 66 | + code: 'import crypto from "crypto";\nexport const e = (k, iv) => crypto.createCipheriv("rc4", k, iv);\n', |
| 67 | + }, |
| 68 | + { |
| 69 | + path: 'src/b.js', |
| 70 | + code: 'import crypto from "crypto";\nexport const e = (k, iv) => crypto.createCipheriv("rc4", k, iv);\n', |
| 71 | + }, |
| 72 | + ]); |
| 73 | + const ids = findings.map((f) => f.id); |
| 74 | + assert.equal(new Set(ids).size, ids.length, `duplicate IDs present: ${ids.join(', ')}`); |
| 75 | +}); |
| 76 | + |
| 77 | +test('findings in non-source paths (test/fixtures) are downgraded; real source keeps its severity', () => { |
| 78 | + const srcFindings = scan([{ path: 'src/x.js', code: 'export const key = "AKIAIOSFODNN7EXAMPLE";\n' }]); |
| 79 | + const testFindings = scan([{ path: 'test/x.test.js', code: 'export const key = "AKIAIOSFODNN7EXAMPLE";\n' }]); |
| 80 | + const inSrc = srcFindings.find((f) => String(f.id).startsWith('AWS_ACCESS_KEY')); |
| 81 | + const inTest = testFindings.find((f) => String(f.id).startsWith('AWS_ACCESS_KEY')); |
| 82 | + assert.ok(inSrc && inTest, 'AWS key detected in both src and test'); |
| 83 | + assert.equal(inSrc.severity, 'CRITICAL', 'real source finding keeps full severity'); |
| 84 | + assert.equal(inTest.severity, 'LOW', 'test/fixture finding is downgraded to LOW'); |
| 85 | + assert.equal(inTest.confidence, 'LOW'); |
| 86 | +}); |
0 commit comments