# 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 ```mermaid 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 | ```sql -- 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 | ```sql -- 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 | ```sql 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 | ```sql 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 | ```sql 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 | ```sql 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 ```sql 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 ```sql 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 ```sql -- 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 ```sql 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 |