diff --git a/plugins/llm-security/knowledge/top-jetbrains-plugins.json b/plugins/llm-security/knowledge/top-jetbrains-plugins.json index 09dec42..f62ce5a 100644 --- a/plugins/llm-security/knowledge/top-jetbrains-plugins.json +++ b/plugins/llm-security/knowledge/top-jetbrains-plugins.json @@ -1,10 +1,68 @@ { "_meta": { - "source": "Stub for v1.1 — IntelliJ discovery deferred. See research brief §2, §4.", - "count": 0, + "source": "Curated from JetBrains Marketplace + bundled IDE plugins per research brief §3 (2026-04-17). See docs/plans/jetbrains-research-brief.md.", + "count": 56, "last_updated": "2026-04-17", - "purpose": "Typosquat detection seed for JetBrains plugins. To be populated in v1.1." + "purpose": "Typosquat detection seed for JetBrains plugins. Canonical xmlIds — Levenshtein <= 2 against these flags suspicious lookalikes.", + "blocklist_note": "Empty by design — no public confirmed-malicious JetBrains Marketplace plugins as of 2026-04-17. Enterprise policy.json can seed private entries with form {id, version_range, reason, source}." }, - "jetbrains": [], + "jetbrains": [ + "com.intellij.java", + "com.intellij.java-i18n", + "com.intellij.copyright", + "com.intellij.properties", + "com.intellij.platform.images", + "com.intellij.tasks", + "com.intellij.terminal", + "com.intellij.markdown", + "com.intellij.gradle", + "com.intellij.groovy", + "com.intellij.maven", + "com.intellij.database", + "com.intellij.clouds.kubernetes", + "com.intellij.clouds.docker", + "com.intellij.spring", + "com.intellij.javaee", + "com.intellij.javaee.web", + "com.intellij.javaee.app.servers.integration", + "com.intellij.settingsSync", + "com.intellij.plugins.watcher", + "org.jetbrains.plugins.yaml", + "org.jetbrains.plugins.gradle", + "org.jetbrains.plugins.github", + "org.jetbrains.idea.eclipse", + "org.jetbrains.plugins.vue", + "org.jetbrains.plugins.node", + "org.jetbrains.plugins.javaFX", + "org.jetbrains.plugins.gitlab", + "org.jetbrains.plugins.textmate", + "org.jetbrains.kotlin", + "org.jetbrains.plugins.ruby", + "org.jetbrains.idea.maven", + "org.jetbrains.plugins.terminal", + "com.jetbrains.php", + "com.jetbrains.python", + "com.jetbrains.python.community", + "com.jetbrains.space", + "com.jetbrains.restClient", + "com.jetbrains.rust", + "org.intellij.scala", + "org.rust.lang", + "com.github.copilot", + "com.sonarlint.idea", + "mobi.hsz.idea.gitignore", + "Lombook Plugin", + "com.google.idea.bazel.ijwb", + "org.asciidoctor.intellij.asciidoc", + "org.toml.lang", + "String Manipulation", + "Key Promoter X", + "Rainbow Brackets", + "com.chrisrm.idea.MaterialThemeUI", + "com.markskelton.one-dark-theme", + "AceJump", + "CodeGlance", + "PlantUML integration" + ], "blocklist": [] } diff --git a/plugins/llm-security/scanners/lib/ide-extension-data.mjs b/plugins/llm-security/scanners/lib/ide-extension-data.mjs index 0e89ab5..bb262fe 100644 --- a/plugins/llm-security/scanners/lib/ide-extension-data.mjs +++ b/plugins/llm-security/scanners/lib/ide-extension-data.mjs @@ -42,15 +42,27 @@ export async function loadVSCodeBlocklist() { } /** - * Load top JetBrains plugin IDs (stub for v1.1). - * @returns {Promise} + * Load top JetBrains plugin xmlIds (canonical corpus for typosquat detection). + * @returns {Promise} Lowercased xmlIds. */ export async function loadTopJetBrains() { - if (_jetbrains !== null) return _jetbrains.jetbrains || []; + if (_jetbrains !== null) return (_jetbrains.jetbrains || []).map(normalizeId); _jetbrains = await loadJson(join(KNOWLEDGE_DIR, 'top-jetbrains-plugins.json')) || { jetbrains: [], blocklist: [] }; return (_jetbrains.jetbrains || []).map(normalizeId); } +/** + * Load JetBrains plugin blocklist entries. + * Empty by design — no public confirmed-malicious JetBrains Marketplace plugins + * as of 2026-04-17. Enterprise policy.json can seed private entries. + * @returns {Promise} Entries of form "xmlId@version" or "xmlId@*". + */ +export async function loadJetBrainsBlocklist() { + if (_jetbrains !== null) return _jetbrains.blocklist || []; + _jetbrains = await loadJson(join(KNOWLEDGE_DIR, 'top-jetbrains-plugins.json')) || { jetbrains: [], blocklist: [] }; + return _jetbrains.blocklist || []; +} + /** * Normalize extension ID for comparison. * @param {string} id diff --git a/plugins/llm-security/tests/scanners/ide-extension-data.test.mjs b/plugins/llm-security/tests/scanners/ide-extension-data.test.mjs new file mode 100644 index 0000000..8c8f48c --- /dev/null +++ b/plugins/llm-security/tests/scanners/ide-extension-data.test.mjs @@ -0,0 +1,100 @@ +// ide-extension-data.test.mjs — Unit tests for knowledge-file loaders. +// +// Verifies loadTopJetBrains, loadJetBrainsBlocklist behavior + cache +// discipline shared with VS Code loaders. + +import { describe, it, beforeEach } from 'node:test'; +import assert from 'node:assert/strict'; +import { + loadTopJetBrains, + loadJetBrainsBlocklist, + loadTopVSCode, + loadVSCodeBlocklist, + normalizeId, + _resetCache, +} from '../../scanners/lib/ide-extension-data.mjs'; + +describe('loadTopJetBrains', () => { + beforeEach(() => _resetCache()); + + it('returns >= 40 canonical xmlIds', async () => { + const ids = await loadTopJetBrains(); + assert.ok(Array.isArray(ids)); + assert.ok( + ids.length >= 40, + `expected >= 40 entries, got ${ids.length}`, + ); + }); + + it('returns lowercased trimmed entries (normalizeId applied)', async () => { + const ids = await loadTopJetBrains(); + for (const id of ids) { + assert.equal(id, id.toLowerCase(), `not lowercased: ${id}`); + assert.equal(id, id.trim(), `not trimmed: ${id}`); + assert.notEqual(id, '', 'empty entry found'); + } + }); + + it('includes bundled JetBrains xmlIds', async () => { + const ids = await loadTopJetBrains(); + assert.ok( + ids.includes('com.intellij.java'), + 'missing com.intellij.java', + ); + assert.ok( + ids.includes('org.jetbrains.kotlin'), + 'missing org.jetbrains.kotlin', + ); + }); + + it('includes the legitimate-typo "lombook plugin" xmlId', async () => { + const ids = await loadTopJetBrains(); + assert.ok( + ids.includes('lombook plugin'), + 'missing "lombook plugin" — canonical xmlId for Lombok integration', + ); + }); +}); + +describe('loadJetBrainsBlocklist', () => { + beforeEach(() => _resetCache()); + + it('returns an empty array (empty by design)', async () => { + const bl = await loadJetBrainsBlocklist(); + assert.ok(Array.isArray(bl)); + assert.equal( + bl.length, + 0, + 'blocklist should be empty by design in v6.6.0', + ); + }); + + it('does not throw on repeated invocation', async () => { + await assert.doesNotReject(() => loadJetBrainsBlocklist()); + await assert.doesNotReject(() => loadJetBrainsBlocklist()); + }); +}); + +describe('cache sanity', () => { + beforeEach(() => _resetCache()); + + it('calling loadTopJetBrains then loadJetBrainsBlocklist does not throw', async () => { + const ids = await loadTopJetBrains(); + const bl = await loadJetBrainsBlocklist(); + assert.ok(Array.isArray(ids) && ids.length > 0); + assert.ok(Array.isArray(bl)); + }); + + it('second loadTopJetBrains call returns same data (cache hit)', async () => { + const a = await loadTopJetBrains(); + const b = await loadTopJetBrains(); + assert.deepEqual(a, b); + }); + + it('VS Code loaders still work alongside JetBrains loaders', async () => { + const jb = await loadTopJetBrains(); + const vs = await loadTopVSCode(); + assert.ok(jb.length >= 40); + assert.ok(Array.isArray(vs)); + }); +});