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 |