Portal de Seguridad Harbor
This commit is contained in:
parent
f0a4899459
commit
8a4c0c4aa6
9
assets/css/fontawesome.all.min.css
vendored
Normal file
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
83
assets/js/tailwindcss.js
Normal file
File diff suppressed because one or more lines are too long
BIN
assets/webfonts/fa-brands-400.ttf
Normal file
BIN
assets/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
assets/webfonts/fa-brands-400.woff2
Normal file
BIN
assets/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
assets/webfonts/fa-regular-400.ttf
Normal file
BIN
assets/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
assets/webfonts/fa-regular-400.woff2
Normal file
BIN
assets/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
assets/webfonts/fa-solid-900.ttf
Normal file
BIN
assets/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
assets/webfonts/fa-solid-900.woff2
Normal file
BIN
assets/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
assets/webfonts/fa-v4compatibility.ttf
Normal file
BIN
assets/webfonts/fa-v4compatibility.ttf
Normal file
Binary file not shown.
BIN
assets/webfonts/fa-v4compatibility.woff2
Normal file
BIN
assets/webfonts/fa-v4compatibility.woff2
Normal file
Binary file not shown.
875
harbor_security_local.html
Normal file
875
harbor_security_local.html
Normal 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, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
875
harbor_security_portal.html
Normal file
875
harbor_security_portal.html
Normal 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, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
65
proxy-harbor.py
Normal file
65
proxy-harbor.py
Normal 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()
|
||||||
Loading…
x
Reference in New Issue
Block a user