// ----------------------------------------------------------------------------- // sink-detect.test.js // Sprint: SPRINT_3600_0004_0001 (Node.js Babel Integration) // Tasks: NODE-019 - Unit tests for sink detection (all categories) // Description: Tests for security sink detection patterns. // ----------------------------------------------------------------------------- import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; import { buildSinkLookup, matchSink, sinkPatterns, isTaintSource } from './sink-detect.js'; describe('buildSinkLookup', () => { test('builds lookup map with all patterns', () => { const lookup = buildSinkLookup(); assert.ok(lookup instanceof Map); assert.ok(lookup.size > 0); }); test('includes command injection sinks', () => { const lookup = buildSinkLookup(); assert.ok(lookup.has('child_process:exec')); assert.ok(lookup.has('child_process:spawn')); assert.ok(lookup.has('child_process:execSync')); }); test('includes SQL injection sinks', () => { const lookup = buildSinkLookup(); assert.ok(lookup.has('connection.query')); assert.ok(lookup.has('mysql:query')); assert.ok(lookup.has('pg:query')); assert.ok(lookup.has('knex:raw')); }); test('includes file write sinks', () => { const lookup = buildSinkLookup(); assert.ok(lookup.has('fs:writeFile')); assert.ok(lookup.has('fs:writeFileSync')); assert.ok(lookup.has('fs:appendFile')); }); test('includes deserialization sinks', () => { const lookup = buildSinkLookup(); assert.ok(lookup.has('global:eval')); assert.ok(lookup.has('global:Function')); assert.ok(lookup.has('vm:runInContext')); }); test('includes SSRF sinks', () => { const lookup = buildSinkLookup(); assert.ok(lookup.has('http:request')); assert.ok(lookup.has('https:get')); assert.ok(lookup.has('axios:get')); assert.ok(lookup.has('global:fetch')); }); test('includes NoSQL injection sinks', () => { const lookup = buildSinkLookup(); assert.ok(lookup.has('mongodb:find')); assert.ok(lookup.has('mongoose:findOne')); assert.ok(lookup.has('mongodb:aggregate')); }); }); describe('matchSink', () => { const lookup = buildSinkLookup(); test('detects command injection via child_process.exec', () => { const result = matchSink('child_process', 'exec', lookup); assert.ok(result); assert.equal(result.category, 'command_injection'); assert.equal(result.method, 'exec'); }); test('detects command injection via child_process.spawn', () => { const result = matchSink('child_process', 'spawn', lookup); assert.ok(result); assert.equal(result.category, 'command_injection'); }); test('detects SQL injection via connection.query', () => { const result = matchSink('connection', 'query', lookup); assert.ok(result); assert.equal(result.category, 'sql_injection'); }); test('detects SQL injection via knex.raw', () => { const result = matchSink('knex', 'raw', lookup); assert.ok(result); assert.equal(result.category, 'sql_injection'); }); test('detects SQL injection via prisma.$queryRaw', () => { const result = matchSink('prisma', '$queryRaw', lookup); assert.ok(result); assert.equal(result.category, 'sql_injection'); }); test('detects file write via fs.writeFile', () => { const result = matchSink('fs', 'writeFile', lookup); assert.ok(result); // fs.writeFile is categorized in both file_write and path_traversal // The lookup returns path_traversal since it's processed later assert.ok(['file_write', 'path_traversal'].includes(result.category)); }); test('detects deserialization via eval', () => { const result = matchSink('eval', 'eval', lookup); assert.ok(result); assert.equal(result.category, 'deserialization'); }); test('detects SSRF via axios.get', () => { const result = matchSink('axios', 'get', lookup); assert.ok(result); assert.equal(result.category, 'ssrf'); }); test('detects SSRF via fetch', () => { const result = matchSink('fetch', 'fetch', lookup); assert.ok(result); assert.equal(result.category, 'ssrf'); }); test('detects NoSQL injection via mongoose.find', () => { const result = matchSink('mongoose', 'find', lookup); assert.ok(result); assert.equal(result.category, 'nosql_injection'); }); test('detects weak crypto via crypto.createCipher', () => { const result = matchSink('crypto', 'createCipher', lookup); assert.ok(result); assert.equal(result.category, 'weak_crypto'); }); test('detects LDAP injection via ldapjs.search', () => { const result = matchSink('ldapjs', 'search', lookup); assert.ok(result); assert.equal(result.category, 'ldap_injection'); }); test('returns null for non-sink methods', () => { const result = matchSink('console', 'clear', lookup); assert.equal(result, null); }); test('returns null for unknown objects', () => { const result = matchSink('myCustomModule', 'doSomething', lookup); assert.equal(result, null); }); }); describe('sinkPatterns', () => { test('has expected categories', () => { const categories = Object.keys(sinkPatterns); assert.ok(categories.includes('command_injection')); assert.ok(categories.includes('sql_injection')); assert.ok(categories.includes('file_write')); assert.ok(categories.includes('deserialization')); assert.ok(categories.includes('ssrf')); assert.ok(categories.includes('nosql_injection')); assert.ok(categories.includes('xss')); assert.ok(categories.includes('log_injection')); }); test('command_injection has child_process patterns', () => { const cmdPatterns = sinkPatterns.command_injection.patterns; const childProcessPattern = cmdPatterns.find(p => p.module === 'child_process'); assert.ok(childProcessPattern); assert.ok(childProcessPattern.methods.includes('exec')); assert.ok(childProcessPattern.methods.includes('spawn')); assert.ok(childProcessPattern.methods.includes('fork')); }); test('sql_injection covers major ORMs', () => { const sqlPatterns = sinkPatterns.sql_injection.patterns; const modules = sqlPatterns.map(p => p.module).filter(Boolean); assert.ok(modules.includes('mysql')); assert.ok(modules.includes('pg')); assert.ok(modules.includes('knex')); assert.ok(modules.includes('sequelize')); assert.ok(modules.includes('prisma')); }); test('ssrf covers HTTP clients', () => { const ssrfPatterns = sinkPatterns.ssrf.patterns; const modules = ssrfPatterns.map(p => p.module).filter(Boolean); assert.ok(modules.includes('http')); assert.ok(modules.includes('https')); assert.ok(modules.includes('axios')); assert.ok(modules.includes('got')); }); }); describe('isTaintSource', () => { test('detects req.body as taint source', () => { assert.ok(isTaintSource('req.body')); assert.ok(isTaintSource('req.body.username')); }); test('detects req.query as taint source', () => { assert.ok(isTaintSource('req.query')); assert.ok(isTaintSource('req.query.id')); }); test('detects req.params as taint source', () => { assert.ok(isTaintSource('req.params')); assert.ok(isTaintSource('req.params.userId')); }); test('detects req.headers as taint source', () => { assert.ok(isTaintSource('req.headers')); assert.ok(isTaintSource('req.headers.authorization')); }); test('detects event.body (Lambda) as taint source', () => { assert.ok(isTaintSource('event.body')); assert.ok(isTaintSource('event.queryStringParameters')); }); test('detects ctx.request.body (Koa) as taint source', () => { assert.ok(isTaintSource('ctx.request.body')); assert.ok(isTaintSource('ctx.params')); }); test('detects process.env as taint source', () => { assert.ok(isTaintSource('process.env')); assert.ok(isTaintSource('process.env.SECRET')); }); test('does not flag safe identifiers', () => { assert.ok(!isTaintSource('myLocalVariable')); assert.ok(!isTaintSource('config.port')); assert.ok(!isTaintSource('user.name')); }); });