feat: Mejoras en auditoría - incidentes de seguridad y esquema de colores

- Backend:
  * Agregado endpoint /v1/audit/security/incidents con paginación y filtros
  * Nuevos schemas SecurityIncidentResponse y SecurityIncidentListResponse
  * Fix timezone: datetime.utcnow() → datetime.now(timezone.utc) en 4 ubicaciones
  * Detección automática de incidentes: mass deletion, brute force, privilege escalation

- Frontend (Internal):
  * Nueva sección de Incidentes de Seguridad con modal de detalles
  * Filtros por severidad, estado y tipo de incidente
  * Conversión completa a esquema grayscale (gray-100 a gray-900)
  * Eliminados todos los emojis de páginas audit y security
  * Implementada paginación para incidentes

- Fixes:
  * Resuelto error 500: TypeError con datetimes timezone-aware/naive
  * Resuelto error 404: endpoint de incidentes faltante
This commit is contained in:
2026-02-16 12:45:33 -07:00
parent 32cc8b6ccd
commit be762585d2
6 changed files with 1038 additions and 96 deletions

View File

@@ -9,9 +9,14 @@
let logs = [];
let stats = null;
let users = [];
let incidents = [];
let securityAnalysis = null;
let isLoading = false;
let isLoadingIncidents = false;
let selectedLog = null;
let selectedIncident = null;
let showDetailModal = false;
let showIncidentModal = false;
// Paginación
let currentPage = 1;
@@ -19,12 +24,24 @@
let totalLogs = 0;
const perPage = 20;
// Paginación de incidentes
let incidentsPage = 1;
let incidentsTotalPages = 1;
let totalIncidents = 0;
const incidentsPerPage = 10;
// Filtros básicos
let filterUserId = '';
let filterAction = '';
let filterResourceType = '';
let searchText = '';
// Filtros de incidentes
let filterSeverity = '';
let filterIncidentType = '';
let filterStatus = '';
let incidentSearchText = '';
// Filtro multi-tenant (solo para ADMIN/SUPPORT_MANAGER)
let allTenants = false;
@@ -126,6 +143,57 @@
}
}
/**
* Cargar incidentes de seguridad
*/
async function loadIncidents() {
isLoadingIncidents = true;
try {
const params: any = {
page: incidentsPage,
per_page: incidentsPerPage
};
// Aplicar filtros de incidentes
if (filterSeverity) params.severity = filterSeverity;
if (filterIncidentType) params.type = filterIncidentType;
if (filterStatus) params.status = filterStatus;
if (incidentSearchText) params.search = incidentSearchText;
// Aplicar filtro multi-tenant si el usuario tiene permiso
if (allTenants && canSeeAllTenants) {
params.all_tenants = true;
}
const response = await api.get('/audit/security/incidents', params);
incidents = response.incidents || [];
totalIncidents = response.total || 0;
incidentsTotalPages = response.total_pages || 1;
incidentsPage = response.page || 1;
} catch (e) {
console.error('Error cargando incidentes:', e);
incidents = [];
} finally {
isLoadingIncidents = false;
}
}
/**
* Cargar análisis de seguridad
*/
async function loadSecurityAnalysis() {
try {
const params: any = { hours: 24 };
if (allTenants && canSeeAllTenants) {
params.all_tenants = true;
}
securityAnalysis = await api.get('/audit/security/analysis', params);
} catch (e) {
console.error('Error cargando análisis de seguridad:', e);
}
}
/**
* Cargar logs de auditoría con filtros
*/
@@ -285,16 +353,50 @@
}
/**
* Obtener color de badge según tipo de acción
* Obtener color de badge según tipo de acción (solo escala de grises)
*/
function getActionColor(action: string): string {
if (action.includes('login')) return 'bg-green-100 text-green-800';
if (action.includes('logout')) return 'bg-gray-100 text-gray-800';
if (action.includes('create')) return 'bg-blue-100 text-blue-800';
if (action.includes('update')) return 'bg-yellow-100 text-yellow-800';
if (action.includes('delete')) return 'bg-red-100 text-red-800';
if (action.includes('assign')) return 'bg-purple-100 text-purple-800';
return 'bg-gray-100 text-gray-800';
if (action.includes('delete')) return 'bg-gray-800 text-white';
if (action.includes('update')) return 'bg-gray-600 text-white';
if (action.includes('login') || action.includes('logout')) return 'bg-gray-400 text-white';
if (action.includes('create')) return 'bg-gray-300 text-gray-800';
return 'bg-gray-200 text-gray-700';
}
/**
* Obtener color de severidad (escala de grises)
*/
function getSeverityColor(severity: string): string {
switch(severity?.toLowerCase()) {
case 'critical':
return 'bg-gray-900 text-white';
case 'high':
return 'bg-gray-700 text-white';
case 'medium':
return 'bg-gray-500 text-white';
case 'low':
return 'bg-gray-300 text-gray-800';
default:
return 'bg-gray-200 text-gray-700';
}
}
/**
* Obtener color de estado (escala de grises)
*/
function getStatusColor(status: string): string {
switch(status?.toLowerCase()) {
case 'active':
case 'open':
return 'bg-gray-800 text-white';
case 'resolved':
case 'closed':
return 'bg-gray-400 text-white';
case 'investigating':
return 'bg-gray-600 text-white';
default:
return 'bg-gray-200 text-gray-700';
}
}
/**
@@ -336,6 +438,58 @@
return roleMap[role] || role;
}
/**
* Ver detalle de un incidente
*/
function viewIncidentDetail(incident: any) {
selectedIncident = incident;
showIncidentModal = true;
}
/**
* Aplicar filtros de incidentes y recargar desde página 1
*/
function applyIncidentFilters() {
incidentsPage = 1;
loadIncidents();
}
/**
* Limpiar filtros de incidentes
*/
function clearIncidentFilters() {
filterSeverity = '';
filterIncidentType = '';
filterStatus = '';
incidentSearchText = '';
incidentsPage = 1;
loadIncidents();
}
/**
* Cambiar página de incidentes
*/
function goToIncidentsPage(page: number) {
if (page >= 1 && page <= incidentsTotalPages) {
incidentsPage = page;
loadIncidents();
}
}
/**
* Formatear fecha simple
*/
function formatSimpleDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('es-MX', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
/**
* Inicializar datos
*/
@@ -343,6 +497,8 @@
loadStats();
loadUsers();
loadLogs();
loadIncidents();
loadSecurityAnalysis();
});
</script>
@@ -353,7 +509,7 @@
<h1 class="text-2xl font-semibold text-gray-900">Auditoría del Sistema</h1>
<p class="mt-1 text-sm text-gray-600">
Registro de actividades •
<span class="font-medium text-primary-600">
<span class="font-medium text-gray-800">
{periodFilter === 'today' ? 'Hoy' :
periodFilter === 'yesterday' ? 'Ayer' :
periodFilter === 'last7days' ? 'Últimos 7 días' :
@@ -369,31 +525,31 @@
<div class="flex flex-wrap gap-2">
<button
on:click={() => changePeriod('today')}
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'today' ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'today' ? 'bg-gray-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
>
Hoy
</button>
<button
on:click={() => changePeriod('yesterday')}
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'yesterday' ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'yesterday' ? 'bg-gray-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
>
Ayer
</button>
<button
on:click={() => changePeriod('last7days')}
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'last7days' ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'last7days' ? 'bg-gray-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
>
Últimos 7 días
</button>
<button
on:click={() => changePeriod('last30days')}
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'last30days' ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'last30days' ? 'bg-gray-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
>
Últimos 30 días
</button>
<button
on:click={() => changePeriod('custom')}
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'custom' ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {periodFilter === 'custom' ? 'bg-gray-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
>
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
@@ -412,7 +568,7 @@
id="custom-date-from"
bind:value={customDateFrom}
on:change={applyFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm"
/>
</div>
<div>
@@ -422,7 +578,7 @@
id="custom-date-to"
bind:value={customDateTo}
on:change={applyFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm"
/>
</div>
</div>
@@ -444,7 +600,7 @@
loadLogs();
loadStats();
}}
class="rounded border-gray-300 text-primary-600 shadow-sm focus:border-primary-500 focus:ring-primary-500 h-4 w-4 mr-3"
class="rounded border-gray-300 text-gray-600 shadow-sm focus:border-gray-500 focus:ring-gray-500 h-4 w-4 mr-3"
/>
<div>
<span class="text-sm font-medium text-gray-900">Ver todos los clientes</span>
@@ -453,7 +609,7 @@
</label>
</div>
{#if allTenants}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-200 text-gray-800">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 9a1 1 0 012 0v4a1 1 0 11-2 0V9zm1-5a1 1 0 100 2 1 1 0 000-2z" />
</svg>
@@ -473,28 +629,28 @@
</div>
<div class="bg-white rounded-lg shadow p-4">
<div class="text-sm text-gray-500">Hoy</div>
<div class="text-2xl font-bold text-primary-600">{stats.actions_today}</div>
<div class="text-2xl font-bold text-gray-600">{stats.actions_today}</div>
</div>
<div class="bg-white rounded-lg shadow p-4">
<div class="text-sm text-gray-500">Esta Semana</div>
<div class="text-2xl font-bold text-green-600">{stats.actions_this_week}</div>
<div class="text-2xl font-bold text-gray-600">{stats.actions_this_week}</div>
</div>
<div class="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
<div class="flex items-center gap-2 mb-1">
<svg class="w-4 h-4 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="text-sm text-gray-500">Vulnerabilidad</div>
<div class="text-sm text-gray-500">Incidentes Criticos</div>
</div>
<div class="flex items-center justify-between">
<div class="text-2xl font-bold {stats.critical_actions_today > 10 ? 'text-red-600' : stats.critical_actions_today > 5 ? 'text-amber-600' : 'text-green-600'}">
{stats.critical_actions_today}
<div class="text-2xl font-bold text-gray-800">
{stats.critical_actions_today || 0}
</div>
<button
type="button"
class="text-xs text-primary-600 hover:text-primary-700 font-medium flex items-center gap-1 px-2 py-1 rounded hover:bg-primary-50 transition-colors"
class="text-xs text-gray-600 hover:text-gray-800 font-medium flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 transition-colors"
on:click={() => filterCriticalActions()}
title="Filtrar acciones críticas"
title="Ver incidentes críticos"
>
Ver
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -502,7 +658,7 @@
</svg>
</button>
</div>
<div class="text-xs text-gray-500 mt-1">Acciones críticas hoy</div>
<div class="text-xs text-gray-500 mt-1">Incidentes críticos hoy</div>
</div>
</div>
{/if}
@@ -519,7 +675,7 @@
</svg>
<span class="text-sm font-medium text-gray-900">Filtros Avanzados</span>
{#if activeFiltersCount > 0}
<span class="px-2 py-0.5 rounded-full bg-primary-100 text-primary-700 text-xs font-medium">
<span class="px-2 py-0.5 rounded-full bg-gray-200 text-gray-700 text-xs font-medium">
{activeFiltersCount}
</span>
{/if}
@@ -541,7 +697,7 @@
bind:value={searchText}
on:input={applyFilters}
placeholder="Buscar en acciones..."
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm"
/>
</div>
@@ -552,7 +708,7 @@
id="user"
bind:value={filterUserId}
on:change={applyFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm"
>
<option value="">Todos</option>
{#each users as user}
@@ -568,7 +724,7 @@
id="action"
bind:value={filterAction}
on:change={applyFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm"
>
<option value="">Todas</option>
{#each Array.from(availableActions).sort() as action}
@@ -584,7 +740,7 @@
id="resource-type"
bind:value={filterResourceType}
on:change={applyFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm"
>
<option value="">Todos</option>
{#each Array.from(availableResourceTypes).sort() as resourceType}
@@ -598,7 +754,7 @@
<div class="mt-4 flex justify-end">
<button
on:click={clearFilters}
class="text-sm text-primary-600 hover:text-primary-700 font-medium"
class="text-sm text-gray-600 hover:text-gray-800 font-medium"
>
Limpiar filtros
</button>
@@ -608,6 +764,130 @@
{/if}
</div>
<!-- Sección de Incidentes de Seguridad -->
<div class="bg-white shadow rounded-lg mb-6">
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
<div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-900">Incidentes de Seguridad</h3>
<span class="text-sm text-gray-500">{totalIncidents} incidentes</span>
</div>
</div>
<!-- Filtros de Incidentes -->
<div class="px-4 py-3 border-b border-gray-200">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<input
type="text"
placeholder="Buscar incidentes..."
bind:value={incidentSearchText}
on:input={applyIncidentFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm"
/>
<select
bind:value={filterSeverity}
on:change={applyIncidentFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm"
>
<option value="">Toda severidad</option>
<option value="critical">Crítico</option>
<option value="high">Alto</option>
<option value="medium">Medio</option>
<option value="low">Bajo</option>
</select>
<select
bind:value={filterStatus}
on:change={applyIncidentFilters}
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm"
>
<option value="">Todo estado</option>
<option value="active">Activo</option>
<option value="investigating">Investigando</option>
<option value="resolved">Resuelto</option>
</select>
<button
on:click={clearIncidentFilters}
class="px-3 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors text-sm"
>
Limpiar filtros
</button>
</div>
</div>
<!-- Lista de Incidentes -->
<div class="overflow-x-auto">
{#if isLoadingIncidents}
<div class="flex items-center justify-center p-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-600"></div>
<span class="ml-2 text-sm text-gray-500">Cargando incidentes...</span>
</div>
{:else if incidents.length === 0}
<div class="text-center py-8">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No hay incidentes</h3>
<p class="mt-1 text-sm text-gray-500">No se encontraron incidentes de seguridad para los filtros seleccionados.</p>
</div>
{:else}
<div class="divide-y divide-gray-200">
{#each incidents as incident (incident.id)}
<div class="p-4 hover:bg-gray-50 transition-colors cursor-pointer" on:click={() => viewIncidentDetail(incident)}>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="flex-shrink-0">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-gray-900 truncate">{incident.title}</p>
<p class="text-sm text-gray-500 truncate">{incident.description || 'Sin descripción'}</p>
</div>
</div>
<div class="flex items-center space-x-2">
<span class="px-2 py-1 text-xs font-medium rounded-full {getSeverityColor(incident.severity)}">
{incident.severity?.toUpperCase()}
</span>
<span class="px-2 py-1 text-xs font-medium rounded-full {getStatusColor(incident.status)}">
{incident.status?.toUpperCase()}
</span>
<span class="text-xs text-gray-500">
{formatSimpleDate(incident.created_at)}
</span>
</div>
</div>
</div>
{/each}
</div>
<!-- Paginación de Incidentes -->
{#if incidentsTotalPages > 1}
<div class="px-4 py-3 border-t border-gray-200 flex items-center justify-between">
<div class="text-sm text-gray-700">
Página {incidentsPage} de {incidentsTotalPages}
</div>
<div class="flex space-x-1">
<button
on:click={() => goToIncidentsPage(incidentsPage - 1)}
disabled={incidentsPage === 1}
class="px-3 py-1 text-sm bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Anterior
</button>
<button
on:click={() => goToIncidentsPage(incidentsPage + 1)}
disabled={incidentsPage === incidentsTotalPages}
class="px-3 py-1 text-sm bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Siguiente
</button>
</div>
</div>
{/if}
{/if}
</div>
</div>
<!-- Tabla de Logs -->
<div class="bg-white shadow rounded-lg overflow-hidden flex flex-col" style="max-height: calc(100vh - 500px); min-height: 400px;">
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50 flex-shrink-0">
@@ -672,8 +952,8 @@
<td class="px-3 py-2 text-sm">
{#if log.user_email}
<div class="flex items-center gap-2">
<div class="flex-shrink-0 w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
<span class="text-xs font-medium text-primary-700">
<div class="flex-shrink-0 w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center">
<span class="text-xs font-medium text-gray-700">
{(log.user_name || '?').charAt(0).toUpperCase()}
</span>
</div>
@@ -701,7 +981,7 @@
<button
type="button"
on:click={() => viewDetail(log)}
class="text-primary-600 hover:text-primary-900 font-medium transition-colors"
class="text-gray-600 hover:text-gray-900 font-medium transition-colors"
>
Ver
</button>
@@ -722,8 +1002,8 @@
<div class="flex items-start gap-3 flex-1 min-w-0">
<!-- Avatar -->
{#if log.user_email}
<div class="flex-shrink-0 w-10 h-10 bg-primary-100 rounded-full flex items-center justify-center">
<span class="text-sm font-medium text-primary-700">
<div class="flex-shrink-0 w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center">
<span class="text-xs font-medium text-gray-700">
{(log.user_name || '?').charAt(0).toUpperCase()}
</span>
</div>
@@ -760,7 +1040,7 @@
<button
type="button"
on:click={() => viewDetail(log)}
class="flex-shrink-0 text-primary-600 hover:text-primary-900 transition-colors p-1"
class="flex-shrink-0 text-gray-600 hover:text-gray-900 transition-colors p-1"
title="Ver detalles"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -835,7 +1115,7 @@
{#each Array.from({length: Math.min(5, totalPages)}, (_, i) => i + Math.max(1, Math.min(currentPage - 2, totalPages - 4))) as page}
<button
on:click={() => goToPage(page)}
class="relative inline-flex items-center px-3 py-1.5 border text-xs font-medium transition-colors {page === currentPage ? 'z-10 bg-primary-600 border-primary-600 text-white' : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'}"
class="relative inline-flex items-center px-3 py-1.5 border text-xs font-medium transition-colors {page === currentPage ? 'z-10 bg-gray-700 border-gray-700 text-white' : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'}"
>
{page}
</button>
@@ -873,8 +1153,99 @@
</div>
</div>
<!-- Modal de Incidentes -->
{#if showIncidentModal && selectedIncident}
<Modal open={showIncidentModal} size="2xl" title="Detalle del Incidente de Seguridad" on:close={() => showIncidentModal = false}>
<div class="space-y-4">
<!-- Información General -->
<div>
<h4 class="text-sm font-medium text-gray-900 mb-2">Información General</h4>
<dl class="grid grid-cols-2 gap-3 text-sm">
<div>
<dt class="font-medium text-gray-500">Título:</dt>
<dd class="text-gray-900">{selectedIncident.title}</dd>
</div>
<div>
<dt class="font-medium text-gray-500">Severidad:</dt>
<dd>
<span class="px-2 py-1 text-xs font-medium rounded-full {getSeverityColor(selectedIncident.severity)}">
{selectedIncident.severity?.toUpperCase()}
</span>
</dd>
</div>
<div>
<dt class="font-medium text-gray-500">Estado:</dt>
<dd>
<span class="px-2 py-1 text-xs font-medium rounded-full {getStatusColor(selectedIncident.status)}">
{selectedIncident.status?.toUpperCase()}
</span>
</dd>
</div>
<div>
<dt class="font-medium text-gray-500">Fecha:</dt>
<dd class="text-gray-900">{formatDate(selectedIncident.created_at)}</dd>
</div>
{#if selectedIncident.affected_user}
<div>
<dt class="font-medium text-gray-500">Usuario Afectado:</dt>
<dd class="text-gray-900">{selectedIncident.affected_user}</dd>
</div>
{/if}
{#if selectedIncident.source_ip}
<div>
<dt class="font-medium text-gray-500">IP Origen:</dt>
<dd class="text-gray-900 font-mono text-xs">{selectedIncident.source_ip}</dd>
</div>
{/if}
</dl>
</div>
<!-- Descripción -->
{#if selectedIncident.description}
<div>
<h4 class="text-sm font-medium text-gray-900 mb-2">Descripción</h4>
<div class="bg-gray-50 rounded-lg p-3 text-sm text-gray-700">
{selectedIncident.description}
</div>
</div>
{/if}
<!-- Evidencia -->
{#if selectedIncident.evidence && selectedIncident.evidence.length > 0}
<div>
<h4 class="text-sm font-medium text-gray-900 mb-2">Evidencia</h4>
<div class="bg-gray-50 rounded-lg p-3">
<ul class="list-disc list-inside text-sm text-gray-700 space-y-1">
{#each selectedIncident.evidence as evidence}
<li>{evidence}</li>
{/each}
</ul>
</div>
</div>
{/if}
<!-- Metadata -->
{#if selectedIncident.metadata && Object.keys(selectedIncident.metadata).length > 0}
<div>
<h4 class="text-sm font-medium text-gray-900 mb-2">Información Adicional</h4>
<pre class="bg-gray-50 rounded-lg p-3 text-xs font-mono text-gray-600 overflow-auto max-h-40">{JSON.stringify(selectedIncident.metadata, null, 2)}</pre>
</div>
{/if}
</div>
<div slot="footer" class="flex justify-end">
<button
on:click={() => showIncidentModal = false}
class="px-4 py-2 bg-white border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50"
>
Cerrar
</button>
</div>
</Modal>
{/if}
<!-- Modal de Detalle -->
{#if showDetailModal && selectedLog}
{#if showDetailModal && selectedLog}}
<Modal open={showDetailModal} size="2xl" title="Detalle del Registro de Auditoría" on:close={() => showDetailModal = false}>
<div class="space-y-4">
<!-- Información General -->

View File

@@ -44,28 +44,28 @@
}
/**
* Obtener color según nivel de riesgo
* Obtener color según nivel de riesgo (escala de grises)
*/
function getRiskColor(level: string) {
const colors: any = {
safe: 'bg-green-100 text-green-800 border-green-300',
low: 'bg-blue-100 text-blue-800 border-blue-300',
medium: 'bg-yellow-100 text-yellow-800 border-yellow-300',
high: 'bg-orange-100 text-orange-800 border-orange-300',
critical: 'bg-red-100 text-red-800 border-red-300'
safe: 'bg-gray-100 text-gray-800 border-gray-300',
low: 'bg-gray-200 text-gray-800 border-gray-400',
medium: 'bg-gray-400 text-white border-gray-500',
high: 'bg-gray-600 text-white border-gray-700',
critical: 'bg-gray-900 text-white border-gray-900'
};
return colors[level] || colors.low;
}
/**
* Obtener color de severidad de amenaza
* Obtener color de severidad de amenaza (escala de grises)
*/
function getSeverityColor(severity: string) {
const colors: any = {
low: 'bg-blue-100 text-blue-800',
medium: 'bg-yellow-100 text-yellow-800',
high: 'bg-orange-100 text-orange-800',
critical: 'bg-red-100 text-red-800'
low: 'bg-gray-200 text-gray-800',
medium: 'bg-gray-400 text-white',
high: 'bg-gray-600 text-white',
critical: 'bg-gray-900 text-white'
};
return colors[severity] || colors.low;
}
@@ -179,7 +179,7 @@
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900 flex items-center gap-2">
<svg class="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-8 h-8 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
Análisis de Seguridad
@@ -190,7 +190,7 @@
</div>
<button
on:click={() => loadSecurityAnalysis()}
class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 flex items-center gap-2"
class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-500 flex items-center gap-2"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
@@ -211,19 +211,19 @@
<div class="flex flex-wrap gap-2">
<button
on:click={() => changeAnalysisPeriod(24)}
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {analysisHours === 24 ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {analysisHours === 24 ? 'bg-gray-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
>
Últimas 24 horas
</button>
<button
on:click={() => changeAnalysisPeriod(48)}
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {analysisHours === 48 ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {analysisHours === 48 ? 'bg-gray-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
>
Últimas 48 horas
</button>
<button
on:click={() => changeAnalysisPeriod(168)}
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {analysisHours === 168 ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
class="px-4 py-2 rounded-lg text-sm font-medium transition-colors {analysisHours === 168 ? 'bg-gray-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
>
Última semana
</button>
@@ -232,7 +232,7 @@
{#if isLoading}
<div class="flex justify-center items-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-600"></div>
</div>
{:else if analysis}
<!-- Resumen de Riesgo -->
@@ -257,9 +257,9 @@
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">Amenazas Detectadas</p>
<p class="text-2xl font-bold text-red-600">{analysis.total_threats_detected}</p>
<p class="text-2xl font-bold text-gray-800">{analysis.total_threats_detected}</p>
</div>
<svg class="w-10 h-10 text-red-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-10 h-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
@@ -269,9 +269,9 @@
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">Intentos Fallidos</p>
<p class="text-2xl font-bold text-orange-600">{analysis.failed_login_attempts}</p>
<p class="text-2xl font-bold text-gray-700">{analysis.failed_login_attempts}</p>
</div>
<svg class="w-10 h-10 text-orange-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-10 h-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
@@ -281,9 +281,9 @@
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">IPs Sospechosas</p>
<p class="text-2xl font-bold text-purple-600">{analysis.suspicious_ips_count}</p>
<p class="text-2xl font-bold text-gray-700">{analysis.suspicious_ips_count}</p>
</div>
<svg class="w-10 h-10 text-purple-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-10 h-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
</div>
@@ -293,9 +293,9 @@
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">Acciones Críticas</p>
<p class="text-2xl font-bold text-amber-600">{analysis.critical_actions_count}</p>
<p class="text-2xl font-bold text-gray-700">{analysis.critical_actions_count}</p>
</div>
<svg class="w-10 h-10 text-amber-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-10 h-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
@@ -304,17 +304,17 @@
<!-- Recomendaciones Generales -->
{#if analysis.recommended_actions && analysis.recommended_actions.length > 0}
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
<div class="flex items-start gap-3">
<svg class="w-6 h-6 text-blue-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 text-gray-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div class="flex-1">
<h4 class="text-sm font-semibold text-blue-900 mb-2">Acciones Recomendadas</h4>
<h4 class="text-sm font-semibold text-gray-900 mb-2">Acciones Recomendadas</h4>
<ul class="space-y-1">
{#each analysis.recommended_actions as action}
<li class="text-sm text-blue-800 flex items-start gap-2">
<svg class="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<li class="text-sm text-gray-700 flex items-start gap-2">
<svg class="w-4 h-4 text-gray-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
{action}
@@ -332,12 +332,12 @@
<h3 class="text-lg font-semibold text-gray-900">Amenazas Detectadas</h3>
{#each analysis.threats as threat}
<div class="bg-white shadow rounded-lg p-6 border-l-4 {threat.severity === 'critical' ? 'border-red-500' : threat.severity === 'high' ? 'border-orange-500' : threat.severity === 'medium' ? 'border-yellow-500' : 'border-blue-500'}">
<div class="bg-white shadow rounded-lg p-6 border-l-4 {threat.severity === 'critical' ? 'border-gray-900' : threat.severity === 'high' ? 'border-gray-600' : threat.severity === 'medium' ? 'border-gray-400' : 'border-gray-200'}">
<!-- Header de Amenaza -->
<div class="flex items-start justify-between mb-4">
<div class="flex items-start gap-3 flex-1">
<div class="p-2 rounded-lg {threat.severity === 'critical' ? 'bg-red-100' : threat.severity === 'high' ? 'bg-orange-100' : threat.severity === 'medium' ? 'bg-yellow-100' : 'bg-blue-100'}">
<svg class="w-6 h-6 {threat.severity === 'critical' ? 'text-red-600' : threat.severity === 'high' ? 'text-orange-600' : threat.severity === 'medium' ? 'text-yellow-600' : 'text-blue-600'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="p-2 rounded-lg {threat.severity === 'critical' ? 'bg-gray-100' : threat.severity === 'high' ? 'bg-gray-100' : threat.severity === 'medium' ? 'bg-gray-100' : 'bg-gray-50'}">
<svg class="w-6 h-6 {threat.severity === 'critical' ? 'text-gray-900' : threat.severity === 'high' ? 'text-gray-700' : threat.severity === 'medium' ? 'text-gray-600' : 'text-gray-500'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getThreatIcon(threat.type)} />
</svg>
</div>
@@ -418,7 +418,7 @@
{#if threat.affected_ips.length > 0}
<button
on:click={() => openActionModal(threat, 'block_ip')}
class="px-3 py-1.5 bg-red-600 text-white text-sm rounded hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 flex items-center gap-1"
class="px-3 py-1.5 bg-gray-800 text-white text-sm rounded hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-500 flex items-center gap-1"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
@@ -429,7 +429,7 @@
{#if threat.affected_users.length > 0}
<button
on:click={() => openActionModal(threat, 'force_password_reset')}
class="px-3 py-1.5 bg-orange-600 text-white text-sm rounded hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-orange-500 flex items-center gap-1"
class="px-3 py-1.5 bg-gray-600 text-white text-sm rounded hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 flex items-center gap-1"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
@@ -439,7 +439,7 @@
{/if}
<button
on:click={() => openActionModal(threat, 'notify_admin')}
class="px-3 py-1.5 bg-blue-600 text-white text-sm rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-1"
class="px-3 py-1.5 bg-gray-500 text-white text-sm rounded hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 flex items-center gap-1"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
@@ -453,12 +453,12 @@
</div>
{:else}
<!-- No hay amenazas -->
<div class="bg-green-50 border border-green-200 rounded-lg p-8 text-center">
<svg class="w-16 h-16 text-green-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="bg-gray-50 border border-gray-200 rounded-lg p-8 text-center">
<svg class="w-16 h-16 text-gray-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<h3 class="text-lg font-semibold text-green-900 mb-2">Sistema Seguro</h3>
<p class="text-sm text-green-700">No se detectaron amenazas en el período analizado</p>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Sistema Seguro</h3>
<p class="text-sm text-gray-600">No se detectaron amenazas en el período analizado</p>
</div>
{/if}
{/if}
@@ -486,7 +486,7 @@
type="text"
bind:value={actionTarget}
placeholder="IP o email del usuario"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm"
/>
</div>
@@ -496,7 +496,7 @@
bind:value={actionReason}
rows="3"
placeholder="Razón de la acción de seguridad"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm"
></textarea>
</div>
@@ -508,7 +508,7 @@
bind:value={actionDuration}
min="1"
max="10080"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm"
/>
</div>
{/if}
@@ -516,13 +516,13 @@
<div class="flex justify-end gap-3 pt-4 border-t">
<button
on:click={() => showActionModal = false}
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary-500"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500"
>
Cancelar
</button>
<button
on:click={executeSecurityAction}
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500"
class="px-4 py-2 text-sm font-medium text-white bg-gray-700 rounded-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-500"
>
Ejecutar Acción
</button>