Contexto: de las specs al enforcement
Este es el tercer articulo de una serie sobre como gestionar un sitio estatico con agentes IA y especificaciones declarativas:
- OpenCode Enterprise: Implementacion para Platform Engineering — El agente IA que vive en tu terminal.
- OpenSpec: Spec-Driven Development para Platform Engineering — El framework de especificaciones que define las reglas.
- Este post — La capa de seguridad que hace cumplir esas reglas automaticamente.
El problema que resolvemos hoy es simple: tener especificaciones sin enforcement automatico equivale a tener specs muertas. Puedes definir la CSP perfecta en un YAML, pero si nadie valida que _headers coincide con esa spec antes de cada deploy, el YAML es documentacion decorativa.
El primer paso es establecer una base solida de validaciones locales con herramientas open-source y hooks de Git: una capa que funciona antes de que el codigo llegue al repositorio remoto y que en el futuro se complementara con CI/CD en GitHub Actions.
Antes vs despues
Para entender el impacto real de esta implementacion, veamos como era el flujo de trabajo antes y como es ahora:
Sin DevSecOps (antes):
| Paso | Accion | Validacion |
|---|---|---|
| Editar post | Modificar markdown | Ninguna |
Editar _headers | Cambiar CSP manualmente | Ninguna, confianza ciega |
| Commit | git add . && git commit | Ninguna |
| Deploy | git push origin developer | Ninguna |
| Detectar error | Un lector reporta header roto | Dias o semanas despues |
Con DevSecOps (ahora):
| Paso | Accion | Validacion |
|---|---|---|
| Editar post | Modificar markdown | OpenSpec valida frontmatter en pre-commit |
Editar _headers | Cambiar CSP manualmente | generate-headers.js --validate bloquea si diverge de spec |
| Commit | git add . && git commit | ESLint, Bandit, JSON integrity, headers check |
| Pre-deploy | ./scripts/pre-deploy-check.sh | Gitleaks + SAST completo + 35 checks OpenSpec |
| Deploy | git push origin developer | Solo si pre-deploy pasa |
La diferencia fundamental no es la cantidad de herramientas sino cuando se detecta el error: antes se descubria en produccion (o peor, no se descubria); ahora se bloquea antes de que el commit exista. El coste de corregir un problema crece exponencialmente con cada fase que avanza sin ser detectado.
Arquitectura de la solucion
La capa DevSecOps se compone de tres niveles de validacion que se ejecutan en momentos diferentes del flujo de trabajo:
Nivel 1: Pre-commit (automatico, en cada commit)
| Herramienta | Proposito | Que valida |
|---|---|---|
| ESLint + eslint-plugin-security | SAST JavaScript | eval(), inyeccion, timing attacks |
| Bandit | SAST Python | pickle, subprocess, hashes inseguros |
| validate-json-integrity.js | Integridad de datos | URLs maliciosas, dominios no autorizados |
| generate-headers.js | Compliance de headers | _headers contra specs YAML |
| OpenSpec validate:post | Schema de posts | Frontmatter contra post.schema.yaml |
Nivel 2: Pre-deploy (manual, antes de push)
| Herramienta | Proposito | Cobertura |
|---|---|---|
| Gitleaks | Deteccion de secretos | Todo el repositorio |
| ESLint (scan completo) | SAST JavaScript | Todos los scripts JS |
| Bandit (scan completo) | SAST Python | Todos los scripts Python |
| OpenSpec validate:deploy | 35 checks | Headers, CSP, SEO, redirects, sitemap |
Nivel 3: OpenSpec compliance (bajo demanda)
node scripts/admin.js reportGenera un score de compliance del 0% al 100% validando schemas, policies, catalogos y ficheros del sitio.
Implementacion: paso a paso
1. Herramientas de seguridad
Las dependencias se dividen en tres categorias:
JavaScript (npm devDependencies):
npm install -D eslint @eslint/js eslint-plugin-security eslint-plugin-unicorneslint-plugin-security detecta patrones peligrosos en Node.js: uso de eval(), require() con argumentos dinamicos, RegExp con input de usuario, object injection sinks y timing attacks potenciales.
Python (pip):
pip install banditBandit es el SAST estandar para Python. Analiza el AST (Abstract Syntax Tree) y detecta uso de pickle (deserializacion insegura), subprocess con shell=True, funciones hash debiles (MD5, SHA1) y eval().
Binario (Gitleaks):
curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz | tar -xz
mv gitleaks ~/.local/bin/Gitleaks escanea el repositorio completo (incluyendo historial Git) buscando API keys, tokens, claves privadas y passwords hardcodeados.
2. Configuracion de ESLint con reglas de seguridad
El fichero eslint.config.mjs configura ESLint en formato flat config (ESLint 9+):
import js from "@eslint/js";
import security from "eslint-plugin-security";
export default [
js.configs.recommended,
security.configs.recommended,
{
languageOptions: {
ecmaVersion: 2022,
sourceType: "commonjs",
},
rules: {
"security/detect-eval-with-expression": "error",
"security/detect-non-literal-require": "error",
"security/detect-non-literal-regexp": "warn",
"security/detect-object-injection": "warn",
"security/detect-possible-timing-attacks": "warn",
"no-implied-eval": "error",
"no-new-func": "error",
"no-eval": "error",
},
},
{
ignores: [
"assets/**", "posts/**", "page/**",
"category/**", "node_modules/**", "*.min.js",
],
},
];El bloque ignores es critico: los directorios de contenido HTML generado (posts/, assets/) se excluyen del SAST porque no son codigo ejecutable en servidor. Solo se analizan los scripts en scripts/ y .openspec/.
3. Configuracion de Bandit para Python
El fichero .bandit.yaml define que tests ejecutar y cuales omitir:
exclude_dirs:
- assets
- posts
- node_modules
- .git
skips:
- B101 # assert_used (aceptable en scripts de admin)
- B404 # import_subprocess (necesario para build scripts)
tests:
- B301 # pickle
- B307 # eval
- B310 # urllib sin validacion SSL
- B311 # random no criptografico
- B324 # hashlib inseguro
- B602 # subprocess con shell=TrueSe omiten B101 (uso de assert) y B404 (import de subprocess) porque son patrones legitimos en scripts de administracion. El resto de tests se enfoca en riesgos reales de ejecucion de codigo y deserializacion.
4. Configuracion de Gitleaks
El fichero .gitleaks.toml extiende las reglas por defecto y anade deteccion especifica para nuestro stack:
[extend]
useDefault = true
[[rules]]
id = "cloudflare-api-token"
description = "Cloudflare API Token"
regex = '''(?i)cloudflare[_\-]?(?:api[_\-]?)?(?:token|key)\s*[:=]\s*['"]?([a-zA-Z0-9_\-]{40,})['"]?'''
keywords = ["cloudflare"]
[[rules]]
id = "anthropic-api-key"
description = "Anthropic API Key"
regex = '''sk-ant-api[0-9a-zA-Z_\-]{30,}'''
keywords = ["sk-ant"]
[allowlist]
description = "False positives for Red Orbita"
paths = [
'''posts/.*\.html$''',
]El allowlist de paths es fundamental: los posts HTML contienen ejemplos de API keys en tutoriales y writeups de CTF. Sin esta exclusion, Gitleaks reportaria decenas de falsos positivos.
5. Validacion de integridad JSON
El script validate-json-integrity.js protege los ficheros JSON de mapeo contra inyecciones:
const CONFIG = {
allowedDomains: [
'red-orbita.com', 'giscus.app',
'fonts.googleapis.com', 'youtube.com',
],
maliciousPatterns: [
/javascript:/i, /vbscript:/i,
/<script/i, /eval\s*\(/i,
/__proto__/i,
],
filesToCheck: [
'url-mapping.json', 'redirects-mapping.json',
'search-index.json', 'deep-categories.json',
],
};El script recorre recursivamente cada valor string de los JSON y verifica que:
- No contiene patrones de inyeccion (
javascript:,,proto) - Las URLs absolutas apuntan exclusivamente a dominios en el allowlist
- El JSON es sintacticamente valido y parseable sin errores
- No existen valores con longitud anormal que sugieran data exfiltration
Esto es relevante porque url-mapping.json y _redirects controlan a donde se redirigen las peticiones HTTP. Una URL maliciosa inyectada en estos ficheros podria redirigir usuarios a phishing. Ademas, search-index.json alimenta el buscador del cliente: un payload XSS insertado ahi se ejecutaria en el navegador de cada visitante.
Ejemplo de ejecucion:
node scripts/validate-json-integrity.js
# Salida esperada:
# Checking url-mapping.json... OK (633 entries)
# Checking redirects-mapping.json... OK (633 entries)
# Checking search-index.json... OK (655 entries)
# Checking deep-categories.json... OK (7 entries)
# All 6 files passed integrity checks6. Generacion automatica de _headers desde OpenSpec
Mantener el fichero _headers de Cloudflare Pages sincronizado manualmente con las specs YAML es propenso a errores. Una directiva CSP olvidada o un header HSTS con un max-age incorrecto puede pasar desapercibido durante semanas.
El script generate-headers.js cierra este gap leyendo las specs y generando o validando el fichero de produccion:
# Validar que _headers cumple las specs
node .openspec/scripts/generate-headers.js --validate
# Regenerar _headers desde las specs (si divergen)
node .openspec/scripts/generate-headers.js --generateEn modo --validate, lee security-headers.spec.yaml y csp-directives.spec.yaml y comprueba que cada header requerido esta presente en _headers con el valor correcto. Para CSP, verifica que:
- No hay directivas prohibidas (
unsafe-inline,unsafe-eval) - No hay origenes de tracking no autorizados (Google Analytics, Facebook Pixel)
- Las directivas
default-src,script-src,style-srcyimg-srccoinciden con la spec - El header
Content-Security-Policy-Report-Onlyno esta presente en produccion
En modo --generate, toma las specs como fuente de verdad unica y regenera _headers completo. Esto garantiza que cualquier cambio en las politicas de seguridad se propague automaticamente al fichero desplegado.
El beneficio principal es convertir las specs en documentacion viva: si modificas csp-directives.spec.yaml para permitir un nuevo origen de imagenes, un solo comando regenera el _headers de produccion sin edicion manual.
Estructura de ficheros DevSecOps
Para tener una vision completa de donde vive cada pieza, esta es la estructura de ficheros relevante:
.openspec/
├── policies/
│ ├── security-headers.spec.yaml # Headers HTTP requeridos
│ ├── csp-directives.spec.yaml # Directivas CSP permitidas/prohibidas
│ └── seo-requirements.spec.yaml # Requisitos SEO minimos
├── schema/
│ └── post.schema.yaml # Schema frontmatter de posts
├── validation/
│ ├── validate-post.js # Validador de posts contra schema
│ ├── validate-deploy.js # 35 checks pre-deploy
│ └── report-compliance.js # Generador de compliance score
├── scripts/
│ └── generate-headers.js # Genera/valida _headers desde specs
├── hooks/
│ └── pre-commit # Git hook de validacion
└── catalogs/
├── categories-catalog.yaml # 7 categorias oficiales
└── tags-vocabulary.yaml # Vocabulario controlado de tags
scripts/
├── admin.js # CLI principal de OpenSpec
├── build.py # Builder del sitio (homepage, paginacion)
├── new-post.py # Builder de posts individuales
├── delete-post.py # Eliminador de posts y artefactos
├── pre-deploy-check.sh # 6 validaciones pre-deploy
└── validate-json-integrity.js # Integridad de JSON de mapeo
# Ficheros de configuracion en raiz
.gitleaks.toml # Reglas de deteccion de secretos
.bandit.yaml # Configuracion SAST Python
eslint.config.mjs # Configuracion SAST JavaScriptEl patron de diseno es intencionado: las specs (fuente de verdad) viven en .openspec/, los scripts de ejecucion viven en scripts/, y la configuracion de herramientas vive en la raiz. Esta separacion permite que un cambio en una politica de seguridad (csp-directives.spec.yaml) se propague a traves de los scripts de validacion sin tocar la configuracion de las herramientas SAST.
Automatizacion: hooks de Git
Hook pre-commit
El hook .openspec/hooks/pre-commit se ejecuta automaticamente en cada git commit. Solo analiza los ficheros staged (modificados), no todo el repo:
#!/bin/bash
set -e
# 1. Posts modificados -> OpenSpec validate:post
CHANGED_POSTS=$(git diff --cached --name-only --diff-filter=ACM | grep '^content/.*\.md$' || true)
if [ -n "$CHANGED_POSTS" ]; then
for file in $CHANGED_POSTS; do
node .openspec/validation/validate-post.js "$file"
done
fi
# 2. JS modificados -> ESLint security
JS_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^(scripts/.*\.js|\.openspec/.*\.js)$' || true)
if [ -n "$JS_FILES" ]; then
npx eslint $JS_FILES
fi
# 3. Python modificados -> Bandit
PY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^scripts/.*\.py$' || true)
if [ -n "$PY_FILES" ]; then
bandit -c .bandit.yaml $PY_FILES -ll
fi
# 4. JSON modificados -> Integridad
JSON_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.json$' || true)
if [ -n "$JSON_FILES" ]; then
node scripts/validate-json-integrity.js
fi
# 5. _headers modificado -> Validar contra specs
if git diff --cached --name-only | grep -q '^_headers$'; then
node .openspec/scripts/generate-headers.js --validate
fiEl patron || true en los grep es importante: si no hay ficheros que coincidan, grep devolveria exit code 1, y con set -e el hook abortaria prematuramente.
La instalacion es un solo comando:
cp .openspec/hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commitA partir de ese momento, cada git commit ejecuta automaticamente las validaciones relevantes. Si alguna falla, el commit se aborta con un mensaje claro indicando que regla se ha violado. Para casos excepcionales (hotfixes urgentes), existe git commit --no-verify, pero su uso deberia ser la excepcion documentada, no la norma.
Script pre-deploy
El script scripts/pre-deploy-check.sh ejecuta las 6 validaciones completas antes de un push. La diferencia clave con el pre-commit es que el pre-deploy:
- Ejecuta Gitleaks sobre todo el historial del repositorio (demasiado lento para pre-commit, pero critico antes de publicar)
- Analiza todos los ficheros JS y Python, no solo los staged
- Ejecuta OpenSpec validate:deploy con las 35 validaciones completas (headers, CSP, SEO, redirects, sitemap, schemas)
./scripts/pre-deploy-check.shLa razon de este paso manual es que Gitleaks necesita escanear todo el historial de Git para detectar secretos que se commitearon y luego se borraron (un git rm no elimina el secreto del historial). Este scan tarda entre 10-30 segundos en un repo con 655 posts, tiempo aceptable antes de un deploy pero no en cada commit.
Resultados
Tras la implementacion, este es el estado del sistema:
========================================
Red Orbita - Pre-deploy validation
========================================
[1/6] Escaneando secretos con Gitleaks...
No se detectaron secretos
[2/6] ESLint security scan (JS)...
0 errores, 15 warnings (esperados en scripts CLI)
[3/6] Bandit SAST scan (Python)...
0 issues
[4/6] Validando integridad JSON...
6/6 archivos validos
[5/6] Validando _headers contra .openspec/...
_headers is compliant with .openspec/ policies
[6/6] OpenSpec validate:deploy...
PASS: 35, WARN: 0, FAIL: 0
========================================
Todas las validaciones OK
========================================Y el reporte de compliance de OpenSpec:
=== Red Orbita - Reporte de Cumplimiento OpenSpec ===
SCHEMAS: 4/4 PASS
POLICIES: 4/4 PASS
CATALOGS: 1/1 PASS
SITE-FILES: 9/9 PASS
CONTENT: 655 posts publicados
Compliance score: 100%Los 15 warnings de ESLint son security/detect-non-literal-fs-filename y security/detect-non-literal-regexp en los scripts de validacion. Son falsos positivos esperados: un script CLI que valida ficheros necesita usar fs.readFileSync() con paths dinamicos. Se mantienen como warnings (no se suprimen) para que sean visibles en caso de que aparezcan nuevos patrones genuinos.
Gestion de falsos positivos
Toda herramienta SAST genera falsos positivos. La diferencia entre un setup util y uno que se ignora es como se gestionan. Estas son las reglas que seguimos:
Regla 1: No suprimir, clasificar. Los 15 warnings de ESLint no se eliminan con // eslint-disable-next-line. Se mantienen visibles. Si manana aparece un warning 16, sera visible precisamente porque los otros 15 son conocidos y esperados. Suprimir warnings crea puntos ciegos.
Regla 2: Documentar cada exclusion. Cada skip en .bandit.yaml y cada entrada en el allowlist de .gitleaks.toml tiene un comentario explicando por que se excluye. Cuando otra persona (o tu yo futuro) revise la configuracion, debe entender la razon sin necesidad de arqueologia en commits.
Regla 3: Revisar periodicamente. Una exclusion que era valida hace seis meses puede no serlo hoy. El reporte de compliance (node scripts/admin.js report) incluye el conteo de warnings y exclusiones activas. Si el numero crece sin justificacion, algo ha cambiado.
Patrones concretos de falsos positivos en este proyecto:
| Herramienta | Warning | Razon | Accion |
|---|---|---|---|
| ESLint | detect-non-literal-fs-filename | Scripts CLI leen paths de argv | Mantener como warning |
| ESLint | detect-non-literal-regexp | Validadores construyen regex dinamicas | Mantener como warning |
| ESLint | detect-object-injection | Acceso a objetos por key variable | Revisar caso a caso |
| Bandit | B101 assert_used | Asserts en scripts de admin | Skip en .bandit.yaml |
| Bandit | B404 import_subprocess | Build scripts necesitan subprocess | Skip en .bandit.yaml |
| Gitleaks | API keys en posts HTML | Tutoriales con ejemplos de keys | Allowlist por path |
Uso diario y comandos de referencia
| Comando | Proposito | Frecuencia |
|---|---|---|
git commit | Validaciones automaticas (pre-commit hook) | Cada cambio |
./scripts/pre-deploy-check.sh | Validacion completa pre-push | Antes de cada deploy |
node scripts/admin.js validate:deploy | 35 checks de OpenSpec | Antes de cada deploy |
node scripts/admin.js report | Reporte de compliance | Semanal o bajo demanda |
node .openspec/scripts/generate-headers.js --validate | Validar _headers contra specs | Cuando cambian policies |
node .openspec/scripts/generate-headers.js --generate | Regenerar _headers desde specs | Cuando cambian policies |
npm run security:lint | ESLint en todos los JS | Bajo demanda |
bandit -c .bandit.yaml scripts/ -ll | Bandit en todos los Python | Bajo demanda |
Para anadir nuevas reglas al sistema, el flujo es:
- Modificar la spec YAML correspondiente en
.openspec/policies/o.openspec/schema/ - Ejecutar
node scripts/admin.js reportpara verificar coherencia - Si es un cambio de headers:
node .openspec/scripts/generate-headers.js --generate - Commit y verificar que el pre-commit hook valida correctamente
Coste
| Componente | Coste |
|---|---|
| ESLint + plugins | Gratuito (npm) |
| Bandit | Gratuito (pip) |
| Gitleaks | Gratuito (binario open-source) |
| OpenSpec | Gratuito (custom) |
| Servicios SaaS | Ninguno |
| Total | 0 EUR/mes |
Como replicar esto en tu proyecto
Esta implementacion es especifica de Red Orbita, pero el patron es portable a cualquier sitio estatico o proyecto web. Los pasos minimos para replicarlo:
1. Instalar las herramientas (5 minutos):
# JavaScript SAST
npm install -D eslint @eslint/js eslint-plugin-security
# Python SAST (si tienes scripts Python)
pip install bandit
# Deteccion de secretos
curl -sSL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_8.21.2_linux_x64.tar.gz | tar -xz2. Crear el hook pre-commit (2 minutos):
Crea .git/hooks/pre-commit con las validaciones que necesites. Empieza con lo minimo: ESLint para JS o Bandit para Python. Anade mas checks gradualmente cuando el equipo se acostumbre al flujo.
3. Definir tus specs (variable):
Este es el paso que mas tiempo lleva, pero tambien el que mas valor aporta. No necesitas OpenSpec: un fichero YAML que defina "estos son los headers obligatorios" ya es una spec. Lo importante es que exista una fuente de verdad que un script pueda validar.
4. Conectar specs con validacion:
Un script que lea tu spec YAML y compare con la realidad (_headers, package.json, .env.example). No necesita ser sofisticado: un diff entre lo esperado y lo real ya es enforcement.
Lo que NO necesitas:
- No necesitas CI/CD para empezar. Un hook de Git local es suficiente como primera capa.
- No necesitas un framework. Cinco scripts de shell y las herramientas open-source hacen el trabajo.
- No necesitas cobertura del 100% desde el dia uno. Empieza con SAST basico y deteccion de secretos, el resto viene despues.
Conclusion y proximos pasos
La combinacion de OpenCode (agente IA), OpenSpec (especificaciones) y una capa DevSecOps local crea un flujo de trabajo donde las reglas de seguridad no son documentacion pasiva sino gates activos que bloquean codigo inseguro antes de que llegue al repositorio.
Los tres niveles de enforcement se complementan:
- Pre-commit: validacion instantanea de los ficheros modificados. Coste cero en friccion, maximo impacto en calidad.
- Pre-deploy: scan completo del repositorio incluyendo deteccion de secretos en el historial. La ultima linea de defensa antes de publicar.
- Compliance bajo demanda: vision global del estado del sitio. Util para auditorias periodicas y para verificar que no hay drift entre specs y realidad.
Hoy estas validaciones se ejecutan localmente con un hook de Git y cinco herramientas open-source. Es una base solida que cubre SAST, deteccion de secretos, integridad de datos y compliance de headers para un sitio con 655 posts y 633 reglas de redireccion.
El siguiente paso natural es llevar estas mismas validaciones a GitHub Actions, ejecutandolas automaticamente en cada pull request y antes de cada deploy a Cloudflare Pages. La logica ya esta encapsulada en scripts independientes (pre-deploy-check.sh, admin.js validate:deploy), asi que la migracion a CI/CD sera una cuestion de orquestacion, no de reescritura.
Specs vivas, enforcement automatico, seguridad por defecto.
Implementaras esta capa en tu proyecto? Cuentame en los comentarios.
Comentarios