SCA, SAST y DAST: cómo agregué seguridad de verdad a mi pipeline
Durante mucho tiempo, CI/CD para mí era solo build, test y deploy. Ninguna de esas etapas responde si el código es seguro. Implementé SCA, SAST y DAST en un pipeline real y aprendí en la práctica qué encuentra cada uno — y qué no reemplaza ninguno.
Durante mucho tiempo, cuando pensaba en CI/CD, pensaba básicamente en esto:
Si compilaba y los tests pasaban, listo.
Pero existe un problema: ninguna de estas etapas responde necesariamente si el código es seguro.
Una aplicación puede compilar, pasar todos los tests y seguir siendo vulnerable a SQL Injection, usar una dependencia con un CVE conocido o incluso tener un secreto commiteado en el repositorio.
Fue estudiando seguridad de aplicaciones que empecé a implementar un pipeline de DevSecOps en un proyecto vulnerable que creé justamente para eso.
Y tres conceptos aparecieron todo el tiempo:
SCA, SAST y DAST.
Parecen hacer lo mismo, pero analizan partes completamente distintas de la aplicación.
Primero: ¿cuál es la diferencia entre SCA, SAST y DAST?
La forma más simple que encontré de pensarlos es:
SCA analiza lo que instalaste. SAST analiza lo que escribiste. DAST analiza cómo se comporta tu aplicación cuando alguien intenta romperla.
En un pipeline, podemos imaginarlo así:
No son competidores.
Uno encuentra problemas que el otro muchas veces ni siquiera puede ver.
1. SCA — Software Composition Analysis
Hoy prácticamente ninguna aplicación se escribe desde cero.
En mi backend Node.js, por ejemplo, tengo dependencias como Express, librerías para JWT, acceso a PostgreSQL y varias otras.
El problema es que:
Una vulnerabilidad no tiene que estar en el código que escribí yo.
Puede estar en una librería que instalé.
Ahí es exactamente donde entra el SCA.
En el proyecto, empecé con:
npm auditnpm audit compara el árbol de dependencias del proyecto con advisories de vulnerabilidades conocidas.
Así fue como encontré, por ejemplo, una versión vulnerable de jsonwebtoken.
O sea: podría revisar todo mi código y nunca encontrar ese problema.
El problema estaba en la composición del software.
En el pipeline agregué:
- name: Audit dependencies
run: npm audit --audit-level=highAhora el CI también verifica las dependencias.
Y --audit-level=high empieza a introducir otro concepto importante: security gate.
Puedo decidir que las vulnerabilidades menores se reporten, mientras que las de riesgo High o Critical bloqueen esa ejecución del pipeline.
2. SAST — Static Application Security Testing
Después de las dependencias, viene el código que escribimos nosotros.
Ahí fue cuando agregué SAST.
Su característica principal es que el SAST puede buscar patrones potencialmente vulnerables sin necesidad de atacar una aplicación en ejecución.
En mi caso usé dos herramientas:
- CodeQL
- Semgrep
CodeQL es particularmente interesante porque convierte el código en una representación consultable y busca flujos que correspondan a problemas de seguridad.
En el pipeline:
- uses: github/codeql-action/init@v4
with:
languages: javascript-typescript
queries: security-extended
- uses: github/codeql-action/analyze@v4También agregué Semgrep:
- name: Install Semgrep
run: pip install semgrep
- name: Run Semgrep
run: semgrep scan --config=autoDurante las pruebas, CodeQL logró señalar problemas que pasarían fácilmente desapercibidos en una revisión rápida, como log injection.
La idea acá es distinta a la del SCA:
Pero todavía existe una limitación importante.
Estamos analizando el código.
No estamos necesariamente observando el comportamiento real de la aplicación mientras está en ejecución.
Ahí es donde entra la parte que me pareció más interesante.
3. DAST — Dynamic Application Security Testing
Con el DAST dejé de simplemente analizar archivos.
Levanté la aplicación y dejé que una herramienta intentara atacarla.
Usé OWASP ZAP.
Mi pipeline pasó a hacer aproximadamente esto:
El ZAP recibe mi especificación OpenAPI y empieza a testear los endpoints.
Y ahí apareció una diferencia enorme respecto al SAST.
Puede hacer requests como:
GET /notes/search?q='
Authorization: Bearer <token>y observar qué pasa.
En uno de los scans, ese request resultó en:
500 Internal Server ErrorEl ZAP entonces levantó una alerta de posible:
Eso no significa automáticamente que todo hallazgo del scanner sea una vulnerabilidad confirmada.
En este caso, por ejemplo, la confianza reportada era baja. El siguiente paso es analizar la implementación, reproducir el comportamiento y determinar si realmente existe SQL Injection.
Esta parte es fundamental:
El scanner encuentra indicios. El desarrollador igual necesita entender el finding.
Un problema que encontré implementando DAST
Al principio, el scan parecía funcionar.
Solo que casi todas las peticiones estaban devolviendo:
401 UnauthorizedEl ZAP estaba "testeando" mi API, pero no lograba llegar justamente a los endpoints que quería testear.
Fue necesario crear un usuario específico para el DAST durante la ejecución del pipeline:
Después apareció otro problema.
Rate limiting.
Había agregado rate limits justamente como protección de seguridad, pero un scanner envía una cantidad enorme de requests.
Resultado:
El scanner volvía a perder cobertura.
Este fue un aprendizaje importante: no basta con poner una herramienta de seguridad en el CI y asumir que está testeando tu aplicación correctamente.
Un scan verde con autenticación rota puede ser mucho peor que un scan rojo.
Da una falsa sensación de seguridad.
El secret scanning también se sumó al pipeline
Además de las tres principales, agregué también Gitleaks.
El objetivo es distinto:
Un secreto encontrado puede significar que esa credencial ya tiene que considerarse comprometida.
En GitHub Actions quedó algo así:
- uses: gitleaks/gitleaks-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Eso complementa a los otros scanners.
Cómo quedó el pipeline
Después de juntar todo:
Pero todavía faltaba algo.
¿Qué pasa cuando alguna herramienta encuentra una vulnerabilidad?
Los findings también necesitan llegar a algún lugar
Otra cosa que implementé fue lograr que los resultados de los scanners no se perdieran en el log de GitHub Actions.
Con OWASP ZAP, por ejemplo, se puede permitir que la propia Action registre los resultados en un GitHub Issue:
allow_issue_writing: trueAsí, el finding no existe solo durante esa ejecución del pipeline. Se convierte en algo que se puede seguir y tratar dentro del propio repositorio.
Con CodeQL, la integración es todavía más directa.
Los resultados del análisis se envían al code scanning de GitHub y aparecen en el área Security and quality, donde se pueden ver los alerts, la severidad, el archivo afectado y seguir la corrección.
Otras herramientas de análisis estático también pueden integrar sus resultados a GitHub a través del formato SARIF:
GitHub interpreta ese archivo y transforma los resultados en code scanning alerts dentro del repositorio.
Entonces empezamos a separar dos responsabilidades importantes:
Porque reportar un problema e impedir que llegue a producción son cosas distintas.
Ahí es donde entran los security gates.
Encontrar una vulnerabilidad no es suficiente
Un scanner puede simplemente generar un reporte y dejar que el pipeline continúe.
Para transformar eso en una política de seguridad de verdad, necesitamos definir security gates.
Por ejemplo:
Si una vulnerabilidad viola la política:
Y hay una distinción importante acá.
Que el pipeline falle no significa automáticamente que nadie pueda hacer merge.
El repositorio también necesita exigir ese status check a través de las reglas de protección de la branch.
Entonces el flujo completo pasa a ser:
Acá es donde la seguridad deja de ser solo un reporte que nadie lee y empieza a formar parte del proceso de desarrollo.
Ninguna de estas herramientas reemplaza a las otras
Esta fue probablemente la mayor conclusión que saqué implementando todo esto.
Imaginá que instalo una librería vulnerable:
SCA puede encontrarlo.
Ahora imaginá que escribo una query SQL concatenando input del usuario:
SAST puede encontrarlo.
Ahora imaginá que determinado payload provoca un comportamiento inesperado solo cuando la base de datos, la autenticación, el middleware y la aplicación funcionan juntos:
DAST puede encontrarlo.
Y imaginá que tengo una falla de autorización donde:
Tal vez ningún scanner lo encuentre de forma confiable.
Ahí es donde entran la revisión humana, tests específicos y el Threat Modeling.
La seguridad no es:
"Instalé el ZAP, así que ahora mi aplicación es segura."Es mucho más parecido a:
Cada capa cubre una parte distinta del problema.
Lo que cambió en mi forma de ver el CI/CD
Antes pensaba:
"El pipeline verifica si mi código funciona."
Ahora prefiero pensar:
"El pipeline verifica algunas de las condiciones necesarias para confiar en que ese código puede seguir adelante."
Que el build pase no significa código correcto.
Que el test pase no significa código seguro.
Que el SAST pase no significa una aplicación segura.
Que el DAST pase tampoco significa una aplicación segura.
Pero cuando combinamos varias de estas técnicas, aumentamos bastante nuestra capacidad de encontrar problemas antes de que lleguen a producción.
Y, para mí, esa fue la parte más interesante de estudiar DevSecOps en la práctica:
la seguridad deja de ser una etapa hecha al final del proyecto y pasa a formar parte del ciclo normal de desarrollo.