ktg-plugin-marketplace/plugins/llm-security/scanners/lib/ide-extension-parser.mjs
Kjell Tore Guttormsen 6252e55700 feat(llm-security): add /security ide-scan — VS Code / JetBrains extension prescan (v6.3.0)
New standalone scanner (prefix IDE) discovers installed VS Code extensions
across forks (Cursor, Windsurf, VSCodium, code-server, Insiders, Remote-SSH)
and runs 7 IDE-specific threat checks: blocklist match (CRITICAL),
theme-with-code, sideload (unsigned .vsix), dangerous uninstall hook (HIGH),
wildcard activation, extension-pack expansion, typosquat (MEDIUM).

Per-extension reuse of UNI/ENT/NET/TNT/MEM/SCR scanners with bounded
concurrency. Offline-first; --online opt-in. JetBrains discovery stubbed
for v1.1. 22 new tests (1296 total, was 1274).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 16:23:35 +02:00

112 lines
3.9 KiB
JavaScript

// ide-extension-parser.mjs — Parse VS Code extension package.json into normalized manifest.
// Zero dependencies (Node.js builtins only).
import { readFile, access } from 'node:fs/promises';
import { join } from 'node:path';
async function pathExists(p) {
try { await access(p); return true; } catch { return false; }
}
/**
* @typedef {object} ParsedManifest
* @property {string} id
* @property {string} publisher
* @property {string} name
* @property {string} version
* @property {object} engines
* @property {string|null} main
* @property {string|null} browser
* @property {string[]} activationEvents
* @property {object} contributes
* @property {string[]} extensionPack
* @property {string[]} extensionDependencies
* @property {string[]} extensionKind
* @property {string[]} categories
* @property {object} capabilities
* @property {object} scripts
* @property {object|string|null} repository
* @property {object} dependencies
* @property {boolean} hasSignature
*/
/**
* Parse a VS Code extension directory.
* @param {string} extRoot - Absolute path to extracted extension root.
* @returns {Promise<{ manifest: ParsedManifest, warnings: string[] } | null>}
*/
export async function parseVSCodeExtension(extRoot) {
const warnings = [];
const pkgPath = join(extRoot, 'package.json');
let raw;
try {
raw = await readFile(pkgPath, 'utf8');
} catch (err) {
return null;
}
let pkg;
try {
pkg = JSON.parse(raw);
} catch (err) {
warnings.push(`malformed package.json at ${pkgPath}: ${err.message}`);
return null;
}
if (!pkg || typeof pkg !== 'object') {
warnings.push(`package.json at ${pkgPath} is not an object`);
return null;
}
const publisher = typeof pkg.publisher === 'string' ? pkg.publisher : '';
const name = typeof pkg.name === 'string' ? pkg.name : '';
const version = typeof pkg.version === 'string' ? pkg.version : '';
if (!publisher || !name) {
warnings.push(`missing publisher/name in ${pkgPath}`);
return null;
}
const hasSignature = await pathExists(join(extRoot, '.signature.p7s'));
const manifest = {
id: `${publisher}.${name}`.toLowerCase(),
publisher: publisher.toLowerCase(),
name: name.toLowerCase(),
version,
engines: pkg.engines && typeof pkg.engines === 'object' ? pkg.engines : {},
main: typeof pkg.main === 'string' ? pkg.main : null,
browser: typeof pkg.browser === 'string' ? pkg.browser : null,
activationEvents: Array.isArray(pkg.activationEvents) ? pkg.activationEvents.filter(e => typeof e === 'string') : [],
contributes: pkg.contributes && typeof pkg.contributes === 'object' ? pkg.contributes : {},
extensionPack: Array.isArray(pkg.extensionPack) ? pkg.extensionPack.filter(e => typeof e === 'string') : [],
extensionDependencies: Array.isArray(pkg.extensionDependencies) ? pkg.extensionDependencies.filter(e => typeof e === 'string') : [],
extensionKind: Array.isArray(pkg.extensionKind) ? pkg.extensionKind.filter(e => typeof e === 'string') : [],
categories: Array.isArray(pkg.categories) ? pkg.categories.filter(c => typeof c === 'string') : [],
capabilities: pkg.capabilities && typeof pkg.capabilities === 'object' ? pkg.capabilities : {},
scripts: pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {},
repository: pkg.repository || null,
dependencies: pkg.dependencies && typeof pkg.dependencies === 'object' ? pkg.dependencies : {},
hasSignature,
};
return { manifest, warnings };
}
/**
* Parse a .vsix file. Stub for v1 — user must extract first.
* @param {string} vsixPath
* @throws {Error}
*/
export async function parseVsixFile(vsixPath) {
throw new Error(`VSIX parsing not implemented in v6.3.0. Extract manually (unzip ${vsixPath}) and pass the extracted directory.`);
}
/**
* Parse an IntelliJ plugin. Stub for v1.1.
* @param {string} pluginRoot
* @returns {Promise<null>}
*/
export async function parseIntelliJPlugin(pluginRoot) {
return null;
}