Simulador de reglas.
Un dry-run en producción: antes de aplicar una regla nueva o modificada, reprodúcela contra el historial real de queries de tu workspace y mira qué habría cambiado. Qué flujos legítimos de tu aplicación habría bloqueado, qué marcaría y qué dejaría de bloquear.
Resumen
Escribir una regla custom lleva minutos. Aplicarla es lo delicado: si algún camino de tu aplicación emite la query que la regla prohíbe, el proxy la rechaza en producción. El simulador responde la pregunta que importa antes de aplicar: ¿qué de mi propio tráfico habría bloqueado este cambio el último mes?
Toma un cambio de reglas candidato, lo evalúa contra las queries registradas en la ventana y lo compara con lo que deciden tus reglas de hoy. El resultado es un reporte con un veredicto, una frase lista para pegar en el PR («Would have blocked 2 legitimate flows (9,442 executions) in the last 30 days.») y cada flujo afectado, con sus ejecuciones, identidades y muestras enmascaradas.
Nada se ejecuta contra tu base de datos, nada se bloquea y nada se escribe en el historial de queries ni en tu cuota.
Disponibilidad. Planes Builder, Team y Enterprise. Puedes simular hasta 30 días en Builder, 90 en Team y según contrato (hasta 180) en Enterprise. Son máximos: eliges la ventana en cada ejecución (por defecto 30 días), y nunca supera el historial de queries que conserva tu plan ni la antigüedad del workspace. Se usa desde el dashboard y desde CI con la API.
Cómo funciona
Un cambio candidato puede combinar hasta 20 de estos elementos. Cada YAML se valida con el mismo checker que al crear la regla:
- Una regla custom nueva (
custom_new): nombre, severidad, dialecto y su condición YAML. - Una edición de una regla custom existente (
custom_edit): otra severidad, otro YAML o los dos. - Desactivar una regla custom (
custom_disable). - Una regla estándar (
standard_override): activarla, desactivarla o cambiar su severidad.
La simulación corre en segundo plano, en cuatro fases:
- Lectura de eventos. Las queries que registraron el proxy TCP y la API de runtime en la ventana, de todo el workspace o de una base de datos.
- Agrupación por flujo. Cada query se agrupa por su huella: el texto con los literales reemplazados.
DELETE FROM cart_cartitem WHERE id = 41y… WHERE id = 97son el mismo flujo. - Evaluación. Primero una sentencia representativa de cada flujo y luego las demás, hasta el presupuesto del plan (50.000 evaluaciones en Builder, 200.000 en Team y Enterprise). Cada sentencia se evalúa con el motor de hoy, con tus reglas actuales y con las candidatas. Las que no se llegan a evaluar heredan el resultado de su flujo y se cuentan como inferidas.
- Comparación. Candidatas contra actuales es el efecto de tu cambio, y es lo único que cuenta para el veredicto. Actuales contra lo que se registró en su momento es deriva (por ejemplo, una versión nueva del motor): se reporta aparte y nunca se atribuye a tu cambio.
Desde el dashboard
- En el editor de reglas custom, Simular contra producción simula la regla que estás escribiendo o editando, sin guardarla. Eliges cuántos días simular (7, 30, 90 o 180, o un número propio), hasta el máximo de tu plan; por defecto, 30.
- En la lista de reglas, Simular en la fila de una regla simula activarla o desactivarla.
- En Reglas → Simulaciones ves el progreso de cada simulación (lectura, agrupación, evaluación, comparación), puedes cancelarla y abres el reporte cuando termina. Ahí quedan las simulaciones de los últimos 7 días.
- Al elegir la ventana, el diálogo muestra una estimación (por ejemplo, «≈ 3200 sentencias distintas, ~2 min»): cuenta los eventos y las sentencias distintas de esa ventana sin leer ninguna query (exacta hasta 100.000 eventos y extrapolada de una muestra por encima) y la convierte en tiempo con la velocidad de tus simulaciones recientes. Es una estimación: la simulación puede tardar más o menos.
- En Reglas → Simulaciones puedes marcar dos ejecuciones terminadas, normalmente de la misma regla antes y después de editarla, y Comparar ejecuciones: el veredicto de cada una, las ejecuciones, la ventana y las bases, por categoría los conteos y los flujos nuevos, resueltos o sin cambios (comparados por fingerprint), y el diff del YAML de las reglas. Si las reglas, las ventanas o las bases no coinciden, la comparación lo advierte.
Se ejecuta una simulación a la vez por workspace, con hasta 10 por día en Builder y 30 en Team y Enterprise.
Desde CI con la API
Para fallar el PR que cambia un archivo de reglas si rompería un flujo legítimo, crea en API Keys una key con el scope rules:simulate y guárdala como secreto de CI. Ese scope nunca se asigna por defecto ni va incluido en ci_dryrun:execute, porque permite que Vericto lea el historial de producción: hay que darlo a propósito. La key solo ve las simulaciones de su workspace.
1. Iniciar la simulación
curl -s -X POST https://api.vericto.com/api/v1/ci/rules/simulate \
-H "X-API-Key: $VERICTO_SIM_KEY" \
-H "Content-Type: application/json" \
-d '{
"days": 30,
"candidates": [
{ "kind": "custom_new", "name": "no-physical-deletes", "severity": "high", "dialect": "postgres",
"ast_condition_yaml": "rule: no-physical-deletes\nnode_type: DeleteStmt" },
{ "kind": "custom_new", "name": "ledger-append-only", "severity": "critical", "dialect": "postgres",
"ast_condition_yaml": "rule: ledger-append-only\nnode_type: UpdateStmt\ncondition:\n relation: \"billing_ledgerentry\"" }
],
"thresholds": { "fail_on_break": 0, "min_executions": 3, "min_days": 2 }
}'
// 202 Accepted · Retry-After: 5
{
"simulation_id": "7f3c9a2e-5b1d-4c8e-9f60-2a7d41e0b9c3",
"status": "queued",
"channel": "cli",
"requested_days": 30,
"progress": { "phase": "queued", "done": 0, "total": 0 },
"summary": null
}
Opcionales: database_id para simular una sola base (sin él, todas) y thresholds. days va de 1 al máximo de tu plan: 30 en Builder, 90 en Team y el de tu contrato en Enterprise (hasta 180). Por defecto es 30 (o el máximo del plan, si es menor). Un valor por encima del máximo se rechaza con un error que indica el máximo de tu plan; nunca se recorta en silencio. POST /ci/rules/simulate y el dashboard aplican las mismas reglas.
2. Esperar el resultado y decidir
while :; do
r=$(curl -s https://api.vericto.com/api/v1/ci/rules/simulations/$SIMULATION_ID \
-H "X-API-Key: $VERICTO_SIM_KEY")
s=$(echo "$r" | jq -r .status) # queued | running | succeeded | failed | cancelled
[ "$s" = queued ] || [ "$s" = running ] || break
sleep 5
done
echo "$r" | jq -r .summary.headline
[ "$(echo "$r" | jq -r .summary.verdict)" != fail ] # the job fails on FAIL
Cuando termina, la misma respuesta trae el summary y los hallazgos que requieren atención, primero los nuevos bloqueos legítimos (recortado):
{
"simulation_id": "7f3c9a2e-5b1d-4c8e-9f60-2a7d41e0b9c3",
"status": "succeeded",
"summary": {
"verdict": "fail",
"headline": "Would have blocked 2 legitimate flows (9,442 executions) in the last 30 days.",
"window": { "start": "2026-09-10T14:02:11Z", "end": "2026-10-10T14:02:11Z", "days": 30,
"requested_days": 30, "cut": false, "retention_days": 90 },
"events": 1842310,
"fingerprints": 2914,
"evaluated_statements": 41206,
"budget": 200000,
"budget_exhausted": false,
"coverage": { "ratio": 0.996, "evaluated_executions": 1207553, "inferred_executions": 627312,
"not_replayable_executions": 7445, "not_covered_executions": 0 },
"categories": {
"newly_blocked": { "fingerprints": 3, "executions": 9443,
"legitimate_fingerprints": 2, "legitimate_executions": 9442 },
"newly_flagged": { "fingerprints": 0, "executions": 0 },
"no_longer_blocked": { "fingerprints": 0, "executions": 0 },
"unchanged": { "fingerprints": 2911, "executions": 1825422 },
"drift": { "fingerprints": 3, "executions": 12 }
},
"candidates": [
{ "code": "SIM-001", "kind": "custom_new", "name": "no-physical-deletes", "matched_executions": 9443 },
{ "code": "SIM-002", "kind": "custom_new", "name": "ledger-append-only", "matched_executions": 0 }
],
"query_text_mode": "raw",
"note": null
},
"findings": [
{
"category": "newly_blocked",
"fingerprint": "4be1c07a9d3f6e21",
"rule_codes": ["SIM-001"],
"statement_kind": "DELETE",
"tables": ["cart_cartitem"],
"executions": 9412,
"days_seen": 30,
"sources": { "tcp": 9412 },
"identities": [{ "type": "db_user", "ref": "app_web", "name": "app_web", "executions": 9412 }],
"recorded": { "ALLOWED": 9412 },
"outcomes": { "current": "ALLOWED", "candidate": "BLOCKED" },
"legitimate": true,
"confidence": "structural",
"samples": [{ "event_id": "…", "created_at": "2026-10-10T13:58:40Z",
"text": "DELETE FROM \"cart_cartitem\" WHERE \"cart_cartitem\".\"id\" = $1" }]
}
]
}
Límites de la API: 6 simulaciones por hora por key, además del límite diario del workspace. Errores: 400 (un YAML o un código de regla no válido), 403 PLAN_UPGRADE_REQUIRED (plan Free), 409 SIMULATION_ALREADY_ACTIVE (ya hay una en curso; la respuesta trae su simulation_id), 429 (límite alcanzado) y 503 SIMULATOR_UNAVAILABLE (el simulador no está disponible en ese momento).
Leer el reporte
| Categoría | Qué significa |
|---|---|
Nuevos bloqueos (newly_blocked) | Hoy pasa y con el cambio se bloquearía. Los flujos legítimos de esta categoría son la señal de «esto rompería». |
Nuevas alertas (newly_flagged) | Seguiría ejecutándose, pero quedaría marcado. |
Dejan de bloquearse (no_longer_blocked) | Hoy se bloquea y con el cambio pasaría: revisa que sea lo que quieres. |
Deriva (drift) | Lo registrado difiere de lo que deciden hoy tus reglas y el motor. No lo causa tu cambio y no cuenta para el veredicto. |
Un flujo es legítimo cuando todas sus ejecuciones registradas pasaron (ninguna bloqueada), corrió al menos 3 veces en al menos 2 días distintos (ajustable con min_executions y min_days) y al menos la mitad de sus ejecuciones vienen de identidades de la aplicación: los usuarios de base de datos (proxy TCP) y las API keys (API de runtime) que aparecen en al menos la mitad de los días de la ventana.
El veredicto:
- FAIL: los flujos legítimos con nuevos bloqueos superan
fail_on_break(por defecto 0), sea cual sea la cobertura. - INCONCLUSIVE: la cobertura es menor al 95% de las ejecuciones, la ventana quedó más corta que la pedida (por la retención del plan o la antigüedad del workspace), o el presupuesto se agotó con flujos mixtos entre los nuevos bloqueos.
- WARN: no supera
fail_on_break, pero algo cambia: nuevas alertas, nuevos bloqueos de flujos no legítimos o reglas que dejan de bloquear. - PASS: ninguna decisión cambia en la ventana, salvo los bloqueos legítimos que tolere
fail_on_break.
Cada hallazgo indica las reglas que lo deciden, el tipo de sentencia y las tablas, las ejecuciones (evaluadas e inferidas), los días en que apareció, el canal (tcp o http), las identidades que lo emitieron (hasta 10), el resultado registrado y la confianza: structural, literal-dependent (la regla depende de literales) o mixed (las sentencias del flujo no coinciden entre sí). Incluye hasta 3 muestras, siempre con los literales reemplazados, con un enlace al evento en el Audit Trail.
Ejemplo: endurecer los DELETE sin romper la aplicación
Ejemplo ilustrativo con datos de prueba: la aplicación, las tablas y las cifras no son de un cliente.
Un equipo de DevOps opera una aplicación Django sobre PostgreSQL detrás del proxy TCP. Las reglas estándar ya bloquean DELETE y UPDATE sin WHERE. Como los pedidos y los pagos usan borrado lógico, quiere ir más allá: bloquear todo DELETE y hacer que el libro contable sea de solo inserción (ninguna regla de UPDATE sobre billing_ledgerentry).
La simulación de 30 días da FAIL: «Would have blocked 2 legitimate flows (9,442 executions)». Los dos flujos son comportamiento real de la aplicación: quitar un artículo del carrito (app_web, 9.412 ejecuciones en 30 de 30 días) y la limpieza diaria de sesiones de Django (app_cron, 30 ejecuciones). El tercer nuevo bloqueo, un DELETE manual de un pedido hecho una sola vez por un usuario de guardia, no se clasifica como legítimo: es justo lo que el equipo quiere frenar. La regla del libro contable no coincidió con nada en 30 días, que es la evidencia que buscaba.
El equipo ajusta el YAML: limita la regla de DELETE a las tablas con borrado lógico (relation: "shop_order" y relation: "shop_payment") y mantiene la del libro contable. La nueva simulación da WARN con 0 flujos legítimos bloqueados: solo queda el DELETE manual. Entonces crea las reglas y las aplica.
Límites
- Ventana. La que eliges en cada ejecución, hasta 30 días en Builder, 90 en Team y según contrato (hasta 180) en Enterprise, recortada por la retención del historial de tu plan y por la antigüedad del workspace. Si queda más corta que la pedida, el veredicto es INCONCLUSIVE.
- El historial es evidencia, no prueba. Solo se cubren los flujos que corrieron en la ventana: un proceso mensual puede quedar fuera.
- Qué tráfico se reproduce. El del proxy TCP y el de la API de runtime. Los checks del CLI, la API de validación y MCP validan SQL antes de que se ejecute: no son ejecuciones en producción y no se reproducen.
- Telemetría en modo
sanitized. Los literales ya llegaron reemplazados, así que las reglas que dependen de ellos (where_always_true, VERICTO-002, 003, 030 y 090) pueden reportar de menos. El reporte lo indica (query_text_mode: "sanitized") y marca esos hallazgos comoliteral-dependent. - Eventos no reproducibles. Los textos que no se pueden descifrar, las evaluaciones que fallan cerradas y los eventos de bases eliminadas se cuentan con sus ejecuciones (
not_replayable_executions), pero no se evalúan. - Tamaño y duración. Hasta 20 cambios por simulación y 15 minutos por ejecución. Los resultados se conservan 7 días.
Permisos, privacidad y auditoría
- Iniciar y cancelar una simulación requiere el rol
membero superior (permisorules.simulate) en un plan Builder o superior. - Ver los reportes requiere el rol
viewer(rules.view). Las muestras requierenmember, igual que en el Audit Trail: unviewerve los conteos, no las sentencias. - Desde CI, una API key con el scope
rules:simulate, que crean los admins del workspace. - Privacidad. Los textos se descifran solo en el backend de Vericto, durante la simulación. Los resultados guardan huellas, conteos, identidades y muestras con los literales siempre reemplazados, sea cual sea el modo de telemetría: ningún valor, tampoco de una columna sensible, se guarda ni se muestra.
- Auditoría. Cada simulación queda en el registro de auditoría del workspace como
rule.simulation.requested(canal, API key, hash de los cambios, ventana y base de datos), y cada cancelación comorule.simulation.cancelled.