diff --git a/plugins/llm-security/tests/scanners/ide-extension-scanner.test.mjs b/plugins/llm-security/tests/scanners/ide-extension-scanner.test.mjs index 985043f..7439a56 100644 --- a/plugins/llm-security/tests/scanners/ide-extension-scanner.test.mjs +++ b/plugins/llm-security/tests/scanners/ide-extension-scanner.test.mjs @@ -3,7 +3,7 @@ // Uses fixture trees under tests/fixtures/ide-extensions/ to simulate // real ~/.vscode/extensions/ layouts via rootsOverride injection. -import { describe, it, beforeEach } from 'node:test'; +import { describe, it, before, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -490,3 +490,135 @@ describe('scanOneExtension JetBrains dispatch', () => { } }); }); + +// --------------------------------------------------------------------------- +// JetBrains discovery + scan — full integration over root-jetbrains fixture +// --------------------------------------------------------------------------- + +describe('JetBrains discovery + scan', () => { + const ROOT_JB = resolve(FIXTURES, 'root-jetbrains'); + let env; + let allFindings; + const findingsById = new Map(); + + // Build jars from source/ trees once before the suite. The builder is + // idempotent + race-safe (atomic temp-then-rename, SHA-256 skip-if-match) + // so it is safe to run under node:test's parallel file execution. + before(async () => { + const { buildJetBrainsFixtures } = await import('../helpers/build-jetbrains-fixtures.mjs'); + await buildJetBrainsFixtures({ fixtureRoot: ROOT_JB }); + + resetCounter(); + env = await scan('all', { rootsOverride: [ROOT_JB], intellijOnly: true }); + allFindings = env.extensions.flatMap(e => e.scanner_results.IDE?.findings || []); + for (const ext of env.extensions) { + findingsById.set(ext.id, ext.scanner_results.IDE?.findings || []); + } + }); + + it('discovers >= 8 JetBrains plugins (Fleet excluded)', () => { + assert.ok( + env.meta.extensions_discovered.jetbrains >= 8, + `expected >= 8 JB plugins, got ${env.meta.extensions_discovered.jetbrains}`, + ); + }); + + it('includes every benign and adversarial IntelliJ fixture', () => { + for (const id of [ + 'com.example.benign', + 'com.example.theme-with-code', + 'com.example.broad-activation', + 'com.example.premain', + 'com.example.native-binary', + 'com.example.depends-chain', + 'com.intellij.jaba', + ]) { + assert.ok( + env.extensions.some(e => e.id === id), + `expected extension ${id} in discovery, got: ${env.extensions.map(e => e.id).join(', ')}`, + ); + } + }); + + it('includes the Android Studio fixture (path divergence test)', () => { + assert.ok( + env.extensions.some(e => e.id === 'com.google.example'), + `expected com.google.example under AndroidStudio base, got: ${env.extensions.map(e => e.id).join(', ')}`, + ); + }); + + it('excludes Fleet plugins (different plugin model)', () => { + assert.ok( + env.extensions.every(e => !e.location.includes('Fleet')), + `Fleet plugin leaked in: ${env.extensions.filter(e => e.location.includes('Fleet')).map(e => e.id).join(', ')}`, + ); + }); + + it('all JB extensions routed through JetBrains path (type=jetbrains)', () => { + for (const ext of env.extensions) { + assert.equal(ext.type, 'jetbrains', `${ext.id} had type=${ext.type}`); + } + }); + + it('theme-with-code fixture triggers checkThemeWithCodeJB', () => { + const findings = findingsById.get('com.example.theme-with-code') || []; + assert.ok( + findings.some(f => f.title.includes('checkThemeWithCodeJB')), + `expected theme-with-code finding; got: ${findings.map(f => f.title).join(' | ')}`, + ); + }); + + it('broad-activation fixture triggers checkBroadActivationJB', () => { + const findings = findingsById.get('com.example.broad-activation') || []; + assert.ok( + findings.some(f => f.title.includes('checkBroadActivationJB')), + `expected broad-activation finding; got: ${findings.map(f => f.title).join(' | ')}`, + ); + }); + + it('premain fixture triggers checkPremainClassJB (HIGH)', () => { + const findings = findingsById.get('com.example.premain') || []; + const premain = findings.filter(f => f.title.includes('checkPremainClassJB')); + assert.ok(premain.length >= 1, `expected premain finding; got: ${findings.map(f => f.title).join(' | ')}`); + assert.equal(premain[0].severity, 'high'); + }); + + it('native-binary fixture triggers checkNativeBinariesJB', () => { + const findings = findingsById.get('com.example.native-binary') || []; + assert.ok( + findings.some(f => f.title.includes('checkNativeBinariesJB')), + `expected native-binary finding; got: ${findings.map(f => f.title).join(' | ')}`, + ); + }); + + it('depends-chain fixture triggers checkDependsChainJB', () => { + const findings = findingsById.get('com.example.depends-chain') || []; + assert.ok( + findings.some(f => f.title.includes('checkDependsChainJB')), + `expected depends-chain finding; got: ${findings.map(f => f.title).join(' | ')}`, + ); + }); + + it('typosquat fixture (com.intellij.jaba) triggers checkTyposquatJB', () => { + const findings = findingsById.get('com.intellij.jaba') || []; + assert.ok( + findings.some(f => f.title.includes('checkTyposquatJB')), + `expected typosquat finding; got: ${findings.map(f => f.title).join(' | ')}`, + ); + }); + + it('benign fixture produces no HIGH/CRITICAL JB findings', () => { + const findings = findingsById.get('com.example.benign') || []; + const highs = findings.filter(f => f.severity === 'high' || f.severity === 'critical'); + assert.equal( + highs.length, 0, + `expected benign plugin to be clean; got: ${highs.map(f => f.title).join(' | ')}`, + ); + }); + + it('all JB findings carry DS-IDE- prefix', () => { + for (const f of allFindings) { + assert.ok(f.id.startsWith('DS-IDE-'), `expected DS-IDE- prefix, got ${f.id}`); + } + }); +});