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>
112 lines
3.9 KiB
JavaScript
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;
|
|
}
|