feat(llm-security): implement parseIntelliJPlugin with nested-jar extraction
This commit is contained in:
parent
b86239448d
commit
5afb9b1f33
3 changed files with 479 additions and 7 deletions
|
|
@ -2,12 +2,18 @@
|
|||
//
|
||||
// All inputs are inline strings — no filesystem fixtures required.
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import { describe, it, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, mkdir, writeFile, readdir, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import {
|
||||
parsePluginXml,
|
||||
parseManifestMf,
|
||||
parseIntelliJPlugin,
|
||||
} from '../../scanners/lib/ide-extension-parser.mjs';
|
||||
import { createZip } from '../helpers/zip-writer.mjs';
|
||||
|
||||
describe('parsePluginXml — happy path', () => {
|
||||
const xml = `<?xml version="1.0"?>
|
||||
|
|
@ -241,3 +247,134 @@ describe('parseManifestMf', () => {
|
|||
assert.deepEqual(parseManifestMf(lf), parseManifestMf(crlf));
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseIntelliJPlugin — synthetic plugin dirs built in-test via zip-writer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TEST_TMP_PREFIX = 'llmsec-jbparse-test-';
|
||||
const createdRoots = [];
|
||||
|
||||
async function makePluginDir(jars) {
|
||||
const root = await mkdtemp(join(tmpdir(), TEST_TMP_PREFIX));
|
||||
createdRoots.push(root);
|
||||
await mkdir(join(root, 'lib'), { recursive: true });
|
||||
for (const { name, entries } of jars) {
|
||||
const buf = createZip(entries);
|
||||
await writeFile(join(root, 'lib', name), buf);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
const BENIGN_PLUGIN_XML = `<?xml version="1.0"?>
|
||||
<idea-plugin>
|
||||
<id>com.example.benign</id>
|
||||
<name>Benign</name>
|
||||
<version>1.0</version>
|
||||
<vendor>Example</vendor>
|
||||
</idea-plugin>`;
|
||||
|
||||
describe('parseIntelliJPlugin — benign synthetic plugin', () => {
|
||||
it('extracts pluginId, depends, no native/premain/signature', async () => {
|
||||
const root = await makePluginDir([
|
||||
{
|
||||
name: 'main.jar',
|
||||
entries: [
|
||||
{ name: 'META-INF/plugin.xml', data: BENIGN_PLUGIN_XML },
|
||||
{ name: 'META-INF/MANIFEST.MF', data: 'Manifest-Version: 1.0\n' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
const res = await parseIntelliJPlugin(root);
|
||||
assert.ok(res, 'expected non-null result');
|
||||
assert.ok(res.manifest, 'expected manifest');
|
||||
assert.equal(res.manifest.type, 'jetbrains');
|
||||
assert.equal(res.manifest.pluginId, 'com.example.benign');
|
||||
assert.equal(res.manifest.nativeBinaries.length, 0);
|
||||
assert.equal(res.manifest.hasPremainClass, false);
|
||||
assert.equal(res.manifest.hasSignature, false);
|
||||
assert.ok(Array.isArray(res.manifest.bundledJars));
|
||||
assert.equal(res.manifest.bundledJars.length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseIntelliJPlugin — Premain-Class detection', () => {
|
||||
it('hasPremainClass === true when MANIFEST.MF sets it', async () => {
|
||||
const root = await makePluginDir([
|
||||
{
|
||||
name: 'main.jar',
|
||||
entries: [
|
||||
{ name: 'META-INF/plugin.xml', data: BENIGN_PLUGIN_XML },
|
||||
{
|
||||
name: 'META-INF/MANIFEST.MF',
|
||||
data: 'Manifest-Version: 1.0\nPremain-Class: com.example.Agent\n',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const res = await parseIntelliJPlugin(root);
|
||||
assert.equal(res.manifest.hasPremainClass, true);
|
||||
assert.equal(res.manifest.premainClass, 'com.example.Agent');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseIntelliJPlugin — native binary detection', () => {
|
||||
it('collects .so files with SHA-256 and size', async () => {
|
||||
const nativeBytes = Buffer.alloc(16, 0xAB);
|
||||
const expectedSha = createHash('sha256').update(nativeBytes).digest('hex');
|
||||
const root = await makePluginDir([
|
||||
{
|
||||
name: 'main.jar',
|
||||
entries: [
|
||||
{ name: 'META-INF/plugin.xml', data: BENIGN_PLUGIN_XML },
|
||||
{ name: 'native/dummy.so', data: nativeBytes },
|
||||
],
|
||||
},
|
||||
]);
|
||||
const res = await parseIntelliJPlugin(root);
|
||||
assert.equal(res.manifest.nativeBinaries.length, 1);
|
||||
assert.equal(res.manifest.nativeBinaries[0].size, 16);
|
||||
assert.equal(res.manifest.nativeBinaries[0].sha256, expectedSha);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseIntelliJPlugin — failure modes', () => {
|
||||
it('missing lib/ returns null with IDE-JB-NO-LIB-DIR warning', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), TEST_TMP_PREFIX));
|
||||
createdRoots.push(root);
|
||||
const res = await parseIntelliJPlugin(root);
|
||||
assert.equal(res.manifest, null);
|
||||
assert.ok(res.warnings.some(w => w.startsWith('IDE-JB-NO-LIB-DIR')));
|
||||
});
|
||||
|
||||
it('no plugin.xml in any jar returns null with IDE-JB-NO-PLUGIN-XML', async () => {
|
||||
const root = await makePluginDir([
|
||||
{
|
||||
name: 'empty.jar',
|
||||
entries: [
|
||||
{ name: 'META-INF/MANIFEST.MF', data: 'Manifest-Version: 1.0\n' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
const res = await parseIntelliJPlugin(root);
|
||||
assert.equal(res.manifest, null);
|
||||
assert.ok(res.warnings.some(w => w.includes('NO-PLUGIN-XML')));
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseIntelliJPlugin — temp dir cleanup', () => {
|
||||
after(async () => {
|
||||
// Cleanup test plugin roots
|
||||
for (const r of createdRoots) {
|
||||
await rm(r, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
// Assert no llmsec-jb-* temp dirs remain
|
||||
const entries = await readdir(tmpdir()).catch(() => []);
|
||||
const leaked = entries.filter(n => n.startsWith('llmsec-jb-'));
|
||||
assert.equal(leaked.length, 0, `leaked temp dirs: ${leaked.join(', ')}`);
|
||||
});
|
||||
|
||||
it('cleanup runs (checked via after hook)', () => {
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue