diff --git a/plugins/llm-security/scanners/taint-tracer.mjs b/plugins/llm-security/scanners/taint-tracer.mjs index 02540c9..df33001 100644 --- a/plugins/llm-security/scanners/taint-tracer.mjs +++ b/plugins/llm-security/scanners/taint-tracer.mjs @@ -22,6 +22,8 @@ import { SEVERITY } from './lib/severity.mjs'; // --------------------------------------------------------------------------- // File extension filter — only scan code files, not config/docs +// JVM-language support (.kt, .kts, .groovy, .gradle, .scala) is required for +// JetBrains plugin scanning — plugin source lives in these languages. // --------------------------------------------------------------------------- const CODE_EXTENSIONS = new Set([ @@ -32,6 +34,9 @@ const CODE_EXTENSIONS = new Set([ '.rb', '.php', '.go', '.rs', '.java', '.cs', + '.kt', '.kts', + '.groovy', '.gradle', + '.scala', '.sh', '.bash', '.zsh', ]); diff --git a/plugins/llm-security/tests/scanners/taint-tracer.test.mjs b/plugins/llm-security/tests/scanners/taint-tracer.test.mjs new file mode 100644 index 0000000..861a39d --- /dev/null +++ b/plugins/llm-security/tests/scanners/taint-tracer.test.mjs @@ -0,0 +1,79 @@ +// 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('; ')}` + ); + }); +});