erp-core/docs/02-fase-core-business/MGN-007-audit/especificaciones/ET-AUDIT-database.md

18 KiB

DDL-SPEC: Schema core_audit

Identificacion

Campo Valor
Schema core_audit
Modulo MGN-007
Version 1.0
Estado En Diseno
Autor Requirements-Analyst
Fecha 2025-12-05

Descripcion General

El schema core_audit almacena registros de auditoria, logs de acceso y eventos de seguridad. Utiliza particionamiento por fecha para alto rendimiento y facilitar archivado.

RF Cubiertos

RF Titulo Tablas
RF-AUDIT-001 Audit Trail audit_logs
RF-AUDIT-002 Logs de Acceso access_logs
RF-AUDIT-003 Eventos Seguridad security_events, security_alerts
RF-AUDIT-004 Reportes scheduled_reports, audit_alerts

Diagrama ER

erDiagram
    audit_logs {
        uuid id PK
        uuid tenant_id FK
        uuid user_id FK
        varchar action
        varchar entity_type
        uuid entity_id
        jsonb old_values
        jsonb new_values
        text[] changed_fields
        inet ip_address
        uuid request_id
        timestamptz created_at
    }

    access_logs {
        uuid id PK
        uuid tenant_id
        uuid user_id
        uuid session_id
        uuid request_id
        varchar method
        varchar path
        int status_code
        int response_time_ms
        inet ip_address
        timestamptz created_at
    }

    security_events {
        uuid id PK
        uuid tenant_id
        uuid user_id
        varchar event_type
        varchar severity
        varchar title
        text description
        jsonb metadata
        inet ip_address
        boolean is_resolved
        timestamptz created_at
    }

    security_alerts {
        uuid id PK
        uuid security_event_id FK
        uuid[] sent_to
        varchar[] channels
        boolean acknowledged
        timestamptz sent_at
    }

    scheduled_reports {
        uuid id PK
        uuid tenant_id FK
        varchar name
        varchar report_type
        jsonb filters
        varchar format
        varchar schedule
        varchar[] recipients
        boolean is_active
        timestamptz last_run_at
    }

    security_events ||--o{ security_alerts : "genera"

Tablas

1. audit_logs

Registro de cambios en entidades (audit trail).

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
user_id UUID NULL - Usuario que realizo la accion
action VARCHAR(20) NOT NULL - create/update/delete/restore
entity_type VARCHAR(100) NOT NULL - Nombre de la entidad
entity_id UUID NOT NULL - ID del registro afectado
old_values JSONB NULL - Valores anteriores
new_values JSONB NULL - Valores nuevos
changed_fields TEXT[] NULL - Lista de campos modificados
ip_address INET NULL - IP del cliente
user_agent TEXT NULL - User agent
request_id UUID NULL - ID de la peticion HTTP
created_at TIMESTAMPTZ NOT NULL NOW() Timestamp
-- Tabla particionada por mes
CREATE TABLE core_audit.audit_logs (
    id UUID DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    user_id UUID,
    action VARCHAR(20) NOT NULL,
    entity_type VARCHAR(100) NOT NULL,
    entity_id UUID NOT NULL,
    old_values JSONB,
    new_values JSONB,
    changed_fields TEXT[],
    ip_address INET,
    user_agent TEXT,
    request_id UUID,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- Crear particiones
CREATE TABLE core_audit.audit_logs_2025_12
    PARTITION OF core_audit.audit_logs
    FOR VALUES FROM ('2025-12-01') TO ('2026-01-01');

-- Indices
CREATE INDEX idx_audit_logs_tenant ON core_audit.audit_logs(tenant_id, created_at DESC);
CREATE INDEX idx_audit_logs_entity ON core_audit.audit_logs(entity_type, entity_id);
CREATE INDEX idx_audit_logs_user ON core_audit.audit_logs(user_id, created_at DESC);
CREATE INDEX idx_audit_logs_action ON core_audit.audit_logs(action);

-- RLS
ALTER TABLE core_audit.audit_logs ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_audit.audit_logs
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

2. access_logs

Registro de peticiones HTTP.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NULL - Tenant (si autenticado)
user_id UUID NULL - Usuario
session_id UUID NULL - Sesion
request_id UUID NOT NULL - ID unico de peticion
method VARCHAR(10) NOT NULL - GET/POST/PUT/DELETE
path VARCHAR(500) NOT NULL - Ruta
query_params JSONB NULL - Query string
request_body JSONB NULL - Body (sanitizado)
status_code INTEGER NOT NULL - Codigo HTTP
response_time_ms INTEGER NOT NULL - Tiempo respuesta
ip_address INET NOT NULL - IP cliente
user_agent TEXT NULL - User agent
referer VARCHAR(500) NULL - Referer
country_code VARCHAR(2) NULL - Pais (geo-ip)
created_at TIMESTAMPTZ NOT NULL NOW() Timestamp
-- Tabla particionada por dia (alto volumen)
CREATE TABLE core_audit.access_logs (
    id UUID DEFAULT gen_random_uuid(),
    tenant_id UUID,
    user_id UUID,
    session_id UUID,
    request_id UUID NOT NULL,
    method VARCHAR(10) NOT NULL,
    path VARCHAR(500) NOT NULL,
    query_params JSONB,
    request_body JSONB,
    status_code INTEGER NOT NULL,
    response_time_ms INTEGER NOT NULL,
    ip_address INET NOT NULL,
    user_agent TEXT,
    referer VARCHAR(500),
    country_code VARCHAR(2),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- Particion de ejemplo
CREATE TABLE core_audit.access_logs_2025_12_05
    PARTITION OF core_audit.access_logs
    FOR VALUES FROM ('2025-12-05') TO ('2025-12-06');

-- Indices
CREATE INDEX idx_access_logs_tenant ON core_audit.access_logs(tenant_id, created_at DESC);
CREATE INDEX idx_access_logs_user ON core_audit.access_logs(user_id, created_at DESC);
CREATE INDEX idx_access_logs_path ON core_audit.access_logs(path, created_at DESC);
CREATE INDEX idx_access_logs_status ON core_audit.access_logs(status_code) WHERE status_code >= 400;
CREATE INDEX idx_access_logs_ip ON core_audit.access_logs(ip_address);

3. security_events

Eventos de seguridad detectados.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NULL - Tenant
user_id UUID NULL - Usuario afectado
event_type VARCHAR(50) NOT NULL - Tipo de evento
severity VARCHAR(20) NOT NULL - low/medium/high/critical
title VARCHAR(255) NOT NULL - Titulo descriptivo
description TEXT NULL - Descripcion detallada
metadata JSONB NULL '{}' Datos adicionales
ip_address INET NULL - IP origen
country_code VARCHAR(2) NULL - Pais
is_resolved BOOLEAN NOT NULL false Resuelto
resolved_by UUID NULL - Usuario que resolvio
resolved_at TIMESTAMPTZ NULL - Fecha resolucion
resolution_notes TEXT NULL - Notas de resolucion
created_at TIMESTAMPTZ NOT NULL NOW() Timestamp
CREATE TABLE core_audit.security_events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID,
    user_id UUID,
    event_type VARCHAR(50) NOT NULL,
    severity VARCHAR(20) NOT NULL,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    metadata JSONB DEFAULT '{}',
    ip_address INET,
    country_code VARCHAR(2),
    is_resolved BOOLEAN NOT NULL DEFAULT false,
    resolved_by UUID,
    resolved_at TIMESTAMPTZ,
    resolution_notes TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT chk_severity CHECK (severity IN ('low', 'medium', 'high', 'critical')),
    CONSTRAINT chk_event_type CHECK (event_type IN (
        'login_failed', 'login_blocked', 'brute_force_detected',
        'password_changed', 'password_reset_requested',
        'session_hijack_attempt', 'concurrent_session',
        'session_from_new_location', 'session_from_new_device',
        'permission_escalation', 'role_changed', 'admin_created',
        'mass_delete_attempt', 'data_export', 'sensitive_data_access',
        'api_key_created', 'webhook_modified', 'settings_changed'
    ))
);

CREATE INDEX idx_security_events_tenant ON core_audit.security_events(tenant_id, created_at DESC);
CREATE INDEX idx_security_events_severity ON core_audit.security_events(severity) WHERE is_resolved = false;
CREATE INDEX idx_security_events_user ON core_audit.security_events(user_id, created_at DESC);
CREATE INDEX idx_security_events_type ON core_audit.security_events(event_type, created_at DESC);
CREATE INDEX idx_security_events_unresolved ON core_audit.security_events(is_resolved, severity)
    WHERE is_resolved = false;

4. security_alerts

Alertas enviadas por eventos de seguridad.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
security_event_id UUID NOT NULL - FK a security_events
sent_to UUID[] NOT NULL - IDs de usuarios
channels VARCHAR(20)[] NOT NULL - Canales utilizados
acknowledged BOOLEAN NOT NULL false Reconocida
acknowledged_by UUID NULL - Usuario que reconocio
acknowledged_at TIMESTAMPTZ NULL - Fecha reconocimiento
sent_at TIMESTAMPTZ NOT NULL NOW() Fecha envio
CREATE TABLE core_audit.security_alerts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    security_event_id UUID NOT NULL REFERENCES core_audit.security_events(id),
    sent_to UUID[] NOT NULL,
    channels VARCHAR(20)[] NOT NULL,
    acknowledged BOOLEAN NOT NULL DEFAULT false,
    acknowledged_by UUID,
    acknowledged_at TIMESTAMPTZ,
    sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_security_alerts_event ON core_audit.security_alerts(security_event_id);
CREATE INDEX idx_security_alerts_unack ON core_audit.security_alerts(acknowledged) WHERE acknowledged = false;

5. scheduled_reports

Reportes programados de auditoria.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
name VARCHAR(255) NOT NULL - Nombre del reporte
report_type VARCHAR(50) NOT NULL - Tipo de reporte
filters JSONB NOT NULL '{}' Filtros de consulta
format VARCHAR(10) NOT NULL 'pdf' Formato exportacion
schedule VARCHAR(100) NOT NULL - Expresion cron
recipients TEXT[] NOT NULL - Emails destinatarios
is_active BOOLEAN NOT NULL true Activo
last_run_at TIMESTAMPTZ NULL - Ultima ejecucion
next_run_at TIMESTAMPTZ NULL - Proxima ejecucion
created_by UUID NOT NULL - Usuario creador
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE core_audit.scheduled_reports (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    name VARCHAR(255) NOT NULL,
    report_type VARCHAR(50) NOT NULL,
    filters JSONB NOT NULL DEFAULT '{}',
    format VARCHAR(10) NOT NULL DEFAULT 'pdf',
    schedule VARCHAR(100) NOT NULL,
    recipients TEXT[] NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT true,
    last_run_at TIMESTAMPTZ,
    next_run_at TIMESTAMPTZ,
    created_by UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT chk_format CHECK (format IN ('csv', 'xlsx', 'pdf', 'json')),
    CONSTRAINT chk_report_type CHECK (report_type IN (
        'activity', 'security', 'compliance', 'change', 'access'
    ))
);

-- RLS
ALTER TABLE core_audit.scheduled_reports ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_audit.scheduled_reports
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

6. audit_alerts

Alertas configurables de auditoria.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
name VARCHAR(255) NOT NULL - Nombre de la alerta
condition JSONB NOT NULL - Condicion de disparo
actions JSONB NOT NULL - Acciones a ejecutar
is_active BOOLEAN NOT NULL true Activa
last_triggered_at TIMESTAMPTZ NULL - Ultimo disparo
created_by UUID NOT NULL - Usuario creador
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE core_audit.audit_alerts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    name VARCHAR(255) NOT NULL,
    condition JSONB NOT NULL,
    actions JSONB NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT true,
    last_triggered_at TIMESTAMPTZ,
    created_by UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- RLS
ALTER TABLE core_audit.audit_alerts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_audit.audit_alerts
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

Funciones de Utilidad

Registrar Evento de Auditoria

CREATE OR REPLACE FUNCTION core_audit.log_entity_change(
    p_tenant_id UUID,
    p_user_id UUID,
    p_action VARCHAR,
    p_entity_type VARCHAR,
    p_entity_id UUID,
    p_old_values JSONB,
    p_new_values JSONB,
    p_ip_address INET DEFAULT NULL,
    p_request_id UUID DEFAULT NULL
) RETURNS UUID AS $$
DECLARE
    v_changed_fields TEXT[];
    v_id UUID;
BEGIN
    -- Calcular campos modificados
    IF p_action = 'update' AND p_old_values IS NOT NULL AND p_new_values IS NOT NULL THEN
        SELECT array_agg(key)
        INTO v_changed_fields
        FROM (
            SELECT key FROM jsonb_object_keys(p_new_values) AS key
            WHERE p_old_values->key IS DISTINCT FROM p_new_values->key
        ) diff;
    END IF;

    INSERT INTO core_audit.audit_logs (
        tenant_id, user_id, action, entity_type, entity_id,
        old_values, new_values, changed_fields, ip_address, request_id
    ) VALUES (
        p_tenant_id, p_user_id, p_action, p_entity_type, p_entity_id,
        p_old_values, p_new_values, v_changed_fields, p_ip_address, p_request_id
    ) RETURNING id INTO v_id;

    RETURN v_id;
END;
$$ LANGUAGE plpgsql;

Detectar Brute Force

CREATE OR REPLACE FUNCTION core_audit.check_brute_force(
    p_email VARCHAR,
    p_ip_address INET,
    p_window_minutes INTEGER DEFAULT 15,
    p_max_attempts INTEGER DEFAULT 5
) RETURNS BOOLEAN AS $$
DECLARE
    v_attempt_count INTEGER;
BEGIN
    SELECT COUNT(*) INTO v_attempt_count
    FROM core_auth.login_attempts
    WHERE (email = p_email OR ip_address = p_ip_address)
    AND success = false
    AND attempted_at > NOW() - (p_window_minutes || ' minutes')::INTERVAL;

    RETURN v_attempt_count >= p_max_attempts;
END;
$$ LANGUAGE plpgsql STABLE;

Mantenimiento

Crear Particiones Automaticas

-- Job para crear particiones (ejecutar mensualmente)
CREATE OR REPLACE FUNCTION core_audit.create_monthly_partitions()
RETURNS void AS $$
DECLARE
    v_next_month DATE := date_trunc('month', NOW() + INTERVAL '1 month');
    v_partition_name TEXT;
BEGIN
    -- audit_logs
    v_partition_name := 'audit_logs_' || to_char(v_next_month, 'YYYY_MM');
    EXECUTE format(
        'CREATE TABLE IF NOT EXISTS core_audit.%I PARTITION OF core_audit.audit_logs
         FOR VALUES FROM (%L) TO (%L)',
        v_partition_name, v_next_month, v_next_month + INTERVAL '1 month'
    );
END;
$$ LANGUAGE plpgsql;

Archivar Particiones Antiguas

CREATE OR REPLACE FUNCTION core_audit.archive_old_partitions(
    p_table_name TEXT,
    p_retention_months INTEGER
) RETURNS void AS $$
DECLARE
    v_cutoff_date DATE := date_trunc('month', NOW() - (p_retention_months || ' months')::INTERVAL);
    v_partition RECORD;
BEGIN
    FOR v_partition IN
        SELECT tablename FROM pg_tables
        WHERE schemaname = 'core_audit'
        AND tablename LIKE p_table_name || '_%'
        AND tablename < p_table_name || '_' || to_char(v_cutoff_date, 'YYYY_MM')
    LOOP
        -- Exportar a S3 (implementar segun infraestructura)
        -- EXECUTE format('COPY core_audit.%I TO PROGRAM ''aws s3 cp - s3://bucket/audit/%I.csv.gz --sse''', v_partition.tablename, v_partition.tablename);

        -- Detach partition
        EXECUTE format('ALTER TABLE core_audit.%I DETACH PARTITION core_audit.%I',
            p_table_name, v_partition.tablename);

        -- Drop partition (o mover a schema de archivo)
        EXECUTE format('DROP TABLE core_audit.%I', v_partition.tablename);
    END LOOP;
END;
$$ LANGUAGE plpgsql;

Consideraciones de Performance

Tabla Volumen Esperado Estrategia
audit_logs Alto (millones/mes) Particionamiento mensual
access_logs Muy alto (millones/dia) Particionamiento diario
security_events Bajo-medio Tabla simple con indices
scheduled_reports Bajo Tabla simple

Historial

Version Fecha Autor Cambios
1.0 2025-12-05 Requirements-Analyst Creacion inicial