Consistent naming with zcat ecosystem (zcatui, zcatgui, zcatsql). All lowercase per Zig naming conventions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
441 lines
14 KiB
Markdown
441 lines
14 KiB
Markdown
# 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) |
|