feat(llm-security): seed top-jetbrains-plugins.json + loadJetBrainsBlocklist export
Step 1/17 of ultraplan-2026-04-17-jetbrains-ide-scan. - Populate top-jetbrains-plugins.json with 56 canonical xmlIds (bundled + popular third-party): com.intellij.java, org.jetbrains.kotlin, com.jetbrains.python.community, org.rust.lang, com.github.copilot, mobi.hsz.idea.gitignore, the legitimate-typo 'Lombook Plugin', etc. - Add loadJetBrainsBlocklist() export mirroring loadVSCodeBlocklist shape. Blocklist is empty by design — no public confirmed-malicious JetBrains Marketplace plugins as of 2026-04-17. - Add tests/scanners/ide-extension-data.test.mjs (9 tests, all pass). - Fix cache bug in loadTopJetBrains: map normalizeId on cache-hit path too (was previously unnormalized on second call). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1634197853
commit
a86ca00960
3 changed files with 177 additions and 7 deletions
|
|
@ -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": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,15 +42,27 @@ export async function loadVSCodeBlocklist() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Load top JetBrains plugin IDs (stub for v1.1).
|
||||
* @returns {Promise<string[]>}
|
||||
* Load top JetBrains plugin xmlIds (canonical corpus for typosquat detection).
|
||||
* @returns {Promise<string[]>} 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<string[]>} 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
|
||||
|
|
|
|||
100
plugins/llm-security/tests/scanners/ide-extension-data.test.mjs
Normal file
100
plugins/llm-security/tests/scanners/ide-extension-data.test.mjs
Normal file
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue