624 lines
24 KiB
JavaScript
624 lines
24 KiB
JavaScript
// ide-extension-scanner.test.mjs — Integration tests for the IDE extension scanner.
|
|
//
|
|
// Uses fixture trees under tests/fixtures/ide-extensions/ to simulate
|
|
// real ~/.vscode/extensions/ layouts via rootsOverride injection.
|
|
|
|
import { describe, it, before, beforeEach } from 'node:test';
|
|
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, __testing as scannerInternals } from '../../scanners/ide-extension-scanner.mjs';
|
|
import {
|
|
discoverVSCodeExtensions,
|
|
parseDirName,
|
|
} from '../../scanners/lib/ide-extension-discovery.mjs';
|
|
import { parseVSCodeExtension } from '../../scanners/lib/ide-extension-parser.mjs';
|
|
|
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
const FIXTURES = resolve(__dirname, '../fixtures/ide-extensions');
|
|
const ROOT_BENIGN = resolve(FIXTURES, 'root-benign');
|
|
const ROOT_MIXED = resolve(FIXTURES, 'root-mixed');
|
|
|
|
describe('parseDirName', () => {
|
|
it('parses plain publisher.name-version', () => {
|
|
const out = parseDirName('ms-python.python-2024.1.0');
|
|
assert.ok(out);
|
|
assert.equal(out.publisher, 'ms-python');
|
|
assert.equal(out.name, 'python');
|
|
assert.equal(out.version, '2024.1.0');
|
|
assert.equal(out.targetPlatform, null);
|
|
});
|
|
|
|
it('parses prerelease suffix', () => {
|
|
const out = parseDirName('publisher.name-1.2.3-beta.1');
|
|
assert.ok(out);
|
|
assert.equal(out.version, '1.2.3-beta.1');
|
|
});
|
|
|
|
it('parses target platform suffix', () => {
|
|
const out = parseDirName('publisher.name-1.2.3-darwin-x64');
|
|
assert.ok(out);
|
|
assert.equal(out.version, '1.2.3');
|
|
assert.equal(out.targetPlatform, 'darwin-x64');
|
|
});
|
|
|
|
it('returns null for non-version-shaped dir', () => {
|
|
const out = parseDirName('.obsolete');
|
|
assert.equal(out, null);
|
|
});
|
|
|
|
it('returns null when identifier has no dot', () => {
|
|
const out = parseDirName('noDotInIdentifier-1.0.0');
|
|
assert.equal(out, null);
|
|
});
|
|
});
|
|
|
|
describe('parseVSCodeExtension', () => {
|
|
it('parses a valid extension manifest', async () => {
|
|
const p = resolve(ROOT_BENIGN, 'publisher.benign-ext-1.0.0');
|
|
const res = await parseVSCodeExtension(p);
|
|
assert.ok(res);
|
|
assert.equal(res.manifest.id, 'publisher.benign-ext');
|
|
assert.equal(res.manifest.publisher, 'publisher');
|
|
assert.equal(res.manifest.name, 'benign-ext');
|
|
assert.equal(res.manifest.main, './extension.js');
|
|
assert.ok(Array.isArray(res.manifest.activationEvents));
|
|
});
|
|
|
|
it('returns null when package.json missing', async () => {
|
|
const res = await parseVSCodeExtension('/nonexistent/path');
|
|
assert.equal(res, null);
|
|
});
|
|
});
|
|
|
|
describe('discoverVSCodeExtensions', () => {
|
|
it('discovers extensions under rootsOverride', async () => {
|
|
const { extensions, warnings, rootsScanned } = await discoverVSCodeExtensions({
|
|
rootsOverride: [ROOT_BENIGN],
|
|
});
|
|
assert.equal(rootsScanned.length, 1);
|
|
assert.equal(extensions.length, 2);
|
|
const ids = extensions.map(e => e.id).sort();
|
|
assert.deepEqual(ids, ['publisher.benign-ext', 'theme.goodtheme']);
|
|
assert.equal(warnings.length, 0);
|
|
});
|
|
|
|
it('reads source/isBuiltin from extensions.json index', async () => {
|
|
const { extensions } = await discoverVSCodeExtensions({
|
|
rootsOverride: [ROOT_MIXED],
|
|
});
|
|
const sideloaded = extensions.find(e => e.id === 'sideloaded.extension');
|
|
assert.ok(sideloaded);
|
|
assert.equal(sideloaded.source, 'vsix');
|
|
assert.equal(sideloaded.isBuiltin, false);
|
|
const gallery = extensions.find(e => e.id === 'wildcard.activator');
|
|
assert.equal(gallery.source, 'gallery');
|
|
});
|
|
});
|
|
|
|
describe('ide-extension-scanner integration', () => {
|
|
beforeEach(() => {
|
|
resetCounter();
|
|
});
|
|
|
|
it('benign root: no CRITICAL or HIGH IDE findings', async () => {
|
|
const env = await scan('all', { rootsOverride: [ROOT_BENIGN], vscodeOnly: true });
|
|
assert.equal(env.meta.extensions_discovered.vscode, 2);
|
|
const ideCrit = env.extensions.flatMap(e => e.scanner_results.IDE.findings)
|
|
.filter(f => f.severity === 'critical');
|
|
assert.equal(ideCrit.length, 0, `Expected no CRITICAL, got ${ideCrit.map(f => f.title).join('; ')}`);
|
|
});
|
|
|
|
it('detects theme-with-code (HIGH) on evil.theme-with-code', async () => {
|
|
const env = await scan('all', { rootsOverride: [ROOT_MIXED], vscodeOnly: true });
|
|
const ext = env.extensions.find(e => e.id === 'evil.theme-with-code');
|
|
assert.ok(ext, 'evil.theme-with-code not found');
|
|
const themeFindings = ext.scanner_results.IDE.findings.filter(f =>
|
|
f.title.toLowerCase().includes('theme'));
|
|
assert.ok(themeFindings.length >= 1, 'expected theme-with-code finding');
|
|
assert.equal(themeFindings[0].severity, 'high');
|
|
});
|
|
|
|
it('detects typosquat (MEDIUM at distance=2 against top-50) on ms-pythom.pythom', async () => {
|
|
const env = await scan('all', { rootsOverride: [ROOT_MIXED], vscodeOnly: true });
|
|
const ext = env.extensions.find(e => e.id === 'ms-pythom.pythom');
|
|
assert.ok(ext);
|
|
const typo = ext.scanner_results.IDE.findings.filter(f =>
|
|
f.title.toLowerCase().includes('typosquat'));
|
|
assert.ok(typo.length >= 1, 'expected typosquat finding');
|
|
assert.equal(typo[0].severity, 'medium');
|
|
assert.ok(typo[0].title.includes('ms-python.python'));
|
|
});
|
|
|
|
it('detects sideload (HIGH unsigned) on sideloaded.extension', async () => {
|
|
const env = await scan('all', { rootsOverride: [ROOT_MIXED], vscodeOnly: true });
|
|
const ext = env.extensions.find(e => e.id === 'sideloaded.extension');
|
|
assert.ok(ext);
|
|
const sf = ext.scanner_results.IDE.findings.filter(f =>
|
|
f.title.toLowerCase().includes('sideloaded'));
|
|
assert.ok(sf.length >= 1);
|
|
assert.equal(sf[0].severity, 'high');
|
|
});
|
|
|
|
it('detects wildcard activation (MEDIUM) on wildcard.activator', async () => {
|
|
const env = await scan('all', { rootsOverride: [ROOT_MIXED], vscodeOnly: true });
|
|
const ext = env.extensions.find(e => e.id === 'wildcard.activator');
|
|
assert.ok(ext);
|
|
const w = ext.scanner_results.IDE.findings.filter(f =>
|
|
f.title.toLowerCase().includes('wildcard activation'));
|
|
assert.ok(w.length >= 1, 'expected wildcard activation finding');
|
|
assert.equal(w[0].severity, 'medium');
|
|
});
|
|
|
|
it('detects dangerous uninstall hook (HIGH) on hook.uninstall', async () => {
|
|
const env = await scan('all', { rootsOverride: [ROOT_MIXED], vscodeOnly: true });
|
|
const ext = env.extensions.find(e => e.id === 'hook.uninstall');
|
|
assert.ok(ext);
|
|
const h = ext.scanner_results.IDE.findings.filter(f =>
|
|
f.title.toLowerCase().includes('uninstall hook'));
|
|
assert.ok(h.length >= 1, 'expected uninstall-hook finding');
|
|
assert.equal(h[0].severity, 'high');
|
|
});
|
|
|
|
it('detects extension pack expansion (MEDIUM) on pack.big', async () => {
|
|
const env = await scan('all', { rootsOverride: [ROOT_MIXED], vscodeOnly: true });
|
|
const ext = env.extensions.find(e => e.id === 'pack.big');
|
|
assert.ok(ext);
|
|
const p = ext.scanner_results.IDE.findings.filter(f =>
|
|
f.title.toLowerCase().includes('extension pack'));
|
|
assert.ok(p.length >= 1);
|
|
assert.equal(p[0].severity, 'medium');
|
|
});
|
|
|
|
it('top-level verdict is WARNING/BLOCK for mixed root', async () => {
|
|
const env = await scan('all', { rootsOverride: [ROOT_MIXED], vscodeOnly: true });
|
|
assert.ok(
|
|
env.aggregate.verdict === 'WARNING' || env.aggregate.verdict === 'BLOCK',
|
|
`Expected WARNING/BLOCK, got ${env.aggregate.verdict}`,
|
|
);
|
|
});
|
|
|
|
it('all findings have DS-IDE- prefix', async () => {
|
|
const env = await scan('all', { rootsOverride: [ROOT_MIXED], vscodeOnly: true });
|
|
for (const ext of env.extensions) {
|
|
const ideFindings = ext.scanner_results.IDE.findings;
|
|
for (const f of ideFindings) {
|
|
assert.ok(f.id.startsWith('DS-IDE-'), `Expected DS-IDE- prefix, got ${f.id}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('single-target mode scans one extracted directory', async () => {
|
|
const target = resolve(ROOT_BENIGN, 'publisher.benign-ext-1.0.0');
|
|
const env = await scan(target, { vscodeOnly: true });
|
|
assert.equal(env.extensions.length, 1);
|
|
assert.equal(env.extensions[0].id, 'publisher.benign-ext');
|
|
});
|
|
|
|
it('discoverAll returns extensions list', async () => {
|
|
const exts = await discoverAll({ rootsOverride: [ROOT_BENIGN] });
|
|
assert.equal(exts.length, 2);
|
|
});
|
|
|
|
it('envelope shape is valid', async () => {
|
|
const env = await scan('all', { rootsOverride: [ROOT_BENIGN], vscodeOnly: true });
|
|
assert.ok(env.meta);
|
|
assert.ok(env.extensions);
|
|
assert.ok(env.aggregate);
|
|
assert.ok(env.meta.scanner);
|
|
assert.ok(env.meta.version);
|
|
assert.ok(typeof env.meta.duration_ms === 'number');
|
|
assert.ok(Array.isArray(env.meta.roots_scanned));
|
|
assert.ok(env.aggregate.counts);
|
|
assert.ok(typeof env.aggregate.risk_score === 'number');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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
|
|
// a fake blocklist-matching extension via rootsOverride + custom fixture.
|
|
// Since production blocklist may be empty, we test the code path via a
|
|
// minimal manual check: parse an extension and verify scanner does not
|
|
// crash on empty blocklist.
|
|
const env = await scan('all', { rootsOverride: [ROOT_BENIGN], vscodeOnly: true });
|
|
const allFindings = env.extensions.flatMap(e => e.scanner_results.IDE.findings);
|
|
// No blocklist matches expected for the benign root
|
|
const crit = allFindings.filter(f => f.severity === 'critical');
|
|
assert.equal(crit.length, 0);
|
|
});
|
|
});
|
|
|
|
describe('scanOneExtension JetBrains dispatch', () => {
|
|
// Fixture-free verification: synthesize an ExtensionRecord with type:'jetbrains'
|
|
// and a minimal plugin dir (lib/<jar> 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 = `<idea-plugin>
|
|
<id>com.example.dispatch</id>
|
|
<name>Dispatch Test</name>
|
|
<version>1.0.0</version>
|
|
<vendor>Example</vendor>
|
|
<idea-version since-build="241.0"/>
|
|
</idea-plugin>`;
|
|
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');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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}`);
|
|
}
|
|
});
|
|
});
|