# Audit Log System - Diseño de Arquitectura ## Visión General Sistema de log histórico externo para zcatsql que permite: 1. **v1.0**: Auditoría completa de operaciones con integridad verificable 2. **v2.0**: Navegación temporal (time travel) a cualquier punto en el historial ## Arquitectura ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Aplicación │ │ ┌─────────────┐ │ │ │ AuditCtx │ ◀── user_id, app_name (proporcionado por app) │ │ └─────────────┘ │ └────────┬────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ AuditLog │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ Hooks: pre-update, update, commit, rollback │ │ │ │ Captura: SQL + valores antes/después + metadatos │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ Entry Buffer (en memoria hasta commit) │ │ │ │ - Agrupa operaciones por transacción │ │ │ │ - Descarta si rollback │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ LogWriter │ │ │ │ - Escribe a archivo │ │ │ │ - Gestiona rotación │ │ │ │ - Calcula hash chain │ │ │ └──────────────────────────────────────────────────────────────┘ │ └────────┬────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Sistema de Archivos │ │ │ │ mydb_audit_0001.log ──▶ mydb_audit_0002.log ──▶ ... │ │ (hash chain) (hash chain) │ │ │ │ mydb_audit_index.json ◀── Índice de archivos y rangos de tiempo │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## Formato de Entrada de Log Cada operación se registra en formato JSON Lines (una línea por entrada): ```json { "seq": 1, "ts": "2025-12-08T14:32:15.123456Z", "tx_id": 42, "ctx": { "user": "alice", "app": "simifactu", "host": "workstation-01", "pid": 12345 }, "op": "UPDATE", "table": "invoices", "rowid": 157, "sql": "UPDATE invoices SET status = 'paid' WHERE id = 157", "before": { "id": 157, "status": "pending", "amount": 1500.00 }, "after": { "id": 157, "status": "paid", "amount": 1500.00 }, "prev_hash": "a1b2c3d4e5f6...", "hash": "f6e5d4c3b2a1..." } ``` ### Campos | Campo | Tipo | Descripción | |-------|------|-------------| | `seq` | u64 | Número de secuencia global (nunca se repite) | | `ts` | ISO8601 | Timestamp con microsegundos | | `tx_id` | u64 | ID de transacción (agrupa operaciones) | | `ctx` | object | Contexto: user, app, host, pid | | `op` | string | INSERT, UPDATE, DELETE | | `table` | string | Nombre de la tabla | | `rowid` | i64 | SQLite rowid | | `sql` | string | SQL ejecutado (si está disponible) | | `before` | object? | Valores antes (NULL para INSERT) | | `after` | object? | Valores después (NULL para DELETE) | | `prev_hash` | string | Hash de la entrada anterior | | `hash` | string | SHA-256 de esta entrada (sin el campo hash) | --- ## Estructura de Archivos ``` mydb_audit/ ├── index.json # Índice maestro ├── 0001_20251208_143200.log # Archivo de log ├── 0002_20251209_000000.log # Siguiente archivo └── ... ``` ### index.json ```json { "version": 1, "db_name": "mydb.sqlite", "created": "2025-12-08T14:32:00Z", "files": [ { "id": 1, "filename": "0001_20251208_143200.log", "seq_start": 1, "seq_end": 15420, "ts_start": "2025-12-08T14:32:00Z", "ts_end": "2025-12-08T23:59:59Z", "entries": 15420, "bytes": 8523410, "first_hash": "000000...", "last_hash": "a1b2c3...", "closed": true }, { "id": 2, "filename": "0002_20251209_000000.log", "seq_start": 15421, "seq_end": null, "ts_start": "2025-12-09T00:00:00Z", "ts_end": null, "entries": 342, "bytes": 182340, "first_hash": "a1b2c3...", "last_hash": "d4e5f6...", "closed": false } ], "rotation": { "max_bytes": 104857600, "max_age_days": 30 } } ``` --- ## Rotación de Archivos Criterios (configurable, se aplica el primero que se cumpla): 1. **Por tamaño**: cuando alcanza `max_bytes` (default: 100MB) 2. **Por tiempo**: cuando pasa `max_age_days` (default: 30 días) 3. **Manual**: API para forzar rotación Al rotar: 1. Cerrar archivo actual (marcar `closed: true`) 2. Crear nuevo archivo 3. Primer hash del nuevo = último hash del anterior (continuidad) 4. Actualizar index.json --- ## Hash Chain (Integridad) ``` Entry 1: prev_hash = "0000...0000" (genesis) hash = SHA256(entry_1_without_hash) Entry 2: prev_hash = hash(Entry 1) hash = SHA256(entry_2_without_hash) Entry N: prev_hash = hash(Entry N-1) hash = SHA256(entry_N_without_hash) ``` ### Verificación ```zig pub fn verifyChain(log_dir: []const u8) !VerifyResult { // 1. Leer index.json // 2. Para cada archivo: // - Verificar que first_hash == last_hash del anterior // - Para cada entrada: // - Recalcular hash // - Verificar prev_hash == hash anterior // 3. Reportar: OK o primera entrada corrupta } ``` --- ## API v1.0 - Auditoría ```zig const AuditLog = @import("zcatsql").audit.AuditLog; // Configuración const config = AuditLog.Config{ .log_dir = "./mydb_audit", .rotation = .{ .max_bytes = 100 * 1024 * 1024, // 100MB .max_age_days = 30, }, .context = .{ .app_name = "simifactu", .user_id = null, // Se puede cambiar dinámicamente }, .capture = .{ .sql = true, .before_values = true, .after_values = true, }, }; // Inicializar var audit = try AuditLog.init(allocator, &db, config); defer audit.deinit(); // Cambiar usuario dinámicamente (ej: después de login) audit.setUser("alice"); // El audit log captura automáticamente via hooks try db.exec("INSERT INTO users (name) VALUES ('Bob')"); // Forzar rotación manual try audit.rotate(); // Verificar integridad const result = try audit.verify(); if (!result.valid) { std.debug.print("Corrupto en seq {}\n", .{result.first_invalid_seq}); } // Estadísticas const stats = audit.stats(); std.debug.print("Entries: {}, Files: {}\n", .{stats.total_entries, stats.file_count}); ``` --- ## API v2.0 - Time Travel ```zig const TimeMachine = @import("zcatsql").audit.TimeMachine; // Inicializar con log existente var tm = try TimeMachine.init(allocator, "./mydb_audit"); defer tm.deinit(); // Obtener estado en un momento específico var snapshot_db = try tm.getStateAt("2025-12-08T15:30:00Z"); defer snapshot_db.close(); // snapshot_db es una DB en memoria con el estado de ese momento // O navegar por secuencia var snapshot_db2 = try tm.getStateAtSeq(1000); // Replay: reconstruir DB desde cero var rebuilt_db = try tm.replay(":memory:"); defer rebuilt_db.close(); // Replay parcial: desde snapshot hasta punto var partial_db = try tm.replayRange( "base_snapshot.sqlite", // Punto de partida 5000, // seq_start 8000, // seq_end ); // Undo: revertir últimas N operaciones try tm.undoLast(&db, 5); // Redo: re-aplicar operaciones revertidas try tm.redo(&db, 3); // Ver historial de una fila específica const history = try tm.getRowHistory("invoices", 157); defer allocator.free(history); for (history) |entry| { std.debug.print("{s}: {s} -> {s}\n", .{entry.ts, entry.op, entry.after}); } ``` --- ## Consideraciones de Rendimiento ### v1.0 - **Overhead mínimo**: hooks son muy ligeros - **Buffering**: acumular en memoria hasta commit - **Async write**: opción de escribir en thread separado - **Compresión**: opcional, gzip por archivo cerrado ### v2.0 - **Snapshots periódicos**: cada N operaciones o tiempo - Reduce tiempo de replay - `snapshot_0001.sqlite` + logs desde ahí - **Índices en memoria**: para búsqueda rápida por tiempo/tabla/rowid - **Lazy loading**: cargar solo los logs necesarios --- ## Flujo de Datos ### Captura (v1.0) ``` 1. App ejecuta SQL │ ▼ 2. Pre-update hook captura valores ANTES │ ▼ 3. SQLite ejecuta la operación │ ▼ 4. Update hook captura operación + rowid │ ▼ 5. Guardar en buffer de transacción │ ├── Commit hook ──▶ Escribir buffer a archivo │ └── Rollback hook ──▶ Descartar buffer ``` ### Replay (v2.0) ``` 1. Encontrar snapshot más cercano anterior al tiempo deseado │ ▼ 2. Cargar snapshot a memoria │ ▼ 3. Leer logs desde snapshot hasta tiempo deseado │ ▼ 4. Para cada entrada: - INSERT: ejecutar INSERT con valores `after` - UPDATE: ejecutar UPDATE con valores `after` - DELETE: ejecutar DELETE │ ▼ 5. Retornar DB en estado deseado ``` ### Undo ``` 1. Leer entrada a revertir │ ▼ 2. Según operación: - INSERT: DELETE WHERE rowid = X - UPDATE: UPDATE SET (valores `before`) WHERE rowid = X - DELETE: INSERT con valores `before` │ ▼ 3. Registrar operación de undo en el log (para auditoría) ``` --- ## Seguridad (Futuro) - **Encriptación**: AES-256-GCM por archivo - **Firma digital**: además de hash, firmar con clave privada - **Acceso**: permisos de lectura separados de escritura --- ## Módulos a Implementar ``` src/ ├── audit/ │ ├── mod.zig # Exports │ ├── log.zig # AuditLog principal │ ├── writer.zig # Escritura a archivo │ ├── entry.zig # Estructura de entrada │ ├── context.zig # Contexto (user, app, host) │ ├── rotation.zig # Gestión de rotación │ ├── index.zig # Índice de archivos │ ├── hash.zig # Hash chain │ ├── verify.zig # Verificación de integridad │ └── time_machine.zig # Time travel (v2.0) ``` --- ## Fases de Implementación ### Fase 1: Core (v1.0) 1. Estructura de entrada y serialización JSON 2. Writer con rotación básica 3. Hash chain 4. Hooks para captura 5. Contexto y metadatos ### Fase 2: Integridad (v1.0) 1. Verificación de cadena 2. Index.json completo 3. Estadísticas ### Fase 3: Time Travel (v2.0) 1. Replay hacia adelante 2. Undo/Redo 3. getStateAt() 4. getRowHistory() ### Fase 4: Optimización (v2.0) 1. Snapshots automáticos 2. Índices en memoria 3. Compresión de archivos cerrados --- ## Preguntas Resueltas | Pregunta | Decisión | |----------|----------| | ¿Qué capturar? | SQL + valores antes/después | | ¿Cómo identificar contexto? | Mixto: auto-detectar host/pid, app proporciona user/app_name | | ¿Cuándo rotar? | Configurable: por defecto 100MB o 30 días | | ¿Granularidad time travel? | Cualquier operación (seq exacto) |