vulnmatter-extension
Version:
VS Code extension for CVE vulnerability analysis using the VulnMatter API with X-API-Key. See CHANGELOG.md for release notes.
607 lines (566 loc) • 28.4 kB
JavaScript
// Clean reconstructed extension.js (previous corrupted/duplicated content removed)
const vscode = require('vscode');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { spawn } = require('child_process');
class VulnMatterWebviewProvider {
constructor(context) {
this.context = context;
this.configPath = this.getConfigPath();
this.productsPath = this.getProductsPath();
}
// ---------------- Basic config read/write ----------------
getConfigPath() {
const userHome = os.homedir();
const dir = path.join(userHome, '.vulnmatter');
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
return path.join(dir, 'config.json');
}
readConfig() {
try {
if (fs.existsSync(this.configPath)) {
const txt = fs.readFileSync(this.configPath, 'utf8');
return txt ? JSON.parse(txt) : {};
}
} catch (e) {
console.warn('Error leyendo config:', e.message);
}
return {};
}
writeConfig(data) {
try { fs.writeFileSync(this.configPath, JSON.stringify(data, null, 2), 'utf8'); } catch (e) { console.error('Error escribiendo config:', e.message); }
}
// ---------------- MCP VulnMatter server upsert/remove ----------------
// apiUrl: REST API base, mcpUrl: SSE endpoint for vulnmatter-mcp-proxy
createOrUpdateVulnMatterServer(apiKey, mcpUrl = 'https://mcp.singularity-matter.com/sse', enabled = true, apiUrl = 'https://api.vulnmatter.com') {
const cfg = this.readConfig();
cfg.servers = cfg.servers || {};
const existing = cfg.servers.VulnMatter || {};
const next = {
type: 'stdio',
command: 'npx',
args: ['-y', 'vulnmatter-mcp-proxy@latest'],
env: {
VULNMATTER_API_KEY: apiKey || existing.env?.VULNMATTER_API_KEY || '',
VULNMATTER_API_URL: apiUrl || existing.env?.VULNMATTER_API_URL || '',
VULNMATTER_MCP_URL: mcpUrl || existing.env?.VULNMATTER_MCP_URL || '',
VM_HEADER: 'X-API-Key',
VM_DEBUG: '1',
VM_SG_DEBUG: '1'
},
gallery: true,
version: '0.0.1',
enabled: !!enabled
};
const changed = JSON.stringify(existing) !== JSON.stringify(next);
cfg.servers.VulnMatter = next;
if (changed) this.writeConfig(cfg);
// Always sync external configs even if unchanged
this.updateVSCodeMcpConfigVulnMatter(next);
this.updateClaudeDesktopConfigVulnMatter(next);
this.prefetchVulnMatterServer();
this.persistConfigPaths();
return { changed };
}
removeVulnMatterServer() {
const cfg = this.readConfig();
if (cfg.servers && (cfg.servers.VulnMatter || cfg.servers.Assents)) {
delete cfg.servers.VulnMatter;
delete cfg.servers.Assents; // legacy
this.writeConfig(cfg);
}
this.removeVSCodeMcpConfigVulnMatter();
this.removeClaudeDesktopConfigVulnMatter();
this.persistConfigPaths();
}
isMcpVulnMatterConfigured() {
// Local config or external sources
const cfg = this.readConfig();
if (cfg.servers && cfg.servers.VulnMatter && cfg.servers.VulnMatter.command === 'npx') return true;
// Check VS Code mcp.json
try {
const vs = this.getVSCodeMcpConfigPath();
if (fs.existsSync(vs)) {
const data = JSON.parse(fs.readFileSync(vs, 'utf8')) || {};
if (data.servers?.VulnMatter?.command === 'npx') return true;
}
} catch (_) {}
// Check Claude
try {
const cp = this.getClaudeDesktopConfigPath();
if (fs.existsSync(cp)) {
const data = JSON.parse(fs.readFileSync(cp, 'utf8')) || {};
for (const root of ['mcpServers', 'servers']) {
if (data[root]?.VulnMatter?.command === 'npx') return true;
}
}
} catch (_) {}
return false;
}
migrateExternalVulnMatterNodes() {
// Simple migration: move Assents -> VulnMatter in external configs
// VS Code
try {
const vs = this.getVSCodeMcpConfigPath();
if (fs.existsSync(vs)) {
const data = JSON.parse(fs.readFileSync(vs, 'utf8')) || {};
if (data.servers?.Assents && !data.servers.VulnMatter) {
data.servers.VulnMatter = data.servers.Assents;
delete data.servers.Assents;
fs.writeFileSync(vs, JSON.stringify(data, null, 2), 'utf8');
}
}
} catch (_) {}
// Claude
try {
const cp = this.getClaudeDesktopConfigPath();
if (fs.existsSync(cp)) {
const data = JSON.parse(fs.readFileSync(cp, 'utf8')) || {};
let changed = false;
for (const root of ['mcpServers', 'servers']) {
if (data[root]?.Assents && !data[root].VulnMatter) {
data[root].VulnMatter = data[root].Assents;
delete data[root].Assents;
changed = true;
}
}
if (changed) fs.writeFileSync(cp, JSON.stringify(data, null, 2), 'utf8');
}
} catch (_) {}
}
prefetchVulnMatterServer() {
try {
const cwd = this.getWorkspaceRoot() || process.cwd();
// Prefetch the vulnmatter-mcp-proxy package so first run is faster
const child = spawn('npx', ['-y', 'vulnmatter-mcp-proxy@latest', '--help'], { cwd, shell: true, windowsHide: true });
setTimeout(() => { try { child.kill(); } catch (_) {} }, 7000);
} catch (_) { /* ignore */ }
}
// --------------- External config updates (VulnMatter) ---------------
updateVSCodeMcpConfigVulnMatter(node) {
try {
const vsPath = this.getVSCodeMcpConfigPath();
const dir = path.dirname(vsPath);
if (!fs.existsSync(vsPath)) {
if (!fs.existsSync(dir)) return; // don't create full directory tree if VS Code not installed
fs.writeFileSync(vsPath, JSON.stringify({ servers: {} }, null, 2), 'utf8');
}
let data = {};
try { data = JSON.parse(fs.readFileSync(vsPath, 'utf8')); } catch { data = { servers: {} }; }
if (!data.servers || typeof data.servers !== 'object') data.servers = {};
data.servers.VulnMatter = { ...node };
fs.writeFileSync(vsPath, JSON.stringify(data, null, 2), 'utf8');
} catch (e) {
console.warn('VS Code MCP update error:', e.message);
}
}
removeVSCodeMcpConfigVulnMatter() {
try {
const vsPath = this.getVSCodeMcpConfigPath();
if (!fs.existsSync(vsPath)) return;
const data = JSON.parse(fs.readFileSync(vsPath, 'utf8')) || {};
if (data.servers) {
delete data.servers.VulnMatter;
delete data.servers.Assents;
fs.writeFileSync(vsPath, JSON.stringify(data, null, 2), 'utf8');
}
} catch (_) {}
}
updateClaudeDesktopConfigVulnMatter(node) {
try {
const cp = this.getClaudeDesktopConfigPath();
if (!fs.existsSync(cp)) return;
let data = {};
try { data = JSON.parse(fs.readFileSync(cp, 'utf8')) || {}; } catch { data = {}; }
const root = data.mcpServers ? 'mcpServers' : (data.servers ? 'servers' : 'mcpServers');
data[root] = data[root] || {};
data[root].VulnMatter = { command: node.command, args: node.args, env: node.env, enabled: node.enabled };
fs.writeFileSync(cp, JSON.stringify(data, null, 2), 'utf8');
} catch (e) {
console.warn('Claude MCP update error:', e.message);
}
}
removeClaudeDesktopConfigVulnMatter() {
try {
const cp = this.getClaudeDesktopConfigPath();
if (!fs.existsSync(cp)) return;
const data = JSON.parse(fs.readFileSync(cp, 'utf8')) || {};
let changed = false;
for (const root of ['mcpServers', 'servers']) {
if (data[root]) {
if (data[root].VulnMatter) { delete data[root].VulnMatter; changed = true; }
if (data[root].Assents) { delete data[root].Assents; changed = true; }
}
}
if (changed) fs.writeFileSync(cp, JSON.stringify(data, null, 2), 'utf8');
} catch (_) {}
}
// ---------------- Filesystem MCP helper methods ----------------
getWorkspaceRoot() {
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) return null;
try {
const extPath = this.context?.extensionPath || '';
if (extPath) {
const norm = path.resolve(extPath);
const match = folders.find(f => {
const root = path.resolve(f.uri.fsPath);
return norm === root || norm.startsWith(root + path.sep);
});
if (match) return match.uri.fsPath;
}
} catch (_) {}
return folders[0].uri.fsPath;
}
getEffectiveRoot() {
return this.context?.extensionPath || this.getWorkspaceRoot() || os.homedir();
}
computeServerNodeName() {
const base = path.basename(this.getEffectiveRoot()).replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase() || 'workspace';
return `filesystem_${base}`;
}
persistConfigPaths() {
try {
const cfg = this.readConfig();
cfg.paths = cfg.paths || {};
cfg.paths.vulnmatterConfig = this.configPath;
cfg.paths.claudeDesktopConfig = this.getClaudeDesktopConfigPath();
cfg.paths.vsCodeMcpConfig = this.getVSCodeMcpConfigPath();
cfg.paths.effectiveRoot = this.getEffectiveRoot();
cfg.paths.serverNodeName = this.computeServerNodeName();
this.writeConfig(cfg);
} catch (e) {
console.warn('Persist paths error:', e.message);
}
}
isMcpFilesystemConfigured() {
try {
const cfg = this.readConfig();
return !!(cfg.servers?.filesystem?.command === 'npx');
} catch (_) { return false; }
}
createMcpFilesystem() {
const cfg = this.readConfig();
cfg.servers = cfg.servers || {};
const root = this.getEffectiveRoot();
cfg.servers.filesystem = { command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem'], env: { ROOT: root } };
this.writeConfig(cfg);
this.persistConfigPaths();
// Prefetch
try {
const prefetchCwd = this.getWorkspaceRoot() || process.cwd();
const child = spawn('npx', ['-y', '@modelcontextprotocol/server-filesystem', '--help'], { cwd: prefetchCwd, shell: true, windowsHide: true });
setTimeout(() => { try { child.kill(); } catch (_) {} }, 8000);
} catch (_) {}
return 'configured';
}
getClaudeDesktopConfigPath() {
const home = os.homedir();
if (process.platform === 'win32') return path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
if (process.platform === 'darwin') return path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
return path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
}
updateClaudeDesktopConfigFilesystem() {
try {
const file = this.getClaudeDesktopConfigPath();
if (!fs.existsSync(file)) return { updated: false, message: 'Claude Desktop no instalado.' };
let data = {}; try { data = JSON.parse(fs.readFileSync(file, 'utf8')) || {}; } catch { return { updated: false, message: 'No se pudo leer config Claude.' }; }
const rootKey = data.mcpServers ? 'mcpServers' : (data.servers ? 'servers' : 'mcpServers');
data[rootKey] = data[rootKey] || {};
const wsRoot = this.getEffectiveRoot();
const serverKey = this.computeServerNodeName();
data[rootKey][serverKey] = { command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem'], env: { ROOT: wsRoot } };
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
return { updated: true, message: `Filesystem configurado (${serverKey}).` };
} catch (e) { return { updated: false, message: e.message }; }
}
getVSCodeMcpConfigPath() {
const home = os.homedir();
if (process.platform === 'win32') {
const appdata = process.env.APPDATA; if (appdata) return path.join(appdata, 'Code', 'User', 'mcp.json');
return path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'mcp.json');
}
if (process.platform === 'darwin') return path.join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
const xdg = process.env.XDG_CONFIG_HOME; if (xdg) return path.join(xdg, 'Code', 'User', 'mcp.json');
return path.join(home, '.config', 'Code', 'User', 'mcp.json');
}
updateVSCodeMcpConfigFilesystem() {
try {
const file = this.getVSCodeMcpConfigPath();
if (!fs.existsSync(file)) return { updated: false, message: 'VS Code MCP no encontrado.' };
let data = {}; try { data = JSON.parse(fs.readFileSync(file, 'utf8')) || {}; } catch { return { updated: false, message: 'No se pudo leer VS Code MCP.' }; }
data.servers = data.servers || {};
const wsRoot = this.getEffectiveRoot();
const serverKey = this.computeServerNodeName();
data.servers[serverKey] = { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem'], env: { ROOT: wsRoot } };
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
return { updated: true, message: `Filesystem configurado (${serverKey}).` };
} catch (e) { return { updated: false, message: e.message }; }
}
// ---------------- Status senders ----------------
sendMcpVSCodeStatus(view) {
let configured = false;
try {
const p = this.getVSCodeMcpConfigPath();
if (fs.existsSync(p)) {
const d = JSON.parse(fs.readFileSync(p, 'utf8')) || {};
configured = !!(d.servers?.VulnMatter?.command === 'npx');
}
} catch (_) {}
view.webview.postMessage({ command: 'mcpVSCodeStatus', configured });
}
sendMcpClaudeStatus(view) {
let configured = false;
try {
const p = this.getClaudeDesktopConfigPath();
if (fs.existsSync(p)) {
const d = JSON.parse(fs.readFileSync(p, 'utf8')) || {};
for (const root of ['mcpServers', 'servers']) {
if (d[root]?.VulnMatter?.command === 'npx') { configured = true; break; }
}
}
} catch (_) {}
view.webview.postMessage({ command: 'mcpClaudeStatus', configured });
}
sendMcpVulnStatus(view) {
this.migrateExternalVulnMatterNodes();
const configured = this.isMcpVulnMatterConfigured();
const { enabled } = this.getLocalVulnMatterState();
view.webview.postMessage({ command: 'mcpVulnStatus', configured, enabled });
}
sendMcpFsStatus(view, note) {
// Basic check: if filesystem server present locally
const cfg = this.readConfig();
const configured = !!cfg.servers?.filesystem;
view.webview.postMessage({ command: 'mcpFsStatus', configured, note, dir: configured ? (cfg.paths?.effectiveRoot) : undefined });
}
sendInitConfig(view) {
try {
const cfg = this.readConfig();
const node = cfg.servers?.VulnMatter;
view.webview.postMessage({
command: 'initConfig',
apiKey: node?.env?.VULNMATTER_API_KEY || '',
apiUrl: node?.env?.VULNMATTER_API_URL || 'https://api.vulnmatter.com',
mcpUrl: node?.env?.VULNMATTER_MCP_URL || 'https://mcp.singularity-matter.com/sse',
configPath: this.configPath,
userHome: os.homedir(),
locale: 'es'
});
} catch (_) {}
}
sendVulnMatterConfig(view) {
try {
const cfg = this.readConfig();
const node = cfg.servers?.VulnMatter;
if (node) {
view.webview.postMessage({
command: 'vulnConfig',
apiKey: node.env?.VULNMATTER_API_KEY || '',
apiUrl: node.env?.VULNMATTER_API_URL || 'https://api.vulnmatter.com',
mcpUrl: node.env?.VULNMATTER_MCP_URL || 'https://mcp.singularity-matter.com/sse',
enabled: !!node.enabled
});
return;
}
} catch (_) {}
view.webview.postMessage({ command: 'vulnConfig', apiKey: '', apiUrl: 'https://api.vulnmatter.com', mcpUrl: 'https://mcp.singularity-matter.com/sse', enabled: true });
}
getLocalVulnMatterState() {
try {
const cfg = this.readConfig();
const node = cfg.servers?.VulnMatter;
if (node) return { exists: true, enabled: !!node.enabled, node };
} catch (_) {}
return { exists: false, enabled: false, node: null };
}
setVulnMatterEnabled(enabled) {
const cfg = this.readConfig();
cfg.servers = cfg.servers || {};
const node = cfg.servers.VulnMatter;
if (!node) return { changed: false, reason: 'not-configured' };
const prev = !!node.enabled;
node.enabled = !!enabled;
cfg.servers.VulnMatter = node;
if (prev !== node.enabled) this.writeConfig(cfg);
// propagate to external configs if exist
this.updateVSCodeMcpConfigVulnMatter(node);
this.updateClaudeDesktopConfigVulnMatter(node);
this.persistConfigPaths();
return { changed: prev !== node.enabled };
}
// ---------------- Products (example persistence) ----------------
getProductsPath() {
const dir = path.join(os.homedir(), '.vulnmatter');
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
return path.join(dir, 'products.json');
}
loadProducts() {
try { if (fs.existsSync(this.productsPath)) return JSON.parse(fs.readFileSync(this.productsPath, 'utf8')) || []; } catch (e) { console.warn('Error loading products:', e.message); }
return [];
}
saveProduct(product) {
try { const list = this.loadProducts(); list.unshift(product); fs.writeFileSync(this.productsPath, JSON.stringify(list, null, 2), 'utf8'); return true; } catch (e) { console.error('Error saving product:', e.message); return false; }
}
// ---------------- Webview integration ----------------
resolveWebviewView(webviewView) {
webviewView.webview.options = { enableScripts: true };
webviewView.webview.html = this.buildHtml();
webviewView.webview.onDidReceiveMessage(msg => this.handleMessage(webviewView, msg));
// initial status
this.sendMcpVulnStatus(webviewView);
}
handleMessage(view, msg) {
switch (msg.command) {
case 'configureVulnMatter': {
const { apiKey, apiUrl, enabled } = msg;
this.createOrUpdateVulnMatterServer(apiKey, apiUrl, enabled);
vscode.window.showInformationMessage('VulnMatter configured.');
this.sendMcpVulnStatus(view);
break; }
case 'removeVulnMatter': {
this.removeVulnMatterServer();
vscode.window.showWarningMessage('VulnMatter configuration removed.');
this.sendMcpVulnStatus(view);
break; }
case 'toggleVulnMatter': {
if (msg.enabled) {
const state = this.getLocalVulnMatterState();
if (!state.exists) {
vscode.window.showInformationMessage('Please configure VulnMatter first (API key required).');
// revert checkbox via status refresh
this.sendMcpVulnStatus(view);
break;
}
this.setVulnMatterEnabled(true);
vscode.window.showInformationMessage('VulnMatter enabled.');
} else {
const state = this.getLocalVulnMatterState();
if (!state.exists) {
this.sendMcpVulnStatus(view); // nothing to disable
break;
}
this.setVulnMatterEnabled(false);
vscode.window.showInformationMessage('VulnMatter disabled (not removed).');
}
this.sendMcpVulnStatus(view);
break; }
case 'configureFilesystem': {
if (!this.isMcpFilesystemConfigured()) this.createMcpFilesystem();
const r1 = this.updateClaudeDesktopConfigFilesystem();
const r2 = this.updateVSCodeMcpConfigFilesystem();
vscode.window.showInformationMessage(`Filesystem: ${r1.message} / ${r2.message}`);
break; }
case 'requestMcpVulnStatus': this.sendMcpVulnStatus(view); break;
case 'requestVulnMatterConfig': this.sendVulnMatterConfig(view); break;
case 'requestMcpFsStatus': this.sendMcpFsStatus(view); break;
case 'requestMcpVSCodeStatus': this.sendMcpVSCodeStatus(view); break;
case 'requestMcpClaudeStatus': this.sendMcpClaudeStatus(view); break;
case 'requestVulnMatterConfig': this.sendVulnMatterConfig(view); break; // kept for new minimal UI compatibility
case 'setApiConfig': {
const { apiKey, apiUrl, mcpUrl } = msg;
if (!apiKey) { vscode.window.showWarningMessage('API Key missing'); break; }
this.createOrUpdateVulnMatterServer(apiKey, mcpUrl || 'https://mcp.singularity-matter.com/sse', true, apiUrl || 'https://api.vulnmatter.com');
view.webview.postMessage({ command: 'apiKeySaved', configPath: this.configPath });
this.sendMcpVulnStatus(view);
break; }
case 'showConfigPath': {
vscode.window.showInformationMessage(`VulnMatter config: ${this.configPath}`);
break; }
case 'showMcpConfig': {
vscode.window.showInformationMessage(`VS Code MCP: ${this.getVSCodeMcpConfigPath()} / Claude MCP: ${this.getClaudeDesktopConfigPath()}`);
break; }
case 'configureMcpFs': {
if (!this.isMcpFilesystemConfigured()) {
this.createMcpFilesystem();
}
this.updateClaudeDesktopConfigFilesystem();
this.updateVSCodeMcpConfigFilesystem();
this.sendMcpFsStatus(view, 'updated');
break; }
case 'configureMcpVSCode': {
const cfg = this.readConfig();
let node = cfg.servers?.VulnMatter;
if (msg.enabled) {
if (!node) {
// Allow direct install providing apiKey/mcpUrl from webview
const apiKey = msg.apiKey;
if (!apiKey) {
vscode.window.showWarningMessage('Enter API Key first (use Save or provide it before installing).');
this.sendMcpVSCodeStatus(view); // revert checkbox
break;
}
this.createOrUpdateVulnMatterServer(apiKey, msg.mcpUrl || 'https://mcp.singularity-matter.com/sse', true, msg.apiUrl || 'https://api.vulnmatter.com');
node = this.readConfig().servers?.VulnMatter;
}
if (node) this.updateVSCodeMcpConfigVulnMatter(node);
} else {
this.removeVSCodeMcpConfigVulnMatter();
}
this.sendMcpVSCodeStatus(view);
break; }
case 'configureMcpClaude': {
const cfg = this.readConfig();
let node = cfg.servers?.VulnMatter;
if (msg.enabled) {
if (!node) {
const apiKey = msg.apiKey;
if (!apiKey) {
vscode.window.showWarningMessage('Enter API Key first (use Save or provide it before installing).');
this.sendMcpClaudeStatus(view);
break;
}
this.createOrUpdateVulnMatterServer(apiKey, msg.mcpUrl || 'https://mcp.singularity-matter.com/sse', true, msg.apiUrl || 'https://api.vulnmatter.com');
node = this.readConfig().servers?.VulnMatter;
}
if (node) this.updateClaudeDesktopConfigVulnMatter(node);
} else {
this.removeClaudeDesktopConfigVulnMatter();
}
this.sendMcpClaudeStatus(view);
break; }
case 'configureMcpVulnMatter': {
if (msg.enabled) {
const cfg = this.readConfig();
const existing = cfg.servers?.VulnMatter;
if (existing) {
this.setVulnMatterEnabled(true);
} else {
vscode.window.showWarningMessage('Configure API Key first.');
}
} else {
// full removal requested by legacy UI toggle off
this.removeVulnMatterServer();
}
this.sendMcpVulnStatus(view);
break; }
case 'listProducts': {
const products = this.loadProducts();
view.webview.postMessage({ command: 'productsList', products });
break; }
case 'analyzeCVEs': {
vscode.window.showInformationMessage('CVE analysis functionality not yet restored in this version.');
break; }
case 'generateReport': {
vscode.window.showInformationMessage('Report generation not yet restored.');
break; }
}
}
buildHtml() {
try {
const file = path.join(this.context.extensionPath, 'media', 'vulnmatter.html');
if (fs.existsSync(file)) {
return fs.readFileSync(file, 'utf8');
}
} catch (_) {}
// Fallback minimal UI if file missing
return `<html><body><h3>VulnMatter UI file missing</h3><p>Could not locate media/vulnmatter.html.</p></body></html>`;
}
}
function activate(context) {
const provider = new VulnMatterWebviewProvider(context);
context.subscriptions.push(vscode.window.registerWebviewViewProvider('vulnmatterView', provider));
}
function deactivate() {}
module.exports = { activate, deactivate };