Portal de Seguridad Harbor

This commit is contained in:
luisgulo 2026-10-31 01:54:39 +01:00
parent f0a4899459
commit 8a4c0c4aa6
13 changed files with 1907 additions and 0 deletions

9
assets/css/fontawesome.all.min.css vendored Normal file

File diff suppressed because one or more lines are too long

83
assets/js/tailwindcss.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

875
harbor_security_local.html Normal file
View File

@ -0,0 +1,875 @@
<!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>

875
harbor_security_portal.html Normal file
View File

@ -0,0 +1,875 @@
<!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="https://cdn.tailwindcss.com"></script>
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/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: ['Inter', 'sans-serif'],
mono: ['JetBrains Mono', '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>

65
proxy-harbor.py Normal file
View File

@ -0,0 +1,65 @@
import http.server
import urllib.request
import base64
import json
PORT = 8080
# Cambia esto por tu URL de Harbor real
HARBOR_URL = "https://harbor.midominio.com"
class ProxyHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
# Si la petición va dirigida a la API, la redirigimos a Harbor haciendo de proxy
if self.path.startswith("/api/"):
target_url = HARBOR_URL + self.path
# Recuperamos las cabeceras de autorización que envíe el navegador
auth_header = self.headers.get('Authorization')
req = urllib.request.Request(target_url)
if auth_header:
req.add_header('Authorization', auth_header)
try:
# Realizamos la petición a Harbor ignorando problemas de certificados internos si los hubiera
import ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
with urllib.request.urlopen(req, context=ctx) as response:
data = response.read()
self.send_response(response.status)
self.send_header('Content-Type', response.headers.get('Content-Type', 'application/json'))
# Añadimos las cabeceras CORS para que el navegador no se queje nunca más
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(data)
except urllib.error.HTTPError as e:
self.send_response(e.code)
self.end_headers()
self.wfile.write(e.read())
except Exception as e:
self.send_response(500)
self.end_headers()
self.wfile.write(str(e).encode('utf-8'))
else:
# Para cualquier otra ruta, sirve los archivos estáticos normales (tu HTML)
super().do_GET()
def do_OPTIONS(self):
# Respondemos automáticamente al preflight de CORS del navegador
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Authorization, Content-Type')
self.end_headers()
if __name__ == "__main__":
server_address = ('', PORT)
httpd = http.server.HTTPServer(server_address, ProxyHandler)
print(f"---------------------------------------------------------")
print(f"Servidor proxy y web corriendo en http://localhost:{PORT}")
print(f"---------------------------------------------------------")
print(f"\nUse la URL http://localhost:{PORT} para Harbor\nen vez de {HARBOR_URL}")
httpd.serve_forever()