vulnmatter-extension
Version:
VS Code extension for CVE vulnerability analysis using the VulnMatter API with X-API-Key. See CHANGELOG.md for release notes.
1,377 lines (1,211 loc) • 56.8 kB
HTML
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VulnMatter CVE Analysis</title>
<style>
:root {
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--border-radius-sm: 12px;
--border-radius-md: 16px;
--border-radius-lg: 24px;
--border-radius-xl: 28px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.08);
--shadow-md: 0 4px 6px rgba(0,0,0,0.1), 0 2px 4px rgba(0,0,0,0.06);
--shadow-lg: 0 10px 15px rgba(0,0,0,0.1), 0 4px 6px rgba(0,0,0,0.05);
--shadow-xl: 0 20px 25px rgba(0,0,0,0.1), 0 10px 10px rgba(0,0,0,0.04);
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
--material-accent: var(--vscode-textLink-foreground);
--surface-container: var(--vscode-editor-background);
--surface-container-high: var(--vscode-editor-selectionBackground);
}
* {
box-sizing: border-box;
}
body {
font-family: var(--vscode-font-family, 'Roboto', 'Segoe UI', system-ui, -apple-system, sans-serif);
color: var(--vscode-foreground);
background: var(--vscode-editor-background);
padding: var(--spacing-md);
margin: 0;
min-height: 100vh;
line-height: 1.5;
font-weight: 400;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--spacing-md);
animation: fadeIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-xl);
padding: var(--spacing-xl);
background: var(--surface-container);
border-radius: var(--border-radius-xl);
box-shadow: var(--shadow-md);
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--vscode-textLink-foreground), var(--vscode-textLink-activeForeground));
border-radius: var(--border-radius-xl) var(--border-radius-xl) 0 0;
}
.header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
background: linear-gradient(135deg, var(--vscode-textLink-foreground), var(--vscode-textLink-activeForeground));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.lang-switch {
background: var(--surface-container-high);
border: none;
color: var(--vscode-button-secondaryForeground);
border-radius: var(--border-radius-lg);
padding: 12px 20px;
cursor: pointer;
transition: var(--transition);
font-size: 0.9rem;
font-weight: 500;
box-shadow: var(--shadow-sm);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.lang-switch:hover {
background: var(--vscode-button-secondaryHoverBackground);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.section {
margin-bottom: var(--spacing-xl);
background: var(--surface-container);
border-radius: var(--border-radius-lg);
overflow: hidden;
transition: var(--transition);
box-shadow: var(--shadow-sm);
border: none;
}
.section:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.section summary {
color: var(--vscode-textLink-foreground);
font-weight: 600;
font-size: 1.1rem;
padding: var(--spacing-xl);
cursor: pointer;
outline: none;
background: var(--surface-container-high);
transition: var(--transition);
user-select: none;
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.section summary:hover {
background: var(--vscode-list-hoverBackground);
padding-left: calc(var(--spacing-xl) + 4px);
}
.section[open] summary {
background: linear-gradient(135deg, var(--vscode-textLink-foreground), var(--vscode-textLink-activeForeground));
color: var(--vscode-editor-background);
border-radius: 0;
}
.section-content {
padding: var(--spacing-lg);
}
.form-grid {
display: grid;
gap: var(--spacing-lg);
}
.form-group {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-md);
}
@media (max-width: 600px) {
.form-row {
grid-template-columns: 1fr;
}
}
label {
font-weight: 500;
color: var(--vscode-input-foreground);
font-size: 0.9rem;
margin-bottom: var(--spacing-xs);
}
input[type="text"],
input[type="password"],
textarea {
width: 100%;
padding: 16px 20px;
border: 2px solid var(--vscode-input-border);
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border-radius: var(--border-radius-md);
font-size: 1rem;
transition: var(--transition);
line-height: 1.5;
font-weight: 400;
}
input:focus, textarea:focus {
outline: none;
border-color: var(--material-accent);
box-shadow: 0 0 0 3px rgba(var(--material-accent), 0.1);
transform: translateY(-1px);
background-color: var(--surface-container-high);
}
input::placeholder {
color: var(--vscode-input-placeholderForeground);
opacity: 0.8;
}
.status-section {
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-lg) var(--spacing-xl);
background: var(--surface-container-high);
border-radius: var(--border-radius-lg);
margin-bottom: var(--spacing-xl);
box-shadow: var(--shadow-sm);
border: none;
position: relative;
}
.status-indicator {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
}
.status-ok {
background: linear-gradient(135deg, #34d399, #10b981);
box-shadow: 0 4px 12px rgba(52, 211, 153, 0.4);
}
.status-error {
background: linear-gradient(135deg, #f87171, #ef4444);
box-shadow: 0 4px 12px rgba(248, 113, 113, 0.4);
}
.status-indicator::after {
content: '';
position: absolute;
width: 8px;
height: 8px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
top: 4px;
left: 4px;
}
.checkbox-grid {
display: grid;
gap: var(--spacing-md);
margin: var(--spacing-lg) 0;
}
.checkbox-item {
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-lg) var(--spacing-xl);
background: var(--surface-container-high);
border-radius: var(--border-radius-md);
transition: var(--transition);
box-shadow: var(--shadow-sm);
border: none;
position: relative;
overflow: hidden;
}
.checkbox-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: transparent;
transition: var(--transition);
}
.checkbox-item:hover {
background: var(--vscode-list-hoverBackground);
transform: translateX(4px);
box-shadow: var(--shadow-md);
}
.checkbox-item:hover::before {
background: var(--material-accent);
}
.checkbox-item input[type="checkbox"] {
width: 18px;
height: 18px;
margin: 0;
cursor: pointer;
}
.checkbox-label {
flex: 1;
font-weight: 500;
cursor: pointer;
}
.inline-note {
color: var(--vscode-descriptionForeground);
font-size: 0.85rem;
font-style: italic;
}
.button-container {
display: flex;
gap: var(--spacing-md);
align-items: center;
margin-top: var(--spacing-xl);
flex-wrap: wrap;
}
button {
border: none;
border-radius: var(--border-radius-lg);
cursor: pointer;
font-weight: 500;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
min-height: 48px;
font-size: 1rem;
position: relative;
overflow: hidden;
}
.btn-primary {
flex: 1;
min-width: 160px;
background: linear-gradient(135deg, var(--vscode-button-background), var(--vscode-textLink-foreground));
color: var(--vscode-button-foreground);
padding: 16px 32px;
box-shadow: var(--shadow-sm);
font-weight: 600;
}
.btn-primary::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s;
transform: translate(-50%, -50%);
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--vscode-button-hoverBackground), var(--vscode-textLink-activeForeground));
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.btn-primary:hover::before {
width: 300px;
height: 300px;
}
.btn-primary:active {
transform: translateY(0px);
box-shadow: var(--shadow-md);
}
.btn-secondary {
background: var(--surface-container-high);
color: var(--vscode-button-secondaryForeground);
border: 2px solid var(--vscode-input-border);
padding: 12px 16px;
min-width: 48px;
height: 48px;
box-shadow: var(--shadow-sm);
}
.btn-secondary:hover {
background: var(--vscode-button-secondaryHoverBackground);
border-color: var(--material-accent);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-secondary:active {
transform: scale(0.96);
}
.config-info {
background: var(--vscode-textBlockQuote-background);
border-radius: var(--border-radius-md);
margin-top: var(--spacing-xl);
overflow: hidden;
box-shadow: var(--shadow-sm);
border: none;
position: relative;
}
.config-info::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(180deg, var(--vscode-textLink-foreground), var(--vscode-textLink-activeForeground));
}
.config-info summary {
padding: var(--spacing-lg) var(--spacing-xl);
background: var(--surface-container-high);
cursor: pointer;
font-weight: 600;
border: none;
font-size: 1rem;
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.config-info-content {
padding: var(--spacing-xl);
font-size: 0.95rem;
line-height: 1.6;
}
.config-info code {
background: var(--vscode-textPreformat-background);
padding: 4px 8px;
border-radius: var(--border-radius-sm);
font-family: var(--vscode-editor-font-family, 'JetBrains Mono', 'Fira Code', 'Consolas', monospace);
font-size: 0.9rem;
font-weight: 500;
}
.loading {
text-align: center;
padding: var(--spacing-xl);
display: none;
}
.loading-spinner {
font-size: 2rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.results {
display: none;
}
.cve-result {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-md);
margin: var(--spacing-sm) 0;
border-radius: var(--border-radius-md);
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
transition: var(--transition);
}
.cve-result:hover {
transform: translateX(4px);
border-color: var(--vscode-focusBorder);
}
.cve-name {
font-weight: bold;
font-family: var(--vscode-editor-font-family, monospace);
}
.score {
font-weight: bold;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--border-radius-sm);
font-size: 0.85rem;
}
.score.high {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: white;
}
.score.medium {
background: linear-gradient(135deg, #f59e0b, #d97706);
color: white;
}
.score.low {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
}
/* Material Design responsive breakpoints */
@media (max-width: 1200px) {
.container {
max-width: 100%;
padding: 0 var(--spacing-xl);
}
}
@media (max-width: 840px) {
.form-row {
grid-template-columns: 1fr;
gap: var(--spacing-md);
}
.header {
padding: var(--spacing-lg) var(--spacing-xl);
}
}
@media (max-width: 600px) {
body {
padding: var(--spacing-md);
}
.container {
padding: 0;
}
.header {
flex-direction: column;
gap: var(--spacing-lg);
text-align: center;
padding: var(--spacing-xl);
border-radius: var(--border-radius-lg);
}
.header h2 {
font-size: 1.5rem;
}
.button-container {
flex-direction: column;
gap: var(--spacing-md);
}
.btn-primary, .btn-secondary {
width: 100%;
min-width: unset;
}
.section-content {
padding: var(--spacing-lg);
}
.checkbox-item {
padding: var(--spacing-md) var(--spacing-lg);
flex-wrap: wrap;
}
.status-section {
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--border-radius-md);
}
}
@media (max-width: 480px) {
.header {
padding: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
}
.section-content {
padding: var(--spacing-md);
}
.form-grid {
gap: var(--spacing-md);
}
input, textarea, button {
font-size: 16px; /* Prevent zoom on iOS */
padding: 14px 16px;
}
.btn-primary {
padding: 16px 24px;
font-size: 16px;
}
}
/* Enhanced interactions */
.checkbox-item input[type="checkbox"] {
accent-color: var(--vscode-textLink-foreground);
cursor: pointer;
}
.form-group label {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
/* Loading states */
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-primary.loading::after {
content: '';
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: var(--spacing-sm);
}
/* Enhanced textarea */
textarea {
font-family: var(--vscode-editor-font-family, 'Consolas', monospace);
font-size: 0.9rem;
}
/* Improved focus states */
.checkbox-item:has(:focus-visible) {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
button:focus-visible {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
/* Status animations */
.status-indicator {
position: relative;
overflow: hidden;
}
.status-indicator::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
border-radius: 50%;
transform: translate(-50%, -50%);
animation: pulse 2s infinite;
opacity: 0.3;
}
.status-ok::before {
background: #4ade80;
}
.status-error::before {
background: #f87171;
}
@keyframes pulse {
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.3; }
50% { transform: translate(-50%, -50%) scale(1.5); opacity: 0.1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.3; }
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideOut {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(100%);
}
}
/* Notification styles */
.notification {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
border-left: 4px solid rgba(255, 255, 255, 0.3);
}
/* Footer styles */
.footer {
margin-top: var(--spacing-xl);
opacity: 0.8;
transition: var(--transition);
}
.footer:hover {
opacity: 1;
}
/* Material Design hover effects */
input[type="text"]:hover,
input[type="password"]:hover,
textarea:hover {
border-color: var(--material-accent);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.checkbox-item:hover .checkbox-label {
color: var(--material-accent);
font-weight: 600;
}
/* Material Design transitions */
input, button, .checkbox-item, .section {
transition: var(--transition);
}
/* Material Design focus states */
button:focus-visible, input:focus-visible, textarea:focus-visible {
outline: 3px solid var(--material-accent);
outline-offset: 2px;
}
/* Floating action button style for primary buttons */
.btn-primary {
position: relative;
z-index: 1;
}
/* Material ripple effect */
@keyframes ripple {
0% {
transform: scale(0);
opacity: 0.6;
}
100% {
transform: scale(4);
opacity: 0;
}
}
/* Enhanced elevation on interaction */
.section:focus-within {
box-shadow: var(--shadow-lg);
}
/* Loading spinner enhancement */
.loading-spinner {
display: inline-block;
animation: spin 1s linear infinite;
margin-right: var(--spacing-sm);
}
/* Tooltip-like enhancements */
button[title]:hover::after {
content: attr(title);
position: absolute;
background: var(--vscode-editorHoverWidget-background);
color: var(--vscode-editorHoverWidget-foreground);
border: 1px solid var(--vscode-editorHoverWidget-border);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--border-radius-sm);
font-size: 0.8rem;
white-space: nowrap;
z-index: 1000;
margin-top: 4px;
left: 50%;
transform: translateX(-50%);
}
/* Dark mode enhancements */
@media (prefers-color-scheme: dark) {
.section {
border: 1px solid rgba(255,255,255,0.1);
}
.status-ok {
box-shadow: 0 0 12px rgba(74, 222, 128, 0.3);
}
.status-error {
box-shadow: 0 0 12px rgba(248, 113, 113, 0.3);
}
body {
background: linear-gradient(135deg,
var(--vscode-editor-background) 0%,
color-mix(in srgb, var(--vscode-editor-background) 90%, #000) 100%);
}
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<h2 id="t-app-title">� VulnMatter Configuration</h2>
<button id="langSwitchBtn" class="lang-switch" onclick="switchLang()" title="Cambiar idioma / Switch language">
🌐 <span id="current-lang">ES/EN</span>
</button>
</div>
<!-- Main Configuration Section -->
<details class="section config-section" open>
<summary id="t-reqs-title">⚙️ Connection Configuration</summary>
<div class="section-content">
<!-- Status Section -->
<div class="status-section">
<span id="statusIndicator" class="status-indicator status-error"></span>
<div>
<strong id="t-status-label">Status:</strong> <span id="configStatus">Not configured</span>
<div class="inline-note">Configure your API key to begin</div>
</div>
</div>
<div class="form-grid">
<!-- API Configuration -->
<div class="form-group">
<label for="apiKey" id="t-apikey-label">🔑 VulnMatter API Key</label>
<input type="password" id="apiKey" placeholder="Enter your VulnMatter X-API-Key">
</div>
<div class="form-row">
<div class="form-group">
<label for="apiUrl" id="t-apiurl-label">🌐 API URL</label>
<input type="text" id="apiUrl" placeholder="https://api.vulnmatter.com" value="https://api.vulnmatter.com">
</div>
<div class="form-group">
<label for="mcpUrl" id="t-mcpurl-label">🔗 MCP URL (SSE)</label>
<input type="text" id="mcpUrl" placeholder="https://mcp.singularity-matter.com/sse" value="https://mcp.singularity-matter.com/sse">
</div>
</div>
<!-- MCP Configuration Options -->
<div class="checkbox-grid">
<div class="checkbox-item">
<input type="checkbox" id="mcpFsCheckbox" onchange="toggleMcpFs(this)">
<label for="mcpFsCheckbox" class="checkbox-label" id="t-mcp-label">
Configure MCP Filesystem (Claude)
</label>
<a href="https://www.claudemcp.com/servers/filesystem"
style="color:var(--vscode-textLink-foreground);"
target="_blank"
title="Documentation">📖</a>
<span id="mcpFsStatusText" class="inline-note"></span>
</div>
<div class="checkbox-item">
<input type="checkbox" id="mcpVSCodeCheckbox" onchange="toggleMcpVSCode(this)">
<label for="mcpVSCodeCheckbox" class="checkbox-label" id="t-mcpvscode-label">
Install MCP VulnMatter (VS Code)
</label>
<span id="mcpVSCodeStatusText" class="inline-note"></span>
</div>
<div class="checkbox-item">
<input type="checkbox" id="mcpClaudeCheckbox" onchange="toggleMcpClaude(this)">
<label for="mcpClaudeCheckbox" class="checkbox-label" id="t-mcpclaude-label">
Install MCP VulnMatter (Claude Desktop)
</label>
<span id="mcpClaudeStatusText" class="inline-note"></span>
</div>
<div class="checkbox-item">
<input type="checkbox" id="mcpVulnMatterGlobalCheckbox" onchange="toggleMcpVulnMatter(this)">
<label for="mcpVulnMatterGlobalCheckbox" class="checkbox-label">
Enable/Disable VulnMatter (Global)
</label>
</div>
</div>
<div class="button-container">
<button class="btn-primary" onclick="saveApiKey()" id="t-save-btn">
💾 Save Configuration
</button>
<button class="btn-secondary" onclick="showConfigPath()" title="Show config location">
📁
</button>
<button class="btn-secondary" onclick="showMcpConfig()" title="Show MCP configuration">
🔧
</button>
</div>
</div>
<details class="config-info">
<summary id="t-info-title">📌 Configuration Information</summary>
<div class="config-info-content">
<div style="margin-top:8px;">
• X-API-Key saved to: <code id="configPathDisplay">Loading...</code><br>
• User directory: <code id="userHomeDisplay">Loading...</code><br>
• API URL: <code id="apiUrlDisplay">Loading...</code><br>
</div>
</div>
</details>
</div>
</details>
<!-- CVE Analysis Section (Hidden) -->
<details class="section" style="display:none" data-hidden="true">
<summary id="t-analysis-title">🔍 CVE Analysis</summary>
<div class="section-content">
<div class="form-grid">
<div class="form-group">
<label for="cveList" id="t-cves-label">CVEs to analyze (one per line):</label>
<textarea id="cveList"
rows="6"
placeholder="CVE-2023-12345 CVE-2024-67890 CVE-2023-23456"
style="resize: vertical; min-height: 120px;"></textarea>
</div>
<div class="button-container">
<button class="btn-primary" onclick="analyzeCVEs()" id="t-analyze-btn">
🔍 Analyze CVEs
</button>
<button class="btn-secondary" onclick="loadExampleCVEs()" title="Load example CVEs">
📋
</button>
<button class="btn-secondary" onclick="clearResults()" title="Clear results">
🗑️
</button>
</div>
</div>
</div>
</details>
<!-- Report Generation Section (Hidden) -->
<details class="section" style="display:none" data-hidden="true">
<summary id="t-report-title">📊 Generate Report</summary>
<div class="section-content">
<div class="form-grid">
<div class="form-group">
<label for="reportPostfix" id="t-postfix-label">Additional parameters (optional):</label>
<input type="text" id="reportPostfix" placeholder="format=pdf&detailed=true">
</div>
<div class="button-container">
<button class="btn-primary" onclick="generateReport()">📄 Generate Report</button>
</div>
</div>
</div>
</details>
<div id="loading" class="loading">
<div style="font-size: 2em;">⏳</div>
<p id="loadingMessage">Procesando...</p>
</div>
<div id="results" class="results section" style="display:none" data-hidden="true">
<!-- Sección oculta temporalmente: Resultados del Análisis -->
<h3><span class="icon">📋</span>Resultados del Análisis</h3>
<div id="resultsContent"></div>
</div>
<div id="reportResults" class="results section" style="display:none" data-hidden="true">
<!-- Sección oculta temporalmente: Reporte Generado -->
<h3><span class="icon">📄</span>Reporte Generado</h3>
<div id="reportContent"></div>
</div>
<details id="productsSection" class="section" style="display:none" data-hidden="true">
<!-- Sección oculta temporalmente: Productos -->
<summary id="t-products-title"><span class="icon">🛍️</span>Products</summary>
<div style="margin-bottom:8px;">
<button class="btn-primary" onclick="requestProducts()" id="t-refresh-products">🔄 Actualizar Productos</button>
</div>
<div id="productsList">No hay productos cargados.</div>
</details>
</div>
<script>
// Locale toggle helpers (moved from raw text to script to avoid rendering)
let currentLocale = null;
function switchLang() {
// Toggle between 'en' and 'es' - Default is English
currentLocale = (!currentLocale || currentLocale.startsWith('en')) ? 'es' : 'en';
applyLocale(currentLocale);
}
const vscode = acquireVsCodeApi();
// Inline translations to avoid fetch issues inside the Webview sandbox
const translations = {
es: {
appTitle: '🔒 VulnMatter CVE Analysis',
reqsTitle: 'Requisitos',
mcpLabel: 'Configurar MCP Filesystem (Claude)',
mcpVSCodeLabel: 'Instalar MCP VulnMatter (VS Code)',
mcpClaudeLabel: 'Instalar MCP VulnMatter (Claude Desktop)',
statusLabel: 'Estado:',
apiUrlLabel: 'URL API:',
apiKeyLabel: 'X-API-Key de VulnMatter:',
mcpUrlLabel: 'URL MCP (SSE):',
saveBtn: '💾 Guardar Configuración',
infoTitle: '📌 Información',
analysisTitle: 'Análisis de CVEs',
cvesLabel: 'CVEs a analizar (uno por línea):',
analyzeBtn: '🔍 Analizar CVEs',
exampleBtn: '📋 Ejemplo',
clearBtn: '🗑️ Limpiar Resultados',
reportTitle: '📊 Generar Reporte',
postfixLabel: 'Parámetros adicionales (opcional):',
productsTitle: '🛍️ Productos',
refreshProducts: '🔄 Actualizar Productos',
notConfigured: 'No configurada'
},
en: {
appTitle: '🔒 VulnMatter CVE Analysis',
reqsTitle: 'Requirements',
mcpLabel: 'Configure MCP Filesystem (Claude)',
mcpVSCodeLabel: 'Install MCP VulnMatter (VS Code)',
mcpClaudeLabel: 'Install MCP VulnMatter (Claude Desktop)',
statusLabel: 'Status:',
apiUrlLabel: 'API URL:',
apiKeyLabel: 'VulnMatter X-API-Key:',
mcpUrlLabel: 'MCP URL (SSE):',
saveBtn: '💾 Save Configuration',
infoTitle: '📌 Information',
analysisTitle: 'CVE Analysis',
cvesLabel: 'CVEs to analyze (one per line):',
analyzeBtn: '🔍 Analyze CVEs',
exampleBtn: '📋 Example',
clearBtn: '🗑️ Clear Results',
reportTitle: '📊 Generate Report',
postfixLabel: 'Additional parameters (optional):',
productsTitle: '🛍️ Products',
refreshProducts: '🔄 Refresh Products',
notConfigured: 'Not configured'
}
};
function applyLocale(locale) {
const lang = (locale || 'en').startsWith('en') ? 'en' : 'es';
currentLocale = lang;
const t = translations[lang] || translations.es;
const setText = (id, text) => { const el = document.getElementById(id); if (el && text) el.textContent = text; };
setText('t-app-title', t.appTitle);
setText('t-reqs-title', t.reqsTitle);
setText('t-mcp-label', t.mcpLabel);
setText('t-mcpvscode-label', t.mcpVSCodeLabel);
setText('t-mcpclaude-label', t.mcpClaudeLabel);
setText('t-status-label', t.statusLabel);
setText('t-apiurl-label', t.apiUrlLabel);
setText('t-apiurl-label', t.apiUrlLabel);
setText('t-apikey-label', t.apiKeyLabel);
setText('t-mcpurl-label', t.mcpUrlLabel);
setText('t-save-btn', t.saveBtn);
setText('t-info-title', t.infoTitle);
setText('t-analysis-title', t.analysisTitle);
setText('t-cves-label', t.cvesLabel);
setText('t-analyze-btn', t.analyzeBtn);
setText('t-example-btn', t.exampleBtn);
setText('t-clear-btn', t.clearBtn);
setText('t-report-title', t.reportTitle);
setText('t-postfix-label', t.postfixLabel);
setText('t-products-title', t.productsTitle);
setText('t-refresh-products', t.refreshProducts);
// Status inline text value for "not configured"
const cfgStatus = document.getElementById('configStatus');
if (cfgStatus && (!cfgStatus.textContent || cfgStatus.textContent.trim().length === 0 || cfgStatus.textContent === 'No configurada' || cfgStatus.textContent === 'Not configured')) {
cfgStatus.textContent = t.notConfigured;
}
}
// Initialize locale on load
document.addEventListener('DOMContentLoaded', () => {
applyLocale(currentLocale || 'en');
});
function saveApiKey() {
const apiUrl = (document.getElementById('apiUrl').value || '').trim() || 'https://api.vulnmatter.com';
const mcpUrl = (document.getElementById('mcpUrl').value || '').trim() || 'https://mcp.singularity-matter.com/sse';
const apiKey = document.getElementById('apiKey').value.trim();
if (!apiKey) {
showNotification('Please enter your X-API-Key', 'error');
return;
}
// Show loading state
const saveBtn = document.querySelector('[onclick="saveApiKey()"]');
const originalText = saveBtn.textContent;
saveBtn.classList.add('loading');
saveBtn.disabled = true;
saveBtn.textContent = 'Saving...';
vscode.postMessage({
command: 'setApiConfig',
apiKey: apiKey,
apiUrl: apiUrl,
mcpUrl: mcpUrl
});
// Reset button state after a delay
setTimeout(() => {
saveBtn.classList.remove('loading');
saveBtn.disabled = false;
saveBtn.textContent = originalText;
}, 2000);
}
function refreshStatus() {
const statusIndicator = document.getElementById('statusIndicator');
const configStatus = document.getElementById('configStatus');
// Animate refresh
statusIndicator.style.animation = 'spin 1s linear';
configStatus.textContent = 'Refreshing...';
vscode.postMessage({
command: 'refreshStatus'
});
setTimeout(() => {
statusIndicator.style.animation = '';
}, 1000);
}
function showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
color: white;
font-weight: 500;
z-index: 10000;
animation: slideIn 0.3s ease;
${type === 'error' ? 'background: #ef4444;' :
type === 'success' ? 'background: #10b981;' :
'background: var(--vscode-textLink-foreground);'}
`;
document.body.appendChild(notification);
// Auto remove after 3 seconds
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 3000);
}
function showConfigPath() {
vscode.postMessage({
command: 'showConfigPath'
});
}
function showMcpConfig() {
vscode.postMessage({
command: 'showMcpConfig'
});
}
function toggleMcpFs(checkbox) {
const s = document.getElementById('mcpFsStatusText');
if (checkbox.checked) {
if (s) s.textContent = 'Configurando...';
vscode.postMessage({ command: 'configureMcpFs' });
} else {
if (s) s.textContent = 'Deshabilitado (no se elimina automáticamente)';
}
}
function toggleMcpVSCode(checkbox) {
const s = document.getElementById('mcpVSCodeStatusText');
if (checkbox.checked) {
if (s) s.textContent = 'Configurando...';
vscode.postMessage({ command: 'configureMcpVSCode', enabled: true });
} else {
if (s) s.textContent = 'Removiendo...';
vscode.postMessage({ command: 'configureMcpVSCode', enabled: false });
}
}
function toggleMcpClaude(checkbox) {
const s = document.getElementById('mcpClaudeStatusText');
if (checkbox.checked) {
if (s) s.textContent = 'Configurando...';
vscode.postMessage({ command: 'configureMcpClaude', enabled: true });
} else {
if (s) s.textContent = 'Removiendo...';
vscode.postMessage({ command: 'configureMcpClaude', enabled: false });
}
}
function toggleMcpVulnMatter(checkbox) {
if (checkbox.checked) {
vscode.postMessage({ command: 'configureMcpVulnMatter', enabled: true });
} else {
vscode.postMessage({ command: 'configureMcpVulnMatter', enabled: false });
}
}
function requestProducts() {
vscode.postMessage({ command: 'listProducts' });
}
function analyzeCVEs() {
const cveText = document.getElementById('cveList').value;
if (!cveText.trim()) {
alert('Por favor ingresa al menos un CVE');
return;
}
const cves = cveText.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
vscode.postMessage({
command: 'analyzeCVEs',
cves: cves
});
}
function generateReport() {
const cveText = document.getElementById('cveList').value;
const postfix = document.getElementById('reportPostfix').value;
if (!cveText.trim()) {
alert('Por favor ingresa al menos un CVE para el reporte');
return;
}
const cves = cveText.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
vscode.postMessage({
command: 'generateReport',
cves: cves,
postfix: postfix
});
}
function clearResults() {
document.getElementById('results').style.display = 'none';
document.getElementById('reportResults').style.display = 'none';
document.getElementById('cveList').value = '';
document.getElementById('reportPostfix').value = '';
}
function loadExampleCVEs() {
const examples = [
'CVE-2023-44487',
'CVE-2023-38545',
'CVE-2024-21413',
'CVE-2024-3094',
'CVE-2023-4911'
];
document.getElementById('cveList').value = examples.join('\n');
}
function getScoreClass(score) {
if (score >= 7) return 'high';
if (score >= 4) return 'medium';
return 'low';
}
function updateConfigStatus(hasKey, configPath, userHome) {
const statusIndicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('configStatus');
if (hasKey) {
statusIndicator.className = 'status-indicator status-ok';
statusText.textContent = 'X-API-Key configurada ✓';
} else {
statusIndicator.className = 'status-indicator status-error';
statusText.textContent = 'X-API-Key no configurada';
}
if (configPath) {
document.getElementById('configPathDisplay').textContent = configPath;
}
if (userHome) {
document.getElementById('userHomeDisplay').textContent = userHome;
}
}
window.addEventListener('message', event => {
const message = event.data;
switch (message.command) {
case 'initConfig':
if (message.apiKey) {
document.getElementById('apiKey').value = message.apiKey;
}
if (message.apiUrl) {
document.getElementById('apiUrl').value = message.apiUrl;
}
if (message.mcpUrl) {
const mcpEl = document.getElementById('mcpUrl');
if (mcpEl) mcpEl.value = message.mcpUrl;
}
const urlDisp = document.getElementById('apiUrlDisplay');
if (urlDisp) urlDisp.textContent = (document.getElementById('apiUrl').value || 'https://api.vulnmatter.com');
if (message.locale) {
applyLocale(message.locale);
} else {
applyLocale('en');
}
updateConfigStatus(!!message.apiKey, message.configPath, message.userHome);
break;
case 'apiKeySaved':
updateConfigStatus(true, message.configPath);
// Also refresh the info panel URL display from current input value
const urlDisp2 = document.getElementById('apiUrlDisplay');
const urlIn = document.getElementById('apiUrl');
if (urlDisp2 && urlIn) urlDisp2.textContent = urlIn.value.trim() || 'https://api.vulnmatter.com';
showNotification('Configuration saved successfully!', 'success');
break;
case 'mcpFsStatus':
const txt = document.getElementById('mcpFsStatusText');
if (message.configured) {
txt.textContent = `Configured${message.dir ? ' in ' + message.dir : ''}${message.note ? ' (' + message.note + ')' : ''}`;
const cb = document.getElementById('mcpFsCheckbox');
if (cb) cb.checked = true;
if (message.justConfigured) {
showNotification('MCP Filesystem configured successfully!', 'success');
}
} else {
txt.textContent = message.note ? message.note : 'Not configured';
const cb = document.getElementById('mcpFsCheckbox');
if (cb) cb.checked = false;
}
break;
case 'mcpVSCodeStatus':
const txtVS = document.getElementById('mcpVSCodeStatusText');
if (message.configured) {
if (txtVS) txtVS.textContent = `Configured${message.note ? ' (' + message.note + ')' : ''}`;
const cbVS = document.getElementById('mcpVSCodeCheckbox');
if (cbVS) cbVS.checked = true;
if (message.justConfigured) {
showNotification('MCP VulnMatter (VS Code) configured successfully!', 'success');
}
} else {
if (txtVS) txtVS.textContent = message.note ? message.note : 'Not configured';
const cbVS = document.getElementById('mcpVSCodeCheckbox');
if (cbVS) cbVS.checked = false;
}
break;
case 'mcpClaudeStatus':
const txtCl = document.getElementById('mcpClaudeStatusText');
if (message.configured) {
if (txtCl) txtCl.textContent = `Configured${message.note ? ' (' + message.note + ')' : ''}`;