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
|
|
@ -5,8 +5,11 @@
|
|||
// - type: 'vscode' → parseVSCodeExtension (package.json + contributes)
|
||||
// - type: 'jetbrains' → parseIntelliJPlugin (plugin.xml + MANIFEST.MF inside JARs)
|
||||
|
||||
import { readFile, access } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { readFile, readdir, stat, mkdtemp, rm, access } from 'node:fs/promises';
|
||||
import { join, basename } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { extractToDir } from './zip-extract.mjs';
|
||||
|
||||
async function pathExists(p) {
|
||||
try { await access(p); return true; } catch { return false; }
|
||||
|
|
@ -400,12 +403,217 @@ export function parseManifestMf(mfString) {
|
|||
return out;
|
||||
}
|
||||
|
||||
const NATIVE_BIN_RE = /\.(dll|so|dylib|jnilib|exe)$/i;
|
||||
const SIGNATURE_FILE_RE = /\.(SF|RSA|DSA|EC)$/;
|
||||
|
||||
async function walkFiles(rootDir) {
|
||||
const out = [];
|
||||
async function recurse(dir) {
|
||||
let entries;
|
||||
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
||||
for (const entry of entries) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) await recurse(full);
|
||||
else if (entry.isFile()) out.push(full);
|
||||
}
|
||||
}
|
||||
await recurse(rootDir);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an IntelliJ plugin directory. Implemented in Step 6 (v6.6.0).
|
||||
* Stub preserved until Step 6 lands.
|
||||
* Parse an IntelliJ plugin directory layout:
|
||||
* <pluginRoot>/lib/*.jar — main jar contains META-INF/plugin.xml
|
||||
*
|
||||
* @param {string} pluginRoot
|
||||
* @returns {Promise<null>}
|
||||
* @returns {Promise<{ manifest: ParsedManifest, warnings: string[] } | null>}
|
||||
*/
|
||||
export async function parseIntelliJPlugin(pluginRoot) {
|
||||
return null;
|
||||
if (typeof pluginRoot !== 'string' || !pluginRoot) return null;
|
||||
const warnings = [];
|
||||
|
||||
const libDir = join(pluginRoot, 'lib');
|
||||
try {
|
||||
const s = await stat(libDir);
|
||||
if (!s.isDirectory()) {
|
||||
warnings.push('IDE-JB-NO-LIB-DIR: lib is not a directory');
|
||||
return { manifest: null, warnings };
|
||||
}
|
||||
} catch {
|
||||
warnings.push('IDE-JB-NO-LIB-DIR: lib directory missing');
|
||||
return { manifest: null, warnings };
|
||||
}
|
||||
|
||||
let jarNames;
|
||||
try {
|
||||
jarNames = (await readdir(libDir)).filter(n => n.toLowerCase().endsWith('.jar'));
|
||||
} catch {
|
||||
warnings.push('IDE-JB-NO-LIB-DIR: cannot read lib');
|
||||
return { manifest: null, warnings };
|
||||
}
|
||||
if (jarNames.length === 0) {
|
||||
warnings.push('IDE-JB-NO-PLUGIN-XML: no jars in lib/');
|
||||
return { manifest: null, warnings };
|
||||
}
|
||||
|
||||
const extractionRoot = await mkdtemp(join(tmpdir(), 'llmsec-jb-'));
|
||||
const extractedJars = [];
|
||||
|
||||
try {
|
||||
for (const jarName of jarNames) {
|
||||
const jarPath = join(libDir, jarName);
|
||||
try {
|
||||
const jarBuffer = await readFile(jarPath);
|
||||
const jarDir = await mkdtemp(join(extractionRoot, 'jar-'));
|
||||
await extractToDir(jarBuffer, jarDir);
|
||||
extractedJars.push({ jarPath, jarName, jarDir });
|
||||
} catch (err) {
|
||||
warnings.push(`IDE-JB-JAR-EXTRACT: ${jarName}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (extractedJars.length === 0) {
|
||||
warnings.push('IDE-JB-NO-PLUGIN-XML: no jars could be extracted');
|
||||
return { manifest: null, warnings };
|
||||
}
|
||||
|
||||
// Locate main jar: first one containing META-INF/plugin.xml
|
||||
let mainJar = null;
|
||||
const mainJarCandidates = [];
|
||||
for (const ej of extractedJars) {
|
||||
const xmlPath = join(ej.jarDir, 'META-INF', 'plugin.xml');
|
||||
if (await pathExists(xmlPath)) {
|
||||
mainJarCandidates.push(ej);
|
||||
if (!mainJar) mainJar = ej;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mainJar) {
|
||||
warnings.push('IDE-JB-NO-PLUGIN-XML: no jar contains META-INF/plugin.xml');
|
||||
return { manifest: null, warnings };
|
||||
}
|
||||
if (mainJarCandidates.length > 1) {
|
||||
warnings.push(`IDE-JB-MULTIPLE-PLUGIN-XML: ${mainJarCandidates.length} jars contain plugin.xml; first wins`);
|
||||
}
|
||||
|
||||
// Parse plugin.xml
|
||||
let pluginXmlResult;
|
||||
try {
|
||||
const xmlRaw = await readFile(join(mainJar.jarDir, 'META-INF', 'plugin.xml'), 'utf8');
|
||||
pluginXmlResult = parsePluginXml(xmlRaw);
|
||||
} catch (err) {
|
||||
warnings.push(`IDE-JB-PLUGIN-XML-READ: ${err.message}`);
|
||||
return { manifest: null, warnings };
|
||||
}
|
||||
if (pluginXmlResult.warnings.length) warnings.push(...pluginXmlResult.warnings);
|
||||
if (!pluginXmlResult.manifest) {
|
||||
warnings.push('IDE-JB-PLUGIN-XML-PARSE: unparseable plugin.xml');
|
||||
return { manifest: null, warnings };
|
||||
}
|
||||
const px = pluginXmlResult.manifest;
|
||||
|
||||
// Parse main jar MANIFEST.MF
|
||||
let mainMf = { mainClass: null, premainClass: null, implTitle: null, implVersion: null, premainAttrs: {} };
|
||||
const mainMfPath = join(mainJar.jarDir, 'META-INF', 'MANIFEST.MF');
|
||||
if (await pathExists(mainMfPath)) {
|
||||
try {
|
||||
const mfRaw = await readFile(mainMfPath, 'utf8');
|
||||
mainMf = parseManifestMf(mfRaw);
|
||||
} catch (err) {
|
||||
warnings.push(`IDE-JB-MANIFEST-MF-READ: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk ALL jar-dirs for native binaries
|
||||
const nativeBinaries = [];
|
||||
for (const ej of extractedJars) {
|
||||
const files = await walkFiles(ej.jarDir);
|
||||
for (const f of files) {
|
||||
if (NATIVE_BIN_RE.test(f)) {
|
||||
try {
|
||||
const buf = await readFile(f);
|
||||
const s = await stat(f);
|
||||
nativeBinaries.push({
|
||||
path: `${ej.jarName}:${f.slice(ej.jarDir.length + 1)}`,
|
||||
size: s.size,
|
||||
sha256: createHash('sha256').update(buf).digest('hex'),
|
||||
});
|
||||
} catch (err) {
|
||||
warnings.push(`IDE-JB-NATIVE-READ: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse every jar's MANIFEST.MF for bundled-jars list
|
||||
const bundledJars = [];
|
||||
for (const ej of extractedJars) {
|
||||
const mfPath = join(ej.jarDir, 'META-INF', 'MANIFEST.MF');
|
||||
let mf = { implTitle: null, implVersion: null };
|
||||
if (await pathExists(mfPath)) {
|
||||
try {
|
||||
mf = parseManifestMf(await readFile(mfPath, 'utf8'));
|
||||
} catch {
|
||||
// fall through with nulls
|
||||
}
|
||||
}
|
||||
bundledJars.push({
|
||||
name: ej.jarName,
|
||||
version: mf.implVersion || null,
|
||||
shaded: !mf.implTitle || !mf.implVersion,
|
||||
coords: mf.implTitle || null,
|
||||
});
|
||||
}
|
||||
|
||||
// Signature check on main jar
|
||||
let hasSignature = false;
|
||||
try {
|
||||
const metaInfDir = join(mainJar.jarDir, 'META-INF');
|
||||
const metaEntries = await readdir(metaInfDir);
|
||||
hasSignature = metaEntries.some(f => SIGNATURE_FILE_RE.test(f));
|
||||
} catch { /* no META-INF */ }
|
||||
|
||||
const pluginId = px.pluginId || basename(pluginRoot);
|
||||
|
||||
const manifest = {
|
||||
type: 'jetbrains',
|
||||
id: pluginId.toLowerCase(),
|
||||
pluginId,
|
||||
publisher: (px.vendor || '').toLowerCase(),
|
||||
name: px.name || '',
|
||||
version: px.version || '',
|
||||
engines: {},
|
||||
main: null,
|
||||
browser: null,
|
||||
activationEvents: [],
|
||||
contributes: {},
|
||||
extensionPack: [],
|
||||
extensionDependencies: [],
|
||||
extensionKind: [],
|
||||
categories: [],
|
||||
capabilities: {},
|
||||
scripts: {},
|
||||
repository: px.vendorUrl || null,
|
||||
dependencies: {},
|
||||
hasSignature,
|
||||
sinceBuild: px.sinceBuild,
|
||||
untilBuild: px.untilBuild,
|
||||
depends: px.depends,
|
||||
extensionDeclarations: px.extensionDeclarations,
|
||||
applicationComponents: px.applicationComponents,
|
||||
listeners: px.listeners,
|
||||
themeProviders: px.themeProviders,
|
||||
hasPremainClass: Boolean(mainMf.premainClass),
|
||||
premainClass: mainMf.premainClass || null,
|
||||
nativeBinaries,
|
||||
bundledJars,
|
||||
};
|
||||
|
||||
return { manifest, warnings };
|
||||
} catch (err) {
|
||||
warnings.push(`IDE-JB-UNCAUGHT: ${err.message}`);
|
||||
return { manifest: null, warnings };
|
||||
} finally {
|
||||
await rm(extractionRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue