Skip to content
GitHub

Sistema de Auditoría

El sistema de auditoría proporciona un registro completo de eventos de seguridad y mutaciones de datos. Sanitiza automáticamente datos sensibles, calcula diferencias a nivel de campo para actualizaciones y agrupa escrituras por lotes para mejorar el rendimiento.

El sistema soporta múltiples tipos de operación definidos por el tipo AuditOperation:

OperaciónDescripciónCaso de Uso
LOGINAutenticación exitosaInicio de sesión de usuario
LOGOUTCierre de sesión de usuarioTerminación de sesión
LOGIN_FAILEDAutenticación fallidaDetección de fuerza bruta
ACCESS_DENIEDFallo de autorizaciónRastreo de violación de permisos
INSERTNuevo registro creadoAuditoría de creación de datos
UPDATERegistro modificadoRastreo de cambios de datos con diferencias
DELETERegistro eliminadoAuditoría de eliminación de datos
API_ACCESSEndpoint accedidoRastreo de uso de API
EXPORTOperación de exportación de datosMonitoreo de exfiltración de datos
CONFIG_CHANGEConfiguración del sistema modificadaRastreo de acciones de administrador
RATE_LIMITEDLímite de tasa excedidoDetección de abuso
flowchart TB

    %% =========================
    %% API Pública auditLog
    %% =========================
    subgraph API["API Pública (auditLog)"]
        Login["login(userId, ctx, success)"]
        Logout["logout(userId, ctx)"]
        Insert["insert(table, values, ctx)"]
        Update["update(table, old, new, ctx)"]
        Delete["delete(table, values, ctx)"]
        AccessDenied["accessDenied(resource, ctx)"]
        ApiAccess["apiAccess(endpoint, method, ctx)"]
    end

    %% =========================
    %% Clase AuditLogger
    %% =========================
    subgraph Logger["Clase AuditLogger"]
        LogEntry["log(entry: AuditEntry)<br/>Agregar a cola"]
        Ctor["constructor()<br/>Iniciar temporizador flush"]
        Queue["queue AuditEntry[]<br/>Máx 50 entradas"]
        FlushInterval["flushInterval<br/>Cada 5 segundos"]
        Flush["flush()<br/>Escritura por lotes a BD"]
        Shutdown["shutdown()<br/>Flush final"]
    end

    %% =========================
    %% Pipeline de Procesamiento
    %% =========================
    subgraph Pipeline["Pipeline de Procesamiento de Datos"]
        Sanitize["sanitizeForAudit()<br/>Redactar contraseñas/tokens"]
        Diff["getChangedFields()<br/>Calcular diferencias"]
        Context["extractAuditContext()<br/>Extraer datos de req"]
    end

    %% =========================
    %% Base de Datos
    %% =========================
    DB[(monitoring.audit_log)]

    %% =========================
    %% Flujos API → Logger
    %% =========================
    Login --> LogEntry
    Logout --> LogEntry
    Insert --> LogEntry
    Update --> LogEntry
    Delete --> LogEntry
    AccessDenied --> LogEntry
    ApiAccess --> LogEntry

    %% =========================
    %% Logger → Pipeline
    %% =========================
    LogEntry --> Sanitize
    Sanitize --> Diff
    Diff --> Context
    Context --> Queue

    %% =========================
    %% Control de Flush
    %% =========================
    Ctor --> FlushInterval
    FlushInterval -->|Cada 5s| Flush
    Queue -->|50 entradas llenas| Flush
    Shutdown --> Flush

    %% =========================
    %% Persistencia
    %% =========================
    Flush -->|INSERT por lotes| DB

El sistema redacta automáticamente campos sensibles antes del registro para prevenir filtración de credenciales:

sensitiveFields.ts
// Patrones de campos sensibles redactados
const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'api_key', 'clave'];

Todos los campos coincidentes son reemplazados con [REDACTED] en ambas columnas JSON old_values y new_values.

Para operaciones UPDATE, el sistema calcula automáticamente qué campos cambiaron comparando oldValues y newValues usando serialización JSON:

diffCalculation.ts
// Calcular diferencias entre oldValues y newValues
// Compara JSON.stringify() de cada campo
if (JSON.stringify(oldValues[key]) !== JSON.stringify(newValues[key])) {
changed.push(key);
}

El array resultante se almacena en la columna changed_fields para consultas eficientes de cambios de campos específicos.

El helper extractAuditContext() extrae metadatos estándar de las peticiones Express:

CampoFuenteDescripción
tenantIdreq.user.tenantIdContexto multi-tenant
userIdreq.user.userIdID de usuario autenticado
ipAddressheaders x-forwarded-for o x-real-ipIP real del cliente (consciente de proxy)
userAgentheader user-agentNavegador/herramienta del cliente
requestIdreq.idID de correlación de petición

El auditMiddleware registra automáticamente acceso a la API para operaciones mutantes (POST/PUT/PATCH/DELETE) y respuestas de error (4xx/5xx):

auditMiddleware.ts
// Solo registra operaciones mutantes o errores
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method) || res.statusCode >= 400) {
auditLog.apiAccess(req.originalUrl, req.method, ctx, res.statusCode);
}
  • excludePaths: Array de prefijos de ruta a omitir (por defecto: /health, /api/auth/login, /api/auth/logout)
  • onlyAuthenticated: Solo registrar peticiones autenticadas (por defecto: true)

Los eventos de login y logout se registran explícitamente en las rutas de autenticación:

  1. Validación de usuario/contraseña
  2. Verificación con bcrypt.compare()
  3. En éxito: auditLog.login(userId, ctx, true)
  4. En fallo: auditLog.login(userId, ctx, false)
  1. Extracción de token JWT
  2. auditLog.logout(userId, ctx)
  3. Cookie eliminada

El sistema de auditoría escribe a la tabla monitoring.audit_log en la base de datos nostromo_command:

ColumnaTipoDescripción
idUUIDLlave primaria (auto-generada)
tenant_idUUIDContexto del tenant (nullable)
user_idUUIDUsuario que realizó la acción (nullable)
table_nameTEXTNombre de entidad/recurso
operationTEXTTipo de operación (LOGIN, UPDATE, etc.)
old_valuesJSONBEstado pre-cambio (nullable)
new_valuesJSONBEstado post-cambio (nullable)
changed_fieldsTEXT[]Array de nombres de campos modificados
ip_addressINETDirección IP del cliente
user_agentTEXTCadena user agent del cliente
created_atTIMESTAMPTZTimestamp del evento (por defecto NOW())