diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml
index 8631094..fb3a3f1 100644
--- a/.gitea/workflows/build.yaml
+++ b/.gitea/workflows/build.yaml
@@ -260,6 +260,51 @@ jobs:
--exit-code 0 \
"${MANIFEST_FILE}"
+ # Modo auditoría: sin --error a propósito, Semgrep siempre termina
+ # con exit 0 aunque reporte hallazgos. Es la primera vuelta — se
+ # revisan los resultados en conjunto antes de decidir qué reglas
+ # deberían pasar a bloquear el pipeline más adelante.
+ - name: Escaneo SAST (Semgrep) — modo auditoría, no bloquea
+ shell: bash
+ run: |
+ set -euo pipefail
+ SEMGREP_IMAGE="semgrep/semgrep:1.173.0"
+
+ docker run --rm \
+ -v "${{ github.workspace }}:/src" \
+ -w /src \
+ "${SEMGREP_IMAGE}" \
+ semgrep scan \
+ --config=p/typescript \
+ --config=p/react \
+ --config=p/nextjs \
+ --config=p/security-audit \
+ --json \
+ --output=semgrep-report.json \
+ "${APP_DIR}"
+
+ echo "=== Resumen Semgrep ==="
+ docker run --rm \
+ -v "${{ github.workspace }}:/src" \
+ -w /src \
+ "${SEMGREP_IMAGE}" \
+ python3 -c "
+import json
+data = json.load(open('semgrep-report.json'))
+results = data.get('results', [])
+print(f'Hallazgos: {len(results)}')
+for r in results:
+ print(f\" [{r['extra']['severity']}] {r['check_id']} - {r['path']}:{r['start']['line']}\")
+"
+
+ - name: Publicar reporte de Semgrep
+ if: always()
+ uses: actions/upload-artifact@v3
+ with:
+ name: semgrep-report
+ path: semgrep-report.json
+ if-no-files-found: ignore
+
- name: Login en Gitea Registry
uses: docker/login-action@v2
with:
diff --git a/workloads/docs-portal/docs/devsecops/sast.md b/workloads/docs-portal/docs/devsecops/sast.md
new file mode 100644
index 0000000..bdc4324
--- /dev/null
+++ b/workloads/docs-portal/docs/devsecops/sast.md
@@ -0,0 +1,115 @@
+# SAST (Semgrep) — análisis estático de código
+
+!!! info "Qué problema resuelve"
+ SAST significa *Static Application Security Testing*: analizar el
+ **código fuente** en busca de patrones de programación inseguros, sin
+ ejecutar la aplicación. Semgrep lee cada archivo `.ts`/`.tsx` y lo
+ compara contra un catálogo de reglas — cada regla describe una forma
+ de escribir código que suele terminar en una vulnerabilidad conocida
+ (inyección, XSS, uso inseguro de una API, etc.).
+
+## En qué se diferencia de Gitleaks y Trivy
+
+Las tres herramientas ya integradas a este pipeline analizan cosas
+completamente distintas — vale la pena tenerlo claro porque a primera
+vista "escaneo de seguridad" suena como una sola categoría:
+
+| Herramienta | Qué mira | Pregunta que responde |
+|---|---|---|
+| [Gitleaks](gitleaks.md) | El texto de los archivos y el historial de git | "¿Hay una credencial commiteada?" |
+| [Trivy](trivy.md) | Paquetes instalados (imagen) y configuración (YAML) | "¿Alguna dependencia tiene una CVE conocida? ¿El manifiesto de Kubernetes es inseguro?" |
+| **Semgrep (SAST)** | La **lógica** del código que escribimos nosotros | "¿Esta función, tal como está escrita, abre una vulnerabilidad?" |
+
+Ninguna de las tres reemplaza a las otras. Una dependencia puede estar
+100% al día (sin CVEs, Trivy contento) y aun así el código propio puede
+construir una URL con un string sin sanitizar y quedar abierto a SSRF —
+eso solo lo detecta un análisis de la lógica del código, que es
+exactamente lo que hace Semgrep.
+
+## Qué tipo de bugs detecta (ejemplos genéricos)
+
+Los rulesets usados (`p/typescript`, `p/react`, `p/nextjs`,
+`p/security-audit`) cubren, entre otras cosas:
+
+- **Inyección**: construir queries, comandos de shell o URLs concatenando
+ strings con datos que vienen del usuario, en vez de usar una API
+ parametrizada.
+- **XSS en React/Next.js**: usar `dangerouslySetInnerHTML` con contenido
+ que no pasó por un sanitizador.
+- **SSRF**: hacer un `fetch()`/request server-side hacia una URL que
+ construye el propio usuario, sin validar el host de destino.
+- **Criptografía insegura**: algoritmos de hash débiles (`md5`, `sha1`)
+ usados para contraseñas o tokens, en vez de un KDF diseñado para eso.
+- **`eval` / `new Function()`** sobre datos no confiables.
+- **ReDoS**: expresiones regulares con backtracking exponencial que un
+ input malicioso puede usar para colgar el proceso.
+- **Prototype pollution**: merges/asignaciones dinámicas de objetos que
+ permiten sobrescribir `__proto__`.
+
+## Resultado real de esta primera corrida
+
+```text
+Scanning 72 files tracked by git with 292 Code rules:
+ ts 87 rules 39 files
+ js 81 rules 1 file
+ json 1 rule 6 files
+
+Ran 91 rules on 72 files: 0 findings.
+```
+
+`workloads/ecommerce` salió limpio con estos rulesets: **0 hallazgos**.
+
+!!! warning "0 hallazgos no significa 'código perfecto'"
+ Significa que ningún patrón conocido de estos 91 rules hizo match —
+ no es una garantía de ausencia de bugs, solo de ausencia de *estos*
+ patrones específicos. SAST tiene falsos negativos por naturaleza (un
+ bug de lógica de negocio nuevo, específico de esta app, no está en
+ ningún ruleset genérico). El valor de correr esto en cada build no es
+ "una vez limpio, siempre limpio" — es que si alguien introduce a
+ futuro uno de estos patrones conocidos (por ejemplo, un
+ `dangerouslySetInnerHTML` sin sanitizar en un componente nuevo), el
+ pipeline lo va a marcar en ese mismo push.
+
+## Por qué modo auditoría (no bloquea) en esta primera integración
+
+El step corre **sin** la flag `--error` a propósito — Semgrep siempre
+termina con `exit 0`, reporta lo que encuentra pero nunca frena el
+pipeline. Es la primera vez que esta herramienta corre sobre el repo: la
+idea es revisar juntos qué reglas de los 91 activos generan ruido (falsos
+positivos específicos de este código) antes de decidir cuáles deberían
+pasar a bloquear.
+
+```mermaid
+flowchart LR
+ A[Semgrep scan] --> B{hallazgos?}
+ B -- "sí" --> C["se reportan en el log
+ artifact JSON"]
+ B -- "no" --> C
+ C --> D["✅ pipeline sigue
(exit 0 siempre)"]
+```
+
+Una vez que se decida qué reglas son suficientemente confiables para
+esta app, el paso natural es agregar `--error` **con un subconjunto**
+de reglas (no las 91 completas) usando `--config` más específico o
+`.semgrepignore` / reglas individuales marcadas como bloqueantes —
+todavía no se hizo ese recorte.
+
+## Los rulesets elegidos
+
+- `p/typescript` — patrones generales de TypeScript/JavaScript.
+- `p/react` — específico de componentes React (hooks mal usados, XSS vía
+ props/render, etc.).
+- `p/nextjs` — patrones propios del framework (App Router, API routes,
+ middlewares).
+- `p/security-audit` — catálogo transversal de seguridad (inyección,
+ criptografía débil, deserialización insegura) sin atarse a un
+ framework específico.
+
+Son rulesets **públicos y gratuitos** del registro de Semgrep — no
+requieren cuenta ni login (`semgrep login` solo hace falta para acceder a
+reglas adicionales de pago, que no se usan acá).
+
+## Dónde ver el resultado
+
+El JSON completo (`semgrep-report.json`) se publica como artifact del
+pipeline (`semgrep-report`) en cada corrida, tenga o no hallazgos —
+mismo patrón que Gitleaks y Trivy.
diff --git a/workloads/docs-portal/mkdocs.yml b/workloads/docs-portal/mkdocs.yml
index 8c02f9e..ee971a8 100644
--- a/workloads/docs-portal/mkdocs.yml
+++ b/workloads/docs-portal/mkdocs.yml
@@ -101,6 +101,7 @@ nav:
- DevSecOps:
- Gitleaks (secretos): devsecops/gitleaks.md
- Trivy (imagen + IaC): devsecops/trivy.md
+ - SAST (Semgrep): devsecops/sast.md
extra:
social: