harbor-security/harbor_security_local.html

875 lines
46 KiB
HTML

<!DOCTYPE html>
<html lang="es" class="h-full bg-slate-950 text-slate-100">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Harbor Security Portal - Inspector de Vulnerabilidades y SBOM</title>
<!-- Tailwind CSS -->
<script src="assets/js/tailwindcss.js"></script>
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="assets/css/fontawesome.all.min.css">
<!-- Google Fonts -->
<!-- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> -->
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['system-ui', '-apple-system', 'sans-serif'],
mono: ['ui-monospace', 'SFMono-Regular', 'Consolas', 'monospace'],
},
colors: {
harbor: {
bg: '#0F172A',
card: '#1E293B',
border: '#334155',
accent: '#0EA5E9',
accentHover: '#0284C7',
}
}
}
}
}
</script>
<style>
/* Scrollbar personalizado para tema oscuro */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #0f172a;
}
::-webkit-scrollbar-thumb {
background: #334155;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #475569;
}
</style>
</head>
<body class="h-full font-sans antialiased bg-slate-950 text-slate-200 flex flex-col">
<!-- Encabezado y Navegación Principal -->
<header class="bg-slate-900 border-b border-slate-800 px-6 py-4 flex flex-wrap items-center justify-between gap-4 sticky top-0 z-30 shadow-lg">
<div class="flex items-center space-x-4">
<div class="bg-sky-500/10 p-2.5 rounded-lg border border-sky-500/20 text-sky-400">
<i class="fa-solid fa-shield-halved text-2xl"></i>
</div>
<div>
<h1 class="text-xl font-bold tracking-tight text-white flex items-center gap-2">
Portal de Seguridad Harbor
<span id="version-tag" class="text-xs font-normal px-2.5 py-0.5 rounded-full bg-sky-500/10 text-sky-400 border border-sky-500/20">API REST v2.0</span>
</h1>
<p class="text-xs text-slate-400">Inspector Centralizado de Vulnerabilidades (CVEs) y SBOMs</p>
</div>
</div>
<!-- Acciones y Estado de Conexión -->
<div class="flex items-center space-x-3">
<div id="connection-badge" class="flex items-center space-x-2 bg-amber-500/10 border border-amber-500/20 text-amber-400 px-3 py-1.5 rounded-md text-xs font-medium">
<span id="connection-dot" class="w-2 h-2 rounded-full bg-amber-400 animate-pulse"></span>
<span id="connection-status-text">Modo Simulación (Demo)</span>
</div>
<button onclick="syncWithHarbor()" title="Sincronizar Datos de Harbor" class="flex items-center space-x-2 bg-sky-600 hover:bg-sky-500 text-white px-3.5 py-2 rounded-lg text-sm font-medium transition duration-150 shadow-md shadow-sky-600/20">
<i id="sync-icon" class="fa-solid fa-arrows-rotate"></i>
<span class="hidden md:inline">Sincronizar API</span>
</button>
<button onclick="openConfigModal()" class="flex items-center space-x-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 px-3.5 py-2 rounded-lg text-sm font-medium transition duration-150">
<i class="fa-solid fa-gear text-slate-400"></i>
<span class="hidden md:inline">Ajustes de API</span>
</button>
<button onclick="exportData('csv')" class="flex items-center space-x-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 px-3.5 py-2 rounded-lg text-sm font-medium transition duration-150">
<i class="fa-solid fa-file-csv text-emerald-400"></i>
<span class="hidden md:inline">Exportar CSV</span>
</button>
<button onclick="exportData('json')" class="flex items-center space-x-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 px-3.5 py-2 rounded-lg text-sm font-medium transition duration-150">
<i class="fa-solid fa-code text-sky-400"></i>
<span class="hidden md:inline">Exportar JSON</span>
</button>
</div>
</header>
<!-- Contenido Principal -->
<main class="flex-1 w-full px-4 sm:px-6 lg:px-8 py-6 space-y-6">
<!-- Métricas Rápidas -->
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
<div class="bg-slate-900 border border-slate-800 p-4 rounded-xl flex items-center justify-between">
<div>
<p class="text-xs text-slate-400 uppercase tracking-wider font-semibold">Total CVEs</p>
<p id="count-total" class="text-2xl font-bold text-white mt-1">0</p>
</div>
<div class="p-3 bg-slate-800 rounded-lg text-slate-400">
<i class="fa-solid fa-bug text-xl"></i>
</div>
</div>
<div onclick="quickFilterSeverity('CRITICAL')" class="cursor-pointer bg-slate-900 border border-red-500/20 hover:border-red-500/40 p-4 rounded-xl flex items-center justify-between transition">
<div>
<p class="text-xs text-red-400 uppercase tracking-wider font-semibold">Críticas</p>
<p id="count-critical" class="text-2xl font-bold text-red-400 mt-1">0</p>
</div>
<div class="p-3 bg-red-500/10 rounded-lg text-red-400">
<i class="fa-solid fa-triangle-exclamation text-xl"></i>
</div>
</div>
<div onclick="quickFilterSeverity('HIGH')" class="cursor-pointer bg-slate-900 border border-orange-500/20 hover:border-orange-500/40 p-4 rounded-xl flex items-center justify-between transition">
<div>
<p class="text-xs text-orange-400 uppercase tracking-wider font-semibold">Altas</p>
<p id="count-high" class="text-2xl font-bold text-orange-400 mt-1">0</p>
</div>
<div class="p-3 bg-orange-500/10 rounded-lg text-orange-400">
<i class="fa-solid fa-radiation text-xl"></i>
</div>
</div>
<div onclick="quickFilterSeverity('MEDIUM')" class="cursor-pointer bg-slate-900 border border-yellow-500/20 hover:border-yellow-500/40 p-4 rounded-xl flex items-center justify-between transition">
<div>
<p class="text-xs text-yellow-400 uppercase tracking-wider font-semibold">Medias</p>
<p id="count-medium" class="text-2xl font-bold text-yellow-400 mt-1">0</p>
</div>
<div class="p-3 bg-yellow-500/10 rounded-lg text-yellow-400">
<i class="fa-solid fa-circle-exclamation text-xl"></i>
</div>
</div>
<div onclick="quickFilterSeverity('LOW')" class="cursor-pointer bg-slate-900 border border-blue-500/20 hover:border-blue-500/40 p-4 rounded-xl flex items-center justify-between transition col-span-2 md:col-span-1">
<div>
<p class="text-xs text-blue-400 uppercase tracking-wider font-semibold">Bajas</p>
<p id="count-low" class="text-2xl font-bold text-blue-400 mt-1">0</p>
</div>
<div class="p-3 bg-blue-500/10 rounded-lg text-blue-400">
<i class="fa-solid fa-circle-info text-xl"></i>
</div>
</div>
</div>
<!-- Tarjeta de Filtros idéntica a la captura de Harbor Interrogation Services -->
<div class="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-sm space-y-4">
<h2 class="text-lg font-semibold text-white flex items-center gap-2 border-b border-slate-800 pb-3">
<i class="fa-solid fa-filter text-sky-400"></i> Búsqueda y Filtrado de Vulnerabilidades
</h2>
<div class="flex flex-col md:flex-row items-stretch md:items-center gap-4">
<!-- Desplegable Filter by -->
<div class="w-full md:w-64 flex-shrink-0">
<label class="block text-xs font-medium text-slate-400 mb-1">Filter by <i class="fa-solid fa-circle-info text-slate-500" title="Selecciona la categoría del filtro"></i></label>
<select id="filter-type" class="w-full bg-slate-800 border border-slate-700 text-slate-200 text-sm rounded-lg p-2.5 focus:ring-2 focus:ring-sky-500 focus:border-sky-500 outline-none">
<option value="All">All</option>
<option value="CVE ID">CVE ID</option>
<option value="Severity">Severity</option>
<option value="CVSS3">CVSS3</option>
<option value="Project Name">Project Name</option>
<option value="Repository Name">Repository Name</option>
<option value="Package">Package</option>
<option value="Tag">Tag</option>
<option value="Digest">Digest</option>
</select>
</div>
<!-- Input de consulta textual -->
<div class="flex-1">
<label class="block text-xs font-medium text-slate-400 mb-1">Término de Búsqueda</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-slate-500">
<i class="fa-solid fa-magnifying-glass"></i>
</div>
<input type="text" id="search-query" onkeyup="handleKeySearch(event)" placeholder="Ej. netty-tcnative, CVE-2023-3446, log4j, calidad..." class="w-full bg-slate-800 border border-slate-700 text-slate-100 text-sm rounded-lg pl-10 p-2.5 focus:ring-2 focus:ring-sky-500 focus:border-sky-500 outline-none">
</div>
</div>
<!-- Botones de Acción -->
<div class="flex items-end gap-2 pt-5 md:pt-0">
<button onclick="applyFilters()" class="flex-1 md:flex-initial bg-sky-500 hover:bg-sky-600 text-white font-semibold text-sm px-6 py-2.5 rounded-lg flex items-center justify-center gap-2 transition duration-150 shadow-md shadow-sky-500/10">
<i class="fa-solid fa-magnifying-glass"></i>
<span>SEARCH</span>
</button>
<button onclick="resetFilters()" title="Reiniciar Filtros" class="bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 p-2.5 rounded-lg text-sm transition">
<i class="fa-solid fa-rotate-left"></i>
</button>
</div>
</div>
</div>
<!-- Tabla de Datos -->
<div class="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-lg">
<div class="p-4 border-b border-slate-800 flex justify-between items-center bg-slate-900/50">
<span class="text-sm font-medium text-slate-400">
Mostrando <span id="results-count" class="text-white font-bold">0</span> registros
</span>
<span id="last-sync-time" class="text-xs text-slate-500">Sin sincronizar</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left text-sm text-slate-300">
<thead class="bg-slate-950 text-xs uppercase text-slate-400 font-semibold border-b border-slate-800 tracking-wider">
<tr>
<th class="px-4 py-3.5">CVE ID</th>
<th class="px-4 py-3.5">Severidad</th>
<th class="px-4 py-3.5">Paquete Afectado</th>
<th class="px-4 py-3.5">Proyecto</th>
<th class="px-4 py-3.5">Repositorio</th>
<th class="px-4 py-3.5">Tag / Digest</th>
<th class="px-4 py-3.5 text-center">CVSS3</th>
<th class="px-4 py-3.5 text-right">Acciones</th>
</tr>
</thead>
<tbody id="vulnerabilities-tbody" class="divide-y divide-slate-800/60">
<!-- Se inyecta dinámicamente -->
</tbody>
</table>
</div>
<!-- Estado Vacío (Coincide con captura de pantalla) -->
<div id="empty-state" class="hidden py-16 text-center space-y-4">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-slate-800/80 border border-slate-700 text-slate-400 mb-2">
<i class="fa-solid fa-filter text-2xl"></i>
</div>
<h3 class="text-lg font-medium text-slate-300">We could not find any vulnerability</h3>
<p class="text-sm text-slate-500 max-w-md mx-auto">No se han encontrado vulnerabilidades con los criterios especificados o las imágenes no han sido escaneadas aún.</p>
</div>
</div>
</main>
<!-- Modal de Configuración de la API de Harbor -->
<div id="config-modal" class="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-sm hidden flex items-center justify-center p-4">
<div class="bg-slate-900 border border-slate-800 rounded-xl w-full max-w-lg shadow-2xl p-6 space-y-5">
<div class="flex justify-between items-center border-b border-slate-800 pb-3">
<h3 class="text-lg font-bold text-white flex items-center gap-2">
<i class="fa-solid fa-server text-sky-400"></i> Configuración de la API de Harbor
</h3>
<button onclick="closeConfigModal()" class="text-slate-400 hover:text-white text-lg">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-xs font-semibold text-slate-400 mb-1">URL / URI Base de Harbor</label>
<input type="url" id="config-harbor-url" placeholder="https://harbor.tu-empresa.com" class="w-full bg-slate-800 border border-slate-700 text-slate-100 text-sm rounded-lg p-2.5 focus:ring-2 focus:ring-sky-500 outline-none font-mono">
<p class="text-[11px] text-slate-500 mt-1">Dirección del servidor donde corre Harbor REST API v2.0</p>
</div>
<div>
<label class="block text-xs font-semibold text-slate-400 mb-1">Usuario / Robot Account</label>
<input type="text" id="config-harbor-user" placeholder="robot$security-scanner" class="w-full bg-slate-800 border border-slate-700 text-slate-100 text-sm rounded-lg p-2.5 focus:ring-2 focus:ring-sky-500 outline-none font-mono">
</div>
<div>
<label class="block text-xs font-semibold text-slate-400 mb-1">Contraseña / Secret Token</label>
<input type="password" id="config-harbor-token" placeholder="••••••••••••••••••••" class="w-full bg-slate-800 border border-slate-700 text-slate-100 text-sm rounded-lg p-2.5 focus:ring-2 focus:ring-sky-500 outline-none font-mono">
</div>
<!-- Opción de Guardar Credenciales -->
<div class="flex items-center space-x-2 pt-2">
<input type="checkbox" id="config-remember" class="w-4 h-4 rounded bg-slate-800 border-slate-700 text-sky-500 focus:ring-sky-500">
<label for="config-remember" class="text-xs text-slate-300">Guardar credenciales en el navegador (localStorage)</label>
</div>
<div class="bg-slate-800/60 p-3 rounded-lg border border-slate-700/60 text-xs text-slate-400 space-y-1">
<p class="font-medium text-slate-300 flex items-center gap-1"><i class="fa-solid fa-shield-cat text-sky-400"></i> Fallback Inteligente:</p>
<p>Si la URL se deja en blanco o la API no es accesible (ej. políticas de CORS del servidor Harbor), el portal cargará automáticamente datos de demostración con vulnerabilidades reales (como <code class="text-sky-300 font-mono">netty-tcnative</code>, <code class="text-sky-300 font-mono">log4j</code>, <code class="text-sky-300 font-mono">openssl</code>).</p>
</div>
</div>
<div class="flex justify-between items-center border-t border-slate-800 pt-4">
<button onclick="clearConfig()" class="text-xs text-red-400 hover:text-red-300 font-medium">Limpiar Credenciales</button>
<div class="flex gap-3">
<button onclick="closeConfigModal()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm font-medium transition">Cancelar</button>
<button onclick="saveConfig()" class="px-5 py-2 bg-sky-500 hover:bg-sky-600 text-white rounded-lg text-sm font-semibold transition flex items-center gap-2">
<i class="fa-solid fa-floppy-disk"></i>
<span>Guardar y Conectar</span>
</button>
</div>
</div>
</div>
</div>
<!-- Modal para Inspeccionar CVE individual -->
<div id="cve-modal" class="fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-sm hidden flex items-center justify-center p-4">
<div class="bg-slate-900 border border-slate-800 rounded-xl w-full max-w-2xl shadow-2xl p-6 space-y-5">
<div class="flex justify-between items-start border-b border-slate-800 pb-3">
<div>
<div class="flex items-center gap-3">
<h3 id="modal-cve-id" class="text-xl font-mono font-bold text-sky-400">CVE-0000-0000</h3>
<span id="modal-cve-badge" class="px-2.5 py-0.5 rounded text-xs font-bold">CRITICAL</span>
</div>
<p id="modal-cve-package" class="text-xs text-slate-400 mt-1">Paquete: -</p>
</div>
<button onclick="closeCveModal()" class="text-slate-400 hover:text-white text-lg">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="space-y-4 text-sm text-slate-300">
<div class="grid grid-cols-2 gap-4 bg-slate-800/40 p-3 rounded-lg border border-slate-800">
<div>
<span class="text-xs text-slate-500 block">Puntuación CVSS v3</span>
<span id="modal-cve-cvss" class="font-mono text-base font-bold text-white">9.8</span>
</div>
<div>
<span class="text-xs text-slate-500 block">Versión de Corrección (Fix)</span>
<span id="modal-cve-fixed" class="font-mono text-base font-bold text-emerald-400">2.0.59.Final</span>
</div>
</div>
<div>
<h4 class="text-xs uppercase font-semibold text-slate-400 mb-1">Descripción</h4>
<p id="modal-cve-desc" class="bg-slate-950 p-3 rounded-lg border border-slate-800 text-xs leading-relaxed text-slate-300 font-sans">
Detalles descriptivos...
</p>
</div>
<div>
<h4 class="text-xs uppercase font-semibold text-slate-400 mb-1">Ubicación del Artefacto en Harbor</h4>
<div id="modal-cve-target" class="font-mono text-xs bg-slate-950 p-2.5 rounded border border-slate-800 text-slate-300 space-y-1">
<!-- Ruta del artefacto -->
</div>
</div>
</div>
<div class="flex justify-end border-t border-slate-800 pt-4">
<button onclick="closeCveModal()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm font-medium">Cerrar</button>
</div>
</div>
</div>
<!-- Lógica JavaScript Principal de la Aplicación -->
<script>
// Datos Simulados (Fallback en caso de no haber conexión con servidor Harbor)
const MOCK_DATA = [
{
cve_id: "CVE-2021-43797",
severity: "CRITICAL",
package: "netty-tcnative",
version: "2.0.36.Final",
fixed_version: "2.0.46.Final",
project: "calidad",
repository: "artefactory/java-service",
tag: "v1.4.2",
digest: "sha256:e8a3a921d1982b1c",
cvss3: 9.8,
description: "Vulnerabilidad en Netty-tcnative anterior a 2.0.46.Final que permite lectura fuera de límites durante la verificación de certificados SSL, provocando fuga de información o ejecución remota."
},
{
cve_id: "CVE-2023-3446",
severity: "HIGH",
package: "netty-tcnative",
version: "2.0.50.Final",
fixed_version: "2.0.61.Final",
project: "produccion",
repository: "core/payment-api",
tag: "2.3.0",
digest: "sha256:8b37f191a273e911",
cvss3: 7.5,
description: "Evaluación excesiva de parámetros Diffie-Hellman en comprobaciones de claves en netty-tcnative ocasionando Denegación de Servicio (DoS)."
},
{
cve_id: "CVE-2021-44228",
severity: "CRITICAL",
package: "log4j-core",
version: "2.14.1",
fixed_version: "2.17.1",
project: "sistemas",
repository: "backend/auth-service",
tag: "latest",
digest: "sha256:3f18b528a011ef92",
cvss3: 10.0,
description: "Ejecución remota de código arbitrario a través de JNDI en Apache Log4j2 (Log4Shell)."
},
{
cve_id: "CVE-2023-0286",
severity: "HIGH",
package: "openssl",
version: "1.1.1t-r0",
fixed_version: "1.1.1u-r0",
project: "produccion",
repository: "infrastructure/nginx-proxy",
tag: "v1.24",
digest: "sha256:72a19ff21034bc98",
cvss3: 7.4,
description: "Confusión de tipos en GENERAL_NAME_cmp al procesar puntos de distribución CRL en OpenSSL."
},
{
cve_id: "CVE-2022-43551",
severity: "MEDIUM",
package: "curl",
version: "7.87.0-r0",
fixed_version: "7.87.1-r0",
project: "desarrollo",
repository: "devops/runner-base",
tag: "ubuntu-22.04",
digest: "sha256:a12e4409c551af00",
cvss3: 5.3,
description: "Límite de longitud no respetado en análisis de cabeceras HTTP en libcurl provocando fallo del proceso."
},
{
cve_id: "CVE-2023-29406",
severity: "MEDIUM",
package: "go",
version: "1.20.4",
fixed_version: "1.20.6",
project: "calidad",
repository: "tools/metrics-collector",
tag: "v0.9.1",
digest: "sha256:90ce2b11f321cc88",
cvss3: 6.5,
description: "Agotamiento de memoria por tramas de control HTTP/2 continuas en paquete net/http de Go."
},
{
cve_id: "CVE-2022-37434",
severity: "CRITICAL",
package: "zlib",
version: "1.2.11",
fixed_version: "1.2.12",
project: "sistemas",
repository: "artefactory/build-agent",
tag: "jdk17",
digest: "sha256:550c18fa24098ea1",
cvss3: 9.8,
description: "Desbordamiento de búfer en inflate dentro de zlib durante la descompresión de ficheros maliciosos."
}
];
// Estado global de la app
let vulnerabilities = [...MOCK_DATA];
let filteredVulnerabilities = [...MOCK_DATA];
let harborConfig = {
url: '',
user: '',
token: '',
remember: false
};
// Inicialización al cargar el DOM
document.addEventListener('DOMContentLoaded', () => {
loadSavedConfig();
if (harborConfig.url) {
syncWithHarbor();
} else {
renderTable(filteredVulnerabilities);
updateCounters(filteredVulnerabilities);
}
});
// Cargar configuración guardada desde localStorage
function loadSavedConfig() {
const savedUrl = localStorage.getItem('harbor_url') || '';
const savedUser = localStorage.getItem('harbor_user') || '';
const savedToken = localStorage.getItem('harbor_token') || '';
const savedRemember = localStorage.getItem('harbor_remember') === 'true';
harborConfig = {
url: savedUrl,
user: savedUser,
token: savedToken,
remember: savedRemember
};
document.getElementById('config-harbor-url').value = savedUrl;
document.getElementById('config-harbor-user').value = savedUser;
document.getElementById('config-harbor-token').value = savedToken;
document.getElementById('config-remember').checked = savedRemember;
updateConnectionBadge(false, savedUrl ? 'Configurado' : 'Modo Simulación');
}
// Guardar configuración en modal
function saveConfig() {
const url = document.getElementById('config-harbor-url').value.trim().replace(/\/+$/, '');
const user = document.getElementById('config-harbor-user').value.trim();
const token = document.getElementById('config-harbor-token').value.trim();
const remember = document.getElementById('config-remember').checked;
harborConfig = { url, user, token, remember };
if (remember) {
localStorage.setItem('harbor_url', url);
localStorage.setItem('harbor_user', user);
localStorage.setItem('harbor_token', token);
localStorage.setItem('harbor_remember', 'true');
} else {
clearLocalStorageCredentials();
}
closeConfigModal();
syncWithHarbor();
}
function clearConfig() {
clearLocalStorageCredentials();
document.getElementById('config-harbor-url').value = '';
document.getElementById('config-harbor-user').value = '';
document.getElementById('config-harbor-token').value = '';
document.getElementById('config-remember').checked = false;
harborConfig = { url: '', user: '', token: '', remember: false };
vulnerabilities = [...MOCK_DATA];
filteredVulnerabilities = [...MOCK_DATA];
renderTable(filteredVulnerabilities);
updateCounters(filteredVulnerabilities);
updateConnectionBadge(false, 'Modo Simulación (Demo)');
closeConfigModal();
}
function clearLocalStorageCredentials() {
localStorage.removeItem('harbor_url');
localStorage.removeItem('harbor_user');
localStorage.removeItem('harbor_token');
localStorage.removeItem('harbor_remember');
}
// Sincronización real con la API v2.0 de Harbor
async function syncWithHarbor() {
const syncIcon = document.getElementById('sync-icon');
syncIcon.classList.add('fa-spin');
if (!harborConfig.url) {
console.log('No hay URL configurada. Cargando datos de prueba.');
vulnerabilities = [...MOCK_DATA];
filteredVulnerabilities = [...MOCK_DATA];
renderTable(filteredVulnerabilities);
updateCounters(filteredVulnerabilities);
updateConnectionBadge(false, 'Modo Simulación (Demo)');
syncIcon.classList.remove('fa-spin');
document.getElementById('last-sync-time').textContent = 'Modo Simulación Activo';
return;
}
updateConnectionBadge(false, 'Conectando...', 'animate-pulse');
try {
const headers = new Headers();
headers.append('Accept', 'application/json');
if (harborConfig.user && harborConfig.token) {
const credentials = btoa(`${harborConfig.user}:${harborConfig.token}`);
headers.append('Authorization', `Basic ${credentials}`);
}
// 1. Obtener lista de proyectos
const projectsResponse = await fetch(`${harborConfig.url}/api/v2.0/projects?page_size=15`, { headers });
if (!projectsResponse.ok) throw new Error(`HTTP ${projectsResponse.status}: Error al obtener proyectos`);
const projects = await projectsResponse.json();
let fetchedVulns = [];
// 2. Recorrer repositorios de cada proyecto
for (const proj of projects) {
try {
const reposResp = await fetch(`${harborConfig.url}/api/v2.0/projects/${proj.name}/repositories?page_size=10`, { headers });
if (!reposResp.ok) continue;
const repos = await reposResp.json();
for (const repo of repos) {
const repoShortName = repo.name.replace(`${proj.name}/`, '');
// 3. Obtener artefactos
const artifactsResp = await fetch(`${harborConfig.url}/api/v2.0/projects/${proj.name}/repositories/${repoShortName}/artifacts?page_size=5`, { headers });
if (!artifactsResp.ok) continue;
const artifacts = await artifactsResp.json();
for (const art of artifacts) {
// 4. Consultar adiciones/vulnerabilidades de cada artefacto
const vulnResp = await fetch(`${harborConfig.url}/api/v2.0/projects/${proj.name}/repositories/${repoShortName}/artifacts/${art.digest}/additions/vulnerabilities`, { headers });
if (!vulnResp.ok) continue;
const vulnsData = await vulnResp.json();
const reportKey = Object.keys(vulnsData)[0];
if (reportKey && vulnsData[reportKey] && vulnsData[reportKey].vulnerabilities) {
const items = vulnsData[reportKey].vulnerabilities.map(v => ({
cve_id: v.id || 'N/A',
severity: (v.severity || 'UNKNOWN').toUpperCase(),
package: v.package || 'Desconocido',
version: v.version || '-',
fixed_version: v.fix_version || 'N/A',
project: proj.name,
repository: repo.name,
tag: art.tags && art.tags.length > 0 ? art.tags[0].name : 'latest',
digest: art.digest || 'N/A',
cvss3: v.preferred_cvss ? v.preferred_cvss.scoreV3 || 0 : (v.severity === 'CRITICAL' ? 9.5 : 7.0),
description: v.description || 'Sin descripción provista por el escaner de Harbor.'
}));
fetchedVulns.push(...items);
}
}
}
} catch (e) {
console.warn(`Error procesando proyecto ${proj.name}:`, e);
}
}
if (fetchedVulns.length > 0) {
vulnerabilities = fetchedVulns;
} else {
console.log('No se encontraron vulnerabilidades reportadas en la API. Manteniendo vista.');
}
filteredVulnerabilities = [...vulnerabilities];
renderTable(filteredVulnerabilities);
updateCounters(filteredVulnerabilities);
//updateConnectionBadge(true, `Conectado: ${new URL(harborConfig.url).hostname}`);
updateConnectionBadge(true, `Conectado a HARBOR`);
document.getElementById('last-sync-time').textContent = `Sincronizado: ${new Date().toLocaleTimeString()}`;
} catch (error) {
console.error('Error al conectar con la API de Harbor:', error);
alert(`No se pudo conectar a Harbor API:\n${error.message}\n\nSe cargarán los datos simulados en modo de prueba.`);
vulnerabilities = [...MOCK_DATA];
filteredVulnerabilities = [...MOCK_DATA];
renderTable(filteredVulnerabilities);
updateCounters(filteredVulnerabilities);
updateConnectionBadge(false, 'Error API - Modo Simulación');
document.getElementById('last-sync-time').textContent = 'Fallo de conexión';
} finally {
syncIcon.classList.remove('fa-spin');
}
}
function updateConnectionBadge(isConnected, text) {
const badge = document.getElementById('connection-badge');
const dot = document.getElementById('connection-dot');
const label = document.getElementById('connection-status-text');
label.textContent = text;
if (isConnected) {
badge.className = "flex items-center space-x-2 bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 px-3 py-1.5 rounded-md text-xs font-medium";
dot.className = "w-2 h-2 rounded-full bg-emerald-400 animate-pulse";
} else {
badge.className = "flex items-center space-x-2 bg-amber-500/10 border border-amber-500/20 text-amber-400 px-3 py-1.5 rounded-md text-xs font-medium";
dot.className = "w-2 h-2 rounded-full bg-amber-400 animate-pulse";
}
}
// Lógica de filtrado de la tabla
function applyFilters() {
const filterType = document.getElementById('filter-type').value;
const query = document.getElementById('search-query').value.trim().toLowerCase();
filteredVulnerabilities = vulnerabilities.filter(item => {
if (!query) return true;
switch(filterType) {
case 'CVE ID':
return item.cve_id.toLowerCase().includes(query);
case 'Severity':
return item.severity.toLowerCase().includes(query);
case 'CVSS3':
return item.cvss3.toString().includes(query);
case 'Project Name':
return item.project.toLowerCase().includes(query);
case 'Repository Name':
return item.repository.toLowerCase().includes(query);
case 'Package':
return item.package.toLowerCase().includes(query);
case 'Tag':
return item.tag.toLowerCase().includes(query);
case 'Digest':
return item.digest.toLowerCase().includes(query);
case 'All':
default:
return (
item.cve_id.toLowerCase().includes(query) ||
item.severity.toLowerCase().includes(query) ||
item.package.toLowerCase().includes(query) ||
item.project.toLowerCase().includes(query) ||
item.repository.toLowerCase().includes(query) ||
item.tag.toLowerCase().includes(query) ||
item.digest.toLowerCase().includes(query) ||
item.cvss3.toString().includes(query)
);
}
});
renderTable(filteredVulnerabilities);
updateCounters(filteredVulnerabilities);
}
function handleKeySearch(event) {
if (event.key === 'Enter') applyFilters();
}
function resetFilters() {
document.getElementById('filter-type').value = 'All';
document.getElementById('search-query').value = '';
filteredVulnerabilities = [...vulnerabilities];
renderTable(filteredVulnerabilities);
updateCounters(filteredVulnerabilities);
}
function quickFilterSeverity(sev) {
document.getElementById('filter-type').value = 'Severity';
document.getElementById('search-query').value = sev;
applyFilters();
}
// Renderizado de tabla HTML
function renderTable(items) {
const tbody = document.getElementById('vulnerabilities-tbody');
const emptyState = document.getElementById('empty-state');
const resultsCount = document.getElementById('results-count');
tbody.innerHTML = '';
resultsCount.textContent = items.length;
if (items.length === 0) {
emptyState.classList.remove('hidden');
return;
} else {
emptyState.classList.add('hidden');
}
items.forEach((item, index) => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-slate-800/40 transition duration-150';
const sevBadge = getSeverityBadgeHtml(item.severity);
tr.innerHTML = `
<td class="px-4 py-3.5 font-mono font-medium text-sky-400">
<button onclick="openCveModal(${index})" class="hover:underline flex items-center gap-1.5 text-left">
<i class="fa-solid fa-bug text-xs text-sky-500"></i>
${escapeHtml(item.cve_id)}
</button>
</td>
<td class="px-4 py-3.5">${sevBadge}</td>
<td class="px-4 py-3.5 font-mono text-slate-200">
<span class="font-semibold text-white">${escapeHtml(item.package)}</span>
<span class="text-xs text-slate-500 block">${escapeHtml(item.version)}</span>
</td>
<td class="px-4 py-3.5">
<span class="px-2 py-0.5 rounded bg-slate-800 border border-slate-700 text-xs font-medium text-slate-300">
${escapeHtml(item.project)}
</span>
</td>
<td class="px-4 py-3.5 text-slate-300 font-mono text-xs">${escapeHtml(item.repository)}</td>
<td class="px-4 py-3.5 font-mono text-xs text-slate-400">
<span class="text-slate-200 font-medium block">${escapeHtml(item.tag)}</span>
<span class="text-slate-500" title="${escapeHtml(item.digest)}">${escapeHtml(item.digest).substring(0, 16)}...</span>
</td>
<td class="px-4 py-3.5 text-center font-mono font-bold ${item.cvss3 >= 9 ? 'text-red-400' : item.cvss3 >= 7 ? 'text-orange-400' : 'text-yellow-400'}">
${item.cvss3}
</td>
<td class="px-4 py-3.5 text-right">
<button onclick="openCveModal(${index})" class="bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 text-xs px-2.5 py-1.5 rounded transition">
<i class="fa-solid fa-eye mr-1"></i> Ver
</button>
</td>
`;
tbody.appendChild(tr);
});
}
function getSeverityBadgeHtml(severity) {
switch(severity.toUpperCase()) {
case 'CRITICAL':
return `<span class="bg-red-500/10 text-red-400 border border-red-500/30 px-2.5 py-1 rounded text-xs font-bold tracking-wide">CRITICAL</span>`;
case 'HIGH':
return `<span class="bg-orange-500/10 text-orange-400 border border-orange-500/30 px-2.5 py-1 rounded text-xs font-bold tracking-wide">HIGH</span>`;
case 'MEDIUM':
return `<span class="bg-yellow-500/10 text-yellow-400 border border-yellow-500/30 px-2.5 py-1 rounded text-xs font-bold tracking-wide">MEDIUM</span>`;
case 'LOW':
default:
return `<span class="bg-blue-500/10 text-blue-400 border border-blue-500/30 px-2.5 py-1 rounded text-xs font-bold tracking-wide">LOW</span>`;
}
}
function updateCounters(data) {
document.getElementById('count-total').textContent = data.length;
document.getElementById('count-critical').textContent = data.filter(i => i.severity.toUpperCase() === 'CRITICAL').length;
document.getElementById('count-high').textContent = data.filter(i => i.severity.toUpperCase() === 'HIGH').length;
document.getElementById('count-medium').textContent = data.filter(i => i.severity.toUpperCase() === 'MEDIUM').length;
document.getElementById('count-low').textContent = data.filter(i => i.severity.toUpperCase() === 'LOW').length;
}
// Modales
function openCveModal(index) {
const item = filteredVulnerabilities[index];
document.getElementById('modal-cve-id').textContent = item.cve_id;
document.getElementById('modal-cve-badge').innerHTML = getSeverityBadgeHtml(item.severity);
document.getElementById('modal-cve-package').textContent = `Paquete: ${item.package} (${item.version})`;
document.getElementById('modal-cve-cvss').textContent = item.cvss3;
document.getElementById('modal-cve-fixed').textContent = item.fixed_version || 'N/A';
document.getElementById('modal-cve-desc').textContent = item.description;
document.getElementById('modal-cve-target').innerHTML = `
<div><span class="text-slate-500">Proyecto:</span> ${escapeHtml(item.project)}</div>
<div><span class="text-slate-500">Repositorio:</span> ${escapeHtml(item.repository)}:${escapeHtml(item.tag)}</div>
<div><span class="text-slate-500">Digest:</span> ${escapeHtml(item.digest)}</div>
`;
document.getElementById('cve-modal').classList.remove('hidden');
}
function closeCveModal() {
document.getElementById('cve-modal').classList.add('hidden');
}
function openConfigModal() {
document.getElementById('config-modal').classList.remove('hidden');
}
function closeConfigModal() {
document.getElementById('config-modal').classList.add('hidden');
}
// Exportación de datos
function exportData(format) {
if (filteredVulnerabilities.length === 0) {
alert("No hay vulnerabilidades para exportar con los filtros actuales.");
return;
}
const timestamp = new Date().toISOString().slice(0,10);
if (format === 'json') {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(filteredVulnerabilities, null, 2));
const a = document.createElement('a');
a.setAttribute("href", dataStr);
a.setAttribute("download", `harbor_vulnerabilities_${timestamp}.json`);
document.body.appendChild(a);
a.click();
a.remove();
} else if (format === 'csv') {
const headers = ["CVE ID", "Severidad", "Paquete", "Version Afectada", "Version Correccion", "Proyecto", "Repositorio", "Tag", "CVSS3", "Digest"];
const rows = filteredVulnerabilities.map(i => [
`"${i.cve_id}"`,
`"${i.severity}"`,
`"${i.package}"`,
`"${i.version}"`,
`"${i.fixed_version}"`,
`"${i.project}"`,
`"${i.repository}"`,
`"${i.tag}"`,
i.cvss3,
`"${i.digest}"`
]);
const csvContent = "data:text/csv;charset=utf-8," + [headers.join(","), ...rows.map(e => e.join(","))].join("\n");
const encodedUri = encodeURI(csvContent);
const a = document.createElement("a");
a.setAttribute("href", encodedUri);
a.setAttribute("download", `harbor_vulnerabilities_${timestamp}.csv`);
document.body.appendChild(a);
a.click();
a.remove();
}
}
function escapeHtml(text) {
if (!text) return '';
return text.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
</script>
</body>
</html>