From aa269ed6d8274eb5ae3d86c58f1afe89b0f3464e Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sat, 18 Apr 2026 10:35:46 +0200 Subject: [PATCH] feat(llm-security): wire JetBrains branch into scanOneExtension --- .../scanners/ide-extension-scanner.mjs | 58 +++++++++++----- .../scanners/ide-extension-scanner.test.mjs | 67 +++++++++++++++++++ 2 files changed, 109 insertions(+), 16 deletions(-) diff --git a/plugins/llm-security/scanners/ide-extension-scanner.mjs b/plugins/llm-security/scanners/ide-extension-scanner.mjs index 537b807..af678ea 100644 --- a/plugins/llm-security/scanners/ide-extension-scanner.mjs +++ b/plugins/llm-security/scanners/ide-extension-scanner.mjs @@ -25,8 +25,14 @@ import { discoverVSCodeExtensions, discoverJetBrainsExtensions, } from './lib/ide-extension-discovery.mjs'; -import { parseVSCodeExtension, parseVsixFile } from './lib/ide-extension-parser.mjs'; -import { loadTopVSCode, loadVSCodeBlocklist, normalizeId } from './lib/ide-extension-data.mjs'; +import { parseVSCodeExtension, parseVsixFile, parseIntelliJPlugin } from './lib/ide-extension-parser.mjs'; +import { + loadTopVSCode, + loadVSCodeBlocklist, + loadTopJetBrains, + loadJetBrainsBlocklist, + normalizeId, +} from './lib/ide-extension-data.mjs'; import { fetchVsixFromUrl, detectUrlType } from './lib/vsix-fetch.mjs'; import { extractToDir, ZipError } from './lib/zip-extract.mjs'; import { runVsixWorker } from './lib/vsix-sandbox.mjs'; @@ -516,8 +522,10 @@ async function scanOneExtension(ext, options) { const started = Date.now(); const warnings = []; - // Parse manifest - const parsed = await parseVSCodeExtension(ext.location); + // Parse manifest — dispatch on extension type + const parsed = ext.type === 'jetbrains' + ? await parseIntelliJPlugin(ext.location) + : await parseVSCodeExtension(ext.location); if (!parsed) { return { id: ext.id, @@ -537,28 +545,45 @@ async function scanOneExtension(ext, options) { const manifest = parsed.manifest; warnings.push(...parsed.warnings); - const topList = await loadTopVSCode(); - const blocklist = await loadVSCodeBlocklist(); + const isJetBrains = ext.type === 'jetbrains'; + const topList = isJetBrains ? [] : await loadTopVSCode(); + const blocklist = isJetBrains ? [] : await loadVSCodeBlocklist(); + const topListJB = isJetBrains ? await loadTopJetBrains() : []; + const blocklistJB = isJetBrains ? await loadJetBrainsBlocklist() : []; const relLocation = relative(options.targetBase || ext.location, ext.location) || '.'; // Discover files (Pass A) — excludes node_modules, used for ENT/NET/TNT/UNI const discovery = await discoverFiles(ext.location).catch(() => ({ files: [], skipped: 0, truncated: false })); - // Pass B for MEM — filter to README/CHANGELOG/package.json only + // Pass B for MEM — filter to README/CHANGELOG/package.json only (VS Code), + // plus plugin.xml and META-INF/MANIFEST.MF for JetBrains plugins. const memFiles = discovery.files.filter(f => { const lower = (f.relPath || '').toLowerCase(); - return lower === 'readme.md' || lower === 'changelog.md' || lower === 'package.json'; + if (lower === 'readme.md' || lower === 'changelog.md' || lower === 'package.json') return true; + if (isJetBrains) { + if (lower === 'plugin.xml' || lower.endsWith('/plugin.xml')) return true; + if (lower === 'meta-inf/manifest.mf' || lower.endsWith('/meta-inf/manifest.mf')) return true; + } + return false; }); - // IDE-specific findings - const ideFindings = runIdeChecks( - { ...ext, signed: manifest.hasSignature || ext.signed }, - manifest, - topList, - blocklist, - relLocation, - ); + // IDE-specific findings — dispatch on extension type + const ideFindings = isJetBrains + ? runJetBrainsChecks( + { ...ext, signed: manifest.hasSignature || ext.signed }, + manifest, + topListJB, + blocklistJB, + relLocation, + ) + : runIdeChecks( + { ...ext, signed: manifest.hasSignature || ext.signed }, + manifest, + topList, + blocklist, + relLocation, + ); const ideResult = scannerResult(SCANNER, 'ok', ideFindings, 1, Date.now() - started); // Run reused scanners (each is independent; run sequentially to avoid burst-rate issues) @@ -805,6 +830,7 @@ export const __testing = { checkPremainClassJB, checkNativeBinariesJB, checkShadedJarsJB, + scanOneExtension, }; /** 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 04096ec..985043f 100644 --- a/plugins/llm-security/tests/scanners/ide-extension-scanner.test.mjs +++ b/plugins/llm-security/tests/scanners/ide-extension-scanner.test.mjs @@ -423,3 +423,70 @@ describe('blocklist matching', () => { assert.equal(crit.length, 0); }); }); + +describe('scanOneExtension JetBrains dispatch', () => { + // Fixture-free verification: synthesize an ExtensionRecord with type:'jetbrains' + // and a minimal plugin dir (lib/ with META-INF/plugin.xml) at runtime via + // tests/helpers/zip-writer.mjs. Does not depend on Step 13/14 fixtures. + it('dispatches JetBrains records to parseIntelliJPlugin + runJetBrainsChecks', async () => { + const { mkdtemp, mkdir, writeFile, rm } = await import('node:fs/promises'); + const { join } = await import('node:path'); + const { tmpdir } = await import('node:os'); + const { createZip } = await import('../helpers/zip-writer.mjs'); + + const pluginRoot = await mkdtemp(join(tmpdir(), 'llmsec-jb-disp-')); + try { + const libDir = join(pluginRoot, 'lib'); + await mkdir(libDir, { recursive: true }); + const pluginXml = ` + com.example.dispatch + Dispatch Test + 1.0.0 + Example + +`; + const jarBuf = createZip([ + { name: 'META-INF/plugin.xml', data: pluginXml }, + ]); + await writeFile(join(libDir, 'main.jar'), jarBuf); + + const ext = { + id: 'com.example.dispatch', + version: '1.0.0', + type: 'jetbrains', + location: pluginRoot, + publisher: 'Example', + source: 'installed', + isBuiltin: false, + signed: false, + }; + + const result = await scannerInternals.scanOneExtension(ext, { targetBase: pluginRoot }); + assert.equal(result.id, 'com.example.dispatch'); + assert.equal(result.type, 'jetbrains'); + // IDE result must exist — dispatch produced findings (possibly empty array, but scanner ran) + assert.ok(result.scanner_results.IDE, 'IDE scanner did not run'); + assert.equal(result.scanner_results.IDE.status, 'ok'); + // Manifest was parsed via parseIntelliJPlugin (not parseVSCodeExtension) — + // the scanner wouldn't have a successful IDE result if it had tried to read + // package.json from a dir that only has lib/main.jar. + assert.ok(!result.warnings.some(w => String(w).includes('failed to parse manifest')), + `parse failed: ${result.warnings.join('; ')}`); + } finally { + await rm(pluginRoot, { recursive: true, force: true }); + } + }); + + it('VS Code records still route through parseVSCodeExtension (regression guard)', async () => { + const env = await scan('all', { rootsOverride: [ROOT_BENIGN], vscodeOnly: true }); + // Two VS Code extensions parsed successfully from the existing benign fixture. + assert.equal(env.meta.extensions_discovered.vscode, 2); + // None of them should be tagged as jetbrains. + assert.ok(env.extensions.every(e => e.type !== 'jetbrains')); + // Each has a functioning IDE scanner result (VS Code path intact). + for (const ext of env.extensions) { + assert.ok(ext.scanner_results.IDE, `missing IDE result for ${ext.id}`); + assert.equal(ext.scanner_results.IDE.status, 'ok'); + } + }); +});