UNPKG

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
// 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 };