feat(llm-security): wire JetBrains branch into scanOneExtension
This commit is contained in:
parent
ca43fb8dd1
commit
aa269ed6d8
2 changed files with 109 additions and 16 deletions
|
|
@ -25,8 +25,14 @@ import {
|
||||||
discoverVSCodeExtensions,
|
discoverVSCodeExtensions,
|
||||||
discoverJetBrainsExtensions,
|
discoverJetBrainsExtensions,
|
||||||
} from './lib/ide-extension-discovery.mjs';
|
} from './lib/ide-extension-discovery.mjs';
|
||||||
import { parseVSCodeExtension, parseVsixFile } from './lib/ide-extension-parser.mjs';
|
import { parseVSCodeExtension, parseVsixFile, parseIntelliJPlugin } from './lib/ide-extension-parser.mjs';
|
||||||
import { loadTopVSCode, loadVSCodeBlocklist, normalizeId } from './lib/ide-extension-data.mjs';
|
import {
|
||||||
|
loadTopVSCode,
|
||||||
|
loadVSCodeBlocklist,
|
||||||
|
loadTopJetBrains,
|
||||||
|
loadJetBrainsBlocklist,
|
||||||
|
normalizeId,
|
||||||
|
} from './lib/ide-extension-data.mjs';
|
||||||
import { fetchVsixFromUrl, detectUrlType } from './lib/vsix-fetch.mjs';
|
import { fetchVsixFromUrl, detectUrlType } from './lib/vsix-fetch.mjs';
|
||||||
import { extractToDir, ZipError } from './lib/zip-extract.mjs';
|
import { extractToDir, ZipError } from './lib/zip-extract.mjs';
|
||||||
import { runVsixWorker } from './lib/vsix-sandbox.mjs';
|
import { runVsixWorker } from './lib/vsix-sandbox.mjs';
|
||||||
|
|
@ -516,8 +522,10 @@ async function scanOneExtension(ext, options) {
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
|
|
||||||
// Parse manifest
|
// Parse manifest — dispatch on extension type
|
||||||
const parsed = await parseVSCodeExtension(ext.location);
|
const parsed = ext.type === 'jetbrains'
|
||||||
|
? await parseIntelliJPlugin(ext.location)
|
||||||
|
: await parseVSCodeExtension(ext.location);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
return {
|
return {
|
||||||
id: ext.id,
|
id: ext.id,
|
||||||
|
|
@ -537,28 +545,45 @@ async function scanOneExtension(ext, options) {
|
||||||
const manifest = parsed.manifest;
|
const manifest = parsed.manifest;
|
||||||
warnings.push(...parsed.warnings);
|
warnings.push(...parsed.warnings);
|
||||||
|
|
||||||
const topList = await loadTopVSCode();
|
const isJetBrains = ext.type === 'jetbrains';
|
||||||
const blocklist = await loadVSCodeBlocklist();
|
const topList = isJetBrains ? [] : await loadTopVSCode();
|
||||||
|
const blocklist = isJetBrains ? [] : await loadVSCodeBlocklist();
|
||||||
|
const topListJB = isJetBrains ? await loadTopJetBrains() : [];
|
||||||
|
const blocklistJB = isJetBrains ? await loadJetBrainsBlocklist() : [];
|
||||||
|
|
||||||
const relLocation = relative(options.targetBase || ext.location, ext.location) || '.';
|
const relLocation = relative(options.targetBase || ext.location, ext.location) || '.';
|
||||||
|
|
||||||
// Discover files (Pass A) — excludes node_modules, used for ENT/NET/TNT/UNI
|
// Discover files (Pass A) — excludes node_modules, used for ENT/NET/TNT/UNI
|
||||||
const discovery = await discoverFiles(ext.location).catch(() => ({ files: [], skipped: 0, truncated: false }));
|
const discovery = await discoverFiles(ext.location).catch(() => ({ files: [], skipped: 0, truncated: false }));
|
||||||
|
|
||||||
// Pass B for MEM — filter to README/CHANGELOG/package.json only
|
// Pass B for MEM — filter to README/CHANGELOG/package.json only (VS Code),
|
||||||
|
// plus plugin.xml and META-INF/MANIFEST.MF for JetBrains plugins.
|
||||||
const memFiles = discovery.files.filter(f => {
|
const memFiles = discovery.files.filter(f => {
|
||||||
const lower = (f.relPath || '').toLowerCase();
|
const lower = (f.relPath || '').toLowerCase();
|
||||||
return lower === 'readme.md' || lower === 'changelog.md' || lower === 'package.json';
|
if (lower === 'readme.md' || lower === 'changelog.md' || lower === 'package.json') return true;
|
||||||
|
if (isJetBrains) {
|
||||||
|
if (lower === 'plugin.xml' || lower.endsWith('/plugin.xml')) return true;
|
||||||
|
if (lower === 'meta-inf/manifest.mf' || lower.endsWith('/meta-inf/manifest.mf')) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// IDE-specific findings
|
// IDE-specific findings — dispatch on extension type
|
||||||
const ideFindings = runIdeChecks(
|
const ideFindings = isJetBrains
|
||||||
{ ...ext, signed: manifest.hasSignature || ext.signed },
|
? runJetBrainsChecks(
|
||||||
manifest,
|
{ ...ext, signed: manifest.hasSignature || ext.signed },
|
||||||
topList,
|
manifest,
|
||||||
blocklist,
|
topListJB,
|
||||||
relLocation,
|
blocklistJB,
|
||||||
);
|
relLocation,
|
||||||
|
)
|
||||||
|
: runIdeChecks(
|
||||||
|
{ ...ext, signed: manifest.hasSignature || ext.signed },
|
||||||
|
manifest,
|
||||||
|
topList,
|
||||||
|
blocklist,
|
||||||
|
relLocation,
|
||||||
|
);
|
||||||
const ideResult = scannerResult(SCANNER, 'ok', ideFindings, 1, Date.now() - started);
|
const ideResult = scannerResult(SCANNER, 'ok', ideFindings, 1, Date.now() - started);
|
||||||
|
|
||||||
// Run reused scanners (each is independent; run sequentially to avoid burst-rate issues)
|
// Run reused scanners (each is independent; run sequentially to avoid burst-rate issues)
|
||||||
|
|
@ -805,6 +830,7 @@ export const __testing = {
|
||||||
checkPremainClassJB,
|
checkPremainClassJB,
|
||||||
checkNativeBinariesJB,
|
checkNativeBinariesJB,
|
||||||
checkShadedJarsJB,
|
checkShadedJarsJB,
|
||||||
|
scanOneExtension,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -423,3 +423,70 @@ describe('blocklist matching', () => {
|
||||||
assert.equal(crit.length, 0);
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue