# 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 |
