TL;DR
"Vibe coding" —describir en lenguaje natural y dejar que Cursor, Aider, Cline o Claude Code escriban el código— es ya la norma en muchos equipos. El problema: el código generado pasa review humano menos cuidadoso, acumula vulnerabilidades (OWASP top ten), dependencias sin vetar y patrones que un humano nunca escribiría por defecto. Este artículo arma un pipeline de QA que corre en cada commit: Semgrep con reglas específicas para código generado por IA, Bearer para detección de PII y secretos, Trivy para CVEs en contenedores, SonarQube Community con rules custom, DSPy para generar tests automáticos a partir del diff, Promptfoo para validar prompts antes de merge y Gitleaks para credenciales. Pre-commit, CI y runtime. El equipo sigue vibeando; el sistema detiene el riesgo.
El problema concreto
En un proyecto típico con vibe coding intensivo observamos 1000 LOC/semana generados. De esos, un 8-12% tienen al menos uno de:
- SQL inyectable por concatenación de strings.
eval()/exec()sobre input no validado.- Secretos hardcoded "temporalmente" que pasan a prod.
- Librerías nuevas sin vetar (supply chain risk).
- Prompts con inyección inadvertida (instrucciones embebidas que el modelo respeta).
- Manejo de errores silencioso (
except: pass). - Endpoints sin autenticación porque "lo arreglaremos después".
El revisor humano los pierde porque el PR tiene 400 líneas y el sello de "hecho con Cursor" genera confianza implícita falsa.
Arquitectura del pipeline
Developer → Cursor / Aider / Cline / Claude Code
│
▼
┌──────────────────────────────────────────────┐
│ pre-commit │
│ ├─ gitleaks (secrets) │
│ ├─ semgrep (rules quick) │
│ ├─ bearer (PII/sensitive data) │
│ └─ black/ruff/eslint │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ CI (GitHub Actions) │
│ ├─ semgrep full (OWASP + AI-specific) │
│ ├─ sonarqube community │
│ ├─ trivy fs/image │
│ ├─ bearer full scan │
│ ├─ dspy: generación + ejecución de tests │
│ ├─ promptfoo: validación de prompts │
│ └─ license check (supply chain) │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ runtime │
│ ├─ dependency-track (SBOM monitoring) │
│ ├─ falco / tetragon (syscall anomalies) │
│ └─ traces a Langfuse (si hay LLMs) │
└──────────────────────────────────────────────┘
Pre-commit: rápido y local
.pre-commit-config.yaml:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
- repo: https://github.com/returntocorp/semgrep
rev: v1.95.0
hooks:
- id: semgrep
args:
- --config=p/owasp-top-ten
- --config=p/secrets
- --config=./.semgrep/ai-gen-rules.yml
- --error
- --skip-unknown-extensions
- repo: https://github.com/Bearer/bearer
rev: v1.49.0
hooks:
- id: bearer
args: ["scan", ".", "--severity=critical,high,medium", "--fail-on-severity=critical,high"]
- repo: local
hooks:
- id: forbidden-patterns
name: "AI gen anti-patterns"
entry: scripts/forbidden_patterns.sh
language: script
scripts/forbidden_patterns.sh:
#!/usr/bin/env bash
set -e
# patrones comunes que Cursor/GPT tienden a generar y no deben llegar a prod
grep -nR --include=*.py --include=*.ts --include=*.go \
-E '(except:\s*pass|# FIXME|# TODO:|console\.log\("DEBUG|print\("DEBUG)' \
-- "$@" && {
echo "❌ anti-patterns encontrados"
exit 1
} || true
Semgrep: reglas específicas para IA
Archivo .semgrep/ai-gen-rules.yml:
rules:
- id: hardcoded-api-key
pattern-either:
- pattern: api_key = "sk-..."
- pattern: $KEY = "pk_live_..."
- pattern: ANTHROPIC_API_KEY = "sk-ant-..."
message: "Secret hardcoded. Usa env vars."
severity: ERROR
languages: [python, javascript, typescript, go]
- id: llm-without-timeout
pattern-either:
- pattern: openai.ChatCompletion.create(...)
- pattern: anthropic.messages.create(...)
pattern-not-inside: |
$CTX.with_timeout(...)
message: "Llamada LLM sin timeout; puede colgar el proceso."
severity: WARNING
languages: [python]
- id: prompt-concat-user-input
patterns:
- pattern-either:
- pattern: |
$PROMPT = f"...{$USER_INPUT}..."
...
$LLM.create(messages=[{"role": "system", "content": $PROMPT}, ...])
- pattern: |
$PROMPT = "..." + $USER_INPUT + "..."
...
$LLM.create(...)
message: "Posible prompt injection: input de usuario concatenado al system prompt."
severity: ERROR
languages: [python, typescript]
- id: sql-string-concat
pattern-either:
- pattern: 'cursor.execute(f"SELECT ... {$VAR} ...")'
- pattern: 'conn.Exec("SELECT ... " + $VAR + " ...")'
message: "SQL por concatenación. Usa parámetros."
severity: ERROR
languages: [python, go]
- id: agent-tool-no-idempotency
patterns:
- pattern: |
@tool
def $FN(...):
...
- pattern-not-inside: |
def $FN(..., operation_id: str, ...):
...
message: "Tool agent-side sin operation_id; falta idempotencia."
severity: WARNING
languages: [python]
Estas reglas atrapan lo que SAST clásico no: patrones propios de código IA (prompt injection, tools sin idempotencia, LLM sin timeout).
SonarQube Community Edition
SonarQube genérico se complementa con rules específicas. Archivo .sonar/rules.xml aplicado por sonar-scanner:
- Complejidad ciclomática >10 por función.
- Funciones con >60 líneas (tenderán a ser mal comprendidas por humano).
- Falta de tests para funciones con side effects.
- Duplicación de código >3%.
Para código generado, baja los umbrales temporalmente (ej. complejidad <8) y sube progresivamente — el modelo se acostumbra a escribir más limpio cuando el feedback es claro.
DSPy: tests generados del diff
La idea: tomar el diff del PR, pasarlo por un modelo de DSPy que genera casos de prueba basados en intenciones evidentes, y ejecutarlos.
import dspy
class GenerateTestFromDiff(dspy.Signature):
"""Genera tests pytest para funciones modificadas. Usa nombres descriptivos, cubre happy path y al menos 2 edge cases."""
diff: str = dspy.InputField()
file_path: str = dspy.InputField()
tests: str = dspy.OutputField()
lm = dspy.LM("anthropic/claude-sonnet-4-6", api_base="https://api.numoru.com/v1", api_key=os.environ["LITELLM_MASTER_KEY"])
dspy.configure(lm=lm)
generator = dspy.ChainOfThought(GenerateTestFromDiff)
def generate_and_run(diff: str, file_path: str):
out = generator(diff=diff, file_path=file_path)
tests_file = write_to_tmp(out.tests)
result = subprocess.run(["pytest", tests_file, "-v"], capture_output=True)
return result
Si los tests auto-generados fallan, el PR no pasa. Esto detecta regresiones que el humano olvidó cubrir.
Promptfoo: validar prompts antes de merge
Cuando el PR modifica un archivo prompts/*.txt o *.md, Promptfoo corre una suite contra el prompt nuevo y compara contra el anterior. Detalles en evals de agentes en CI/CD.
Trivy: CVEs en contenedores y dependencias
# .github/workflows/security.yml
- name: Trivy fs scan
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: "1"
- name: Trivy image scan
if: github.event_name == 'pull_request'
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/numoru/app:${{ github.sha }}
severity: CRITICAL,HIGH
Para código generado por IA, la package.json y requirements.txt tienden a crecer con dependencias sugeridas por el modelo — CVEs aparecen rápido.
Supply chain: bloqueo de dependencias nuevas
Problema: Cursor sugiere import superlib sin avisar. La PR agrega un paquete nuevo que nadie vetó.
Solución: pre-commit hook que detecta nuevas líneas en package.json/requirements.txt y exige justificación en el commit message.
#!/usr/bin/env bash
set -e
DIFF=$(git diff --cached --unified=0 -- 'package.json' 'requirements.txt' 'go.mod')
if echo "$DIFF" | grep -q '^+[^+-]'; then
if ! git log --format=%B -n 1 HEAD | grep -q 'new-dep:'; then
echo "❌ nuevas dependencias detectadas. Agrega 'new-dep: <motivo>' al commit message"
exit 1
fi
fi
GitHub Actions completo
name: secure-ai-gen
on: [pull_request]
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: returntocorp/semgrep-action@v1
with:
config: >
p/owasp-top-ten
p/secrets
p/ci
./.semgrep/ai-gen-rules.yml
bearer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bearer/bearer-action@v2
with:
scan-command: "scan . --report=security --severity=critical,high"
sonarqube:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: SonarSource/sonarqube-scan-action@v3
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: https://sonar.numoru.com
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@master
with: { scan-type: fs, severity: "CRITICAL,HIGH", exit-code: "1" }
dspy-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install dspy-ai pytest
- name: Generate + run tests from diff
env:
LITELLM_MASTER_KEY: ${{ secrets.LITELLM_MASTER_KEY }}
run: python scripts/gen_tests_from_diff.py
promptfoo:
if: contains(github.event.pull_request.changed_files, 'prompts/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm i -g promptfoo
- run: promptfoo eval -c promptfoo.yml
Reglas adicionales para Aider/Cline/Cursor
Archivo .cursorrules (funciona también para Cline y Windsurf):
# Reglas Numoru — código generado debe cumplir:
1. Nunca hardcodear API keys, usa env vars via pydantic settings / viper / dotenv.
2. Toda llamada a un LLM debe tener timeout (<30s) y retry con backoff.
3. Los prompts construidos con input de usuario deben usar delimitadores claros (<input>...) ).
4. Las funciones con efectos externos deben aceptar operation_id: str para idempotencia.
5. SQL siempre con parámetros; nunca f-string o concat.
6. Los tests deben existir para toda función pública. Cobertura mínima 70%.
7. En Python, nunca except: pass. En Go, nunca ignora err.
8. Los logs no deben incluir PII en claro; hashea email y teléfono.
Esto sesga al modelo durante la generación, no solo después.
Runtime: detección post-deploy
Dos herramientas valen la pena:
- Falco / Tetragon — eBPF-based runtime security. Alerta si un container hace exec a /bin/sh, abre puertos inesperados o escribe en rutas sensibles.
- Dependency-Track — ingiere SBOMs periódicamente y avisa cuando una dependencia usada hoy tiene CVE reciente.
Falco policy mínima para aplicaciones IA:
- macro: llm_service
condition: (proc.name in (uvicorn, gunicorn, node, go-binary))
- rule: LLM service spawning shell
desc: "Nunca debería spawnear shell"
condition: spawned_process and proc.pname in (uvicorn, gunicorn) and proc.name in (bash, sh, dash, zsh)
output: "LLM service spawned shell (command=%proc.cmdline)"
priority: CRITICAL
tags: [ai, runtime]
Métricas que trackear
- Findings críticos por 1000 LOC (objetivo: <0.5 en 90 días).
- Tiempo medio entre commit y merge (añadir el pipeline añade ~5-8 min; ok).
- Tasa de rechazo de PR (si sube a >30%, el pipeline está mal calibrado o los devs necesitan formación).
- CVE-exposure window (días desde que una CVE se publica hasta que nuestro scan la detecta en prod).
Mismo equipo y herramientas IA; la diferencia es si corre el pipeline pre-commit + CI. Observado 90 días en 4 equipos clientes Numoru.
- Antes del pipeline
- Después (90 días)
Telemetría de engagements Numoru, 2024-2026.
Impacto de negocio y casos
Por qué comprarlo como servicio
Los equipos saben que lo necesitan, pero rara vez lo construyen solos porque cada pieza (reglas Semgrep, tuning SonarQube, test-gen DSPy, reglas Falco en runtime) es un rabbit hole. Numoru lo entrega como install de 2-3 semanas más auditoría de hardening opcional. La venta es esencialmente seguro, cotizado en horas de ingeniería contra el costo asimétrico de una vulnerabilidad shipeada.
Industrias y rangos de ticket
Install + auditoría por comprador (Numoru, USD)
Benchmarks públicos
GitHub — research de Copilot y productividad IA
Snyk — State of open source security 2024
Caso ilustrativo — SaaS de 40 ingenieros adoptando pipeline
Fintech LATAM growth-stage con 40 ingenieros que usan Cursor + Claude Code intensivamente
Calculadora ROI — asegurar org de ingeniería IA-heavy
Fintech de 40 ingenieros adoptando pipeline (12 meses)
| Install (one-time) | −$18,500 |
| Retainer (12 mo × $900) | −$10,800 |
| Infra (Sonar + Semgrep Pro trial) | −$720 |
| Tiempo CI adicional (144 h × $92) | −$13,248 |
| Incidente ponderado evitado | +$153,000 |
| Valor de readiness compliance | +$35,000 |
| Contribución neta año 1 | +$144,732 |
Tiers de pricing Numoru
- Semgrep + Gitleaks + Trivy
- SonarQube Community básico
- Pre-commit hook + GitHub Actions
- Walkthrough al equipo
- Warranty 30 días
- Todo lo del Starter
- Test-gen DSPy + Promptfoo
- Bearer (PII) + supply-chain
- Reglas Falco runtime
- Retainer de tuning de reglas
- Auditoría trimestral
- Alcance multi-repo / monorepo
- Alineamiento SOC 2 / ISO 27001
- Catálogo custom reglas Semgrep
- Integración Airflow + Notify.io
- Ingeniero hardening dedicado
- Engagement red-team anual
Anti-patrones humanos
- Desactivar reglas "temporalmente". Se queda. Mejor: PR con fix del false positive en las rules.
- Aceptar
--skip-unfixedsiempre. Hace el scan cosmético. - No entrenar al equipo. Si el dev no entiende por qué SonarQube falla, apaga el check. Formación mensual corta.
- Sólo ejecutar en CI. Pre-commit ahorra ciclos CI y da feedback inmediato.
- Confiar en que "el modelo aprenderá". No aprende entre PRs; las reglas deben ser explícitas.
FAQ
¿Se vuelve insoportablemente lento?Pre-commit queda bajo 20s si las reglas están bien filtradas. CI completo 5-12 min. Mejor que un incidente en prod.
¿Funciona para equipos pequeños?Sí. Lo mínimo: gitleaks + semgrep + trivy + pre-commit. Cuesta ~30 min de setup.
¿Detecta prompt injection de runtime?Parcialmente. SAST atrapa concat peligroso; guardrails en runtime (NeMo, Rebuff) atrapan lo que pase. Son complementarios.
¿Qué hago cuando el PR tiene 40 findings de Semgrep?Priorizar por severidad: ERROR bloquea, WARNING se queda como tech debt con issue tagueado. En la primera semana se genera ruido; se calibra a 2-5 findings por PR.
¿Sonar Community es suficiente o necesito Enterprise? Community alcanza para equipos <15 personas. Enterprise aporta branch analysis decente y cross-repo, útil en monorepo.
Próximos pasos
Pipeline completo publicado en github.com/numoru-ia/secure-ai-codegen-template. Incluye pre-commit, GitHub Actions, .cursorrules, Semgrep rules custom y políticas Falco. Forkear + correr make bootstrap. Siguiente pieza de la serie: cómo enlazar estas métricas al dashboard de Langfuse + Grafana para visibilidad completa code-to-runtime.
La contraparte para agentes en vez de código está en los evals en CI/CD que frenan una regresión antes de producción. Y como ejemplo de código que sí conviene escribir a mano, un servidor MCP desde cero en Go.