- Complete audit logging for INSERT, UPDATE, DELETE operations - SHA-256 hash chain for tamper detection - File rotation by size (100MB) and age (30 days) - Context tracking (user, app, host, pid) - JSON Lines format output - verifyChain() for integrity verification - Comprehensive REFERENCE.md technical documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
14 KiB
14 KiB
Audit Log System - Diseño de Arquitectura
Visión General
Sistema de log histórico externo para zsqlite que permite:
- v1.0: Auditoría completa de operaciones con integridad verificable
- 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):
{
"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
{
"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):
- Por tamaño: cuando alcanza
max_bytes(default: 100MB) - Por tiempo: cuando pasa
max_age_days(default: 30 días) - Manual: API para forzar rotación
Al rotar:
- Cerrar archivo actual (marcar
closed: true) - Crear nuevo archivo
- Primer hash del nuevo = último hash del anterior (continuidad)
- 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
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
const AuditLog = @import("zsqlite").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
const TimeMachine = @import("zsqlite").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)
- Estructura de entrada y serialización JSON
- Writer con rotación básica
- Hash chain
- Hooks para captura
- Contexto y metadatos
Fase 2: Integridad (v1.0)
- Verificación de cadena
- Index.json completo
- Estadísticas
Fase 3: Time Travel (v2.0)
- Replay hacia adelante
- Undo/Redo
- getStateAt()
- getRowHistory()
Fase 4: Optimización (v2.0)
- Snapshots automáticos
- Índices en memoria
- 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) |