// taint-tracer.test.mjs — JVM-language CODE_EXTENSIONS coverage // // Verifies that the taint-tracer scans Kotlin source files (added in v6.6.0 // to support JetBrains plugin scanning). Creates a tmp fixture with an // env-var → child_process.exec flow in a .kt file and asserts the scanner // produces at least one finding. import { describe, it, before, after } 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 { discoverFiles } from '../../scanners/lib/file-discovery.mjs'; import { scan } from '../../scanners/taint-tracer.mjs'; describe('taint-tracer — Kotlin (.kt) support', () => { let fixtureDir; before(async () => { fixtureDir = await mkdtemp(join(tmpdir(), 'llm-security-kt-taint-')); // Plant a source (process.env-equivalent) → sink (exec) flow in Kotlin. // The taint-tracer is language-agnostic at the regex level — it matches // on identifiers like System.getenv, ProcessBuilder, exec. We mirror the // Node.js process.env pattern to exercise the shared source regex. const ktSource = [ 'class Leak {', ' fun run() {', // Same-line source -> sink: process.env flows directly into exec() // Mirrors the JS pattern detected by Pass 2 (same-line CRITICAL). ' Runtime.getRuntime().exec(process.env["USER_CMD"])', // Variable-propagation case using generic source label user_input ' val tainted = user_input', ' child_process.exec(tainted)', ' }', '}', '' ].join('\n'); await writeFile(join(fixtureDir, 'Leak.kt'), ktSource, 'utf8'); }); after(async () => { if (fixtureDir) await rm(fixtureDir, { recursive: true, force: true }); }); it('discovers the .kt file', async () => { const discovery = await discoverFiles(fixtureDir); const ktFiles = discovery.files.filter(f => f.ext === '.kt'); assert.ok(ktFiles.length >= 1, `Expected ≥1 .kt file discovered, got ${ktFiles.length}`); }); it('returns status ok on Kotlin input', async () => { resetCounter(); const discovery = await discoverFiles(fixtureDir); const result = await scan(fixtureDir, discovery); assert.equal(result.status, 'ok', `Expected status 'ok', got '${result.status}'`); }); it('scans the .kt file (files_scanned >= 1)', async () => { resetCounter(); const discovery = await discoverFiles(fixtureDir); const result = await scan(fixtureDir, discovery); assert.ok( result.files_scanned >= 1, `Expected files_scanned >= 1 (Kotlin file should be included), got ${result.files_scanned}` ); }); it('detects at least one taint flow in Kotlin source', async () => { resetCounter(); const discovery = await discoverFiles(fixtureDir); const result = await scan(fixtureDir, discovery); assert.ok( result.findings.length >= 1, `Expected >= 1 taint finding in Kotlin source, got ${result.findings.length}. ` + `Findings: ${result.findings.map(f => f.title).join('; ')}` ); }); });