zcatsql/docs/AUDIT_LOG_DESIGN.md
reugenio 6891d6e026 feat: add audit log system with hash chain integrity
- 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>
2025-12-08 22:56:23 +01:00

14 KiB

Audit Log System - Diseño de Arquitectura

Visión General

Sistema de log histórico externo para zsqlite 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):

{
  "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):

  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

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)

  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)