feat(llm-security): add runJetBrainsChecks with 7 JB-specific checks (inc. shaded-jar advisory)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-04-18 10:20:42 +02:00
commit ca43fb8dd1
2 changed files with 397 additions and 1 deletions

View file

@ -8,7 +8,7 @@ import assert from 'node:assert/strict';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resetCounter } from '../../scanners/lib/output.mjs';
import { scan, discoverAll } from '../../scanners/ide-extension-scanner.mjs';
import { scan, discoverAll, __testing as scannerInternals } from '../../scanners/ide-extension-scanner.mjs';
import {
discoverVSCodeExtensions,
parseDirName,
@ -215,6 +215,200 @@ describe('ide-extension-scanner integration', () => {
});
});
// ---------------------------------------------------------------------------
// JetBrains check unit tests — crafted manifests, no filesystem fixtures.
// ---------------------------------------------------------------------------
const jbExt = (id) => ({
id,
publisher: '',
name: id,
version: '1.0',
location: '/fake/path',
type: 'jetbrains',
source: null,
isBuiltin: false,
installedTimestamp: null,
targetPlatform: null,
publisherDisplayName: null,
signed: false,
rootDir: '/fake',
productDir: 'IntelliJIdea2024.3',
});
describe('runJetBrainsChecks — Premain-Class detection', () => {
it('HIGH finding when hasPremainClass = true', () => {
const findings = scannerInternals.checkPremainClassJB(
jbExt('com.example.premain'),
{ hasPremainClass: true, premainClass: 'com.example.Agent' },
'plugins/com.example.premain'
);
assert.equal(findings.length, 1);
assert.equal(findings[0].severity, 'high');
assert.ok(findings[0].title.includes('checkPremainClassJB'));
});
it('no finding when hasPremainClass = false', () => {
const findings = scannerInternals.checkPremainClassJB(
jbExt('com.example.clean'),
{ hasPremainClass: false, premainClass: null },
'plugins/com.example.clean'
);
assert.equal(findings.length, 0);
});
});
describe('runJetBrainsChecks — native binaries', () => {
it('MEDIUM finding when nativeBinaries non-empty', () => {
const findings = scannerInternals.checkNativeBinariesJB(
jbExt('com.example.native'),
{ nativeBinaries: [{ path: 'x.so', size: 100, sha256: 'a'.repeat(64) }] },
'plugins/com.example.native'
);
assert.equal(findings.length, 1);
assert.equal(findings[0].severity, 'medium');
assert.ok(findings[0].title.includes('checkNativeBinariesJB'));
});
});
describe('runJetBrainsChecks — broad activation', () => {
it('HIGH on application-components declared', () => {
const findings = scannerInternals.checkBroadActivationJB(
jbExt('com.example.broad'),
{ applicationComponents: ['com.X'], listeners: [], extensionDeclarations: [] },
'plugins/com.example.broad'
);
assert.equal(findings.length, 1);
assert.equal(findings[0].severity, 'high');
});
it('MEDIUM on postStartupActivity only', () => {
const findings = scannerInternals.checkBroadActivationJB(
jbExt('com.example.post'),
{
applicationComponents: [],
listeners: [],
extensionDeclarations: [{ namespace: 'com.intellij', name: 'postStartupActivity', attrs: {} }],
},
'plugins/com.example.post'
);
assert.equal(findings.length, 1);
assert.equal(findings[0].severity, 'medium');
});
});
describe('runJetBrainsChecks — typosquat', () => {
it('MEDIUM when distance 1 from canonical corpus entry', () => {
const findings = scannerInternals.checkTyposquatJB(
jbExt('com.intellij.jaba'),
['com.intellij.java', 'org.jetbrains.kotlin'],
'plugins/com.intellij.jaba'
);
assert.equal(findings.length, 1);
assert.equal(findings[0].severity, 'medium');
});
it('no finding on exact corpus match', () => {
const findings = scannerInternals.checkTyposquatJB(
jbExt('com.intellij.java'),
['com.intellij.java'],
'plugins/com.intellij.java'
);
assert.equal(findings.length, 0);
});
});
describe('runJetBrainsChecks — depends chain', () => {
it('MEDIUM when 3+ depends with mandatory', () => {
const findings = scannerInternals.checkDependsChainJB(
jbExt('com.example.depchain'),
{
depends: [
{ id: 'a', optional: false, configFile: null },
{ id: 'b', optional: false, configFile: null },
{ id: 'c', optional: true, configFile: null },
{ id: 'd', optional: false, configFile: null },
],
},
'plugins/com.example.depchain'
);
assert.equal(findings.length, 1);
assert.equal(findings[0].severity, 'medium');
});
it('no finding when all optional', () => {
const findings = scannerInternals.checkDependsChainJB(
jbExt('com.example.opt'),
{
depends: [
{ id: 'a', optional: true, configFile: null },
{ id: 'b', optional: true, configFile: null },
{ id: 'c', optional: true, configFile: null },
],
},
'plugins/com.example.opt'
);
assert.equal(findings.length, 0);
});
});
describe('runJetBrainsChecks — theme-with-code', () => {
it('HIGH when themeProvider plus applicationComponents', () => {
const findings = scannerInternals.checkThemeWithCodeJB(
jbExt('com.example.twc'),
{
themeProviders: [{ id: 't', path: '/x' }],
extensionDeclarations: [],
applicationComponents: ['com.X'],
},
'plugins/com.example.twc'
);
assert.equal(findings.length, 1);
assert.equal(findings[0].severity, 'high');
});
});
describe('runJetBrainsChecks — shaded jars', () => {
it('MEDIUM on shaded bundled jars', () => {
const findings = scannerInternals.checkShadedJarsJB(
jbExt('com.example.shade'),
{
bundledJars: [
{ name: 'a.jar', version: null, shaded: true, coords: null },
{ name: 'b.jar', version: '1.0', shaded: false, coords: 'b' },
],
},
'plugins/com.example.shade'
);
assert.equal(findings.length, 1);
assert.equal(findings[0].severity, 'medium');
});
});
describe('runJetBrainsChecks — full dispatcher', () => {
it('aggregates all JB checks', () => {
const findings = scannerInternals.runJetBrainsChecks(
jbExt('com.example.all'),
{
themeProviders: [],
applicationComponents: [],
listeners: [],
extensionDeclarations: [],
depends: [],
hasPremainClass: true,
premainClass: 'com.X',
nativeBinaries: [],
bundledJars: [],
},
['com.intellij.java'],
[],
'plugins/com.example.all'
);
assert.ok(findings.length >= 1);
assert.ok(findings.some(f => f.title.includes('checkPremainClassJB')));
});
});
describe('blocklist matching', () => {
it('matchBlocklistEntry matches wildcard version', async () => {
// Unit-test the blocklist logic via scan with custom options — we inject