Skip to content

Repository files navigation

CSP Reporting Platform

Plataforma centralizada y open source para recibir, normalizar, almacenar y consultar reportes de Content Security Policy (CSP) en el edge de Cloudflare.

Diseñada como un único Worker Nuxt/Nitro que actúa como colector público (un solo endpoint), procesa los reportes de forma asíncrona (Queue → R2 + D1) y expone un dashboard de administración para visualizarlos y gestionar los orígenes autorizados.


✨ Características

  • Un único endpoint público de ingestión (POST /api/report) compatible con report-to, report-uri y Reporting-Endpoints.
  • Pipeline asíncrono y resiliente: el Worker recibe, normaliza y encola; un consumer (mismo Worker) persiste en R2 (raw NDJSON) y D1 (índice consultable).
  • Allowlist por aplicación (Activa/Desactivada): solo los orígenes autorizados envían reportes; los desactivados se rechazan con 403 sin borrar su historial.
  • Modelo por aplicación (hostname + path): un mismo dominio puede tener varios aplicativos (app.example.com/app1, app.example.com/app2).
  • URL inmutable al editar: la identidad (hostname/path) se fija al crear para no re-etiquetar eventos históricos.
  • Dashboard responsive (Nuxt UI v4): eventos, detalle con raw preservado, catálogo de aplicaciones y métricas de Overview.
  • Idempotente: reintentos con backoff exponencial, dead-letter en R2 y ON CONFLICT DO NOTHING para evitar duplicados.
  • Privacidad por diseño: hash de IP de origen (sin guardar IPs crudas).

🏗️ Arquitectura

  Orígenes (IIS, SPA, etc.)
   report-to / report-uri
            │
            ▼
  POST /api/report  →  validate → normalize → enqueue → 202
                                                  │
                               Cloudflare Queue (csp-report)
                                                  │
                            Consumer (hook cloudflare:queue)
                                                  │
                              ┌───────────────────┴───────────────────┐
                              │                                       │
                              ▼                                       ▼
                     R2 (raw NDJSON)                        D1 (índice)
                     csp-reports/…                       events · applications · daily_stats

Un solo Worker Nuxt/Nitro es productor y consumidor de la cola mediante el hook nativo cloudflare:queue de Nitro. Los reintentos y dead-letters se gestionan dentro del propio consumidor.

Flujo de un reporte

  1. El navegador/origen envía el reporte a POST /api/report.
  2. El endpoint valida (Content-Type, tamaño, esquema Zod) y responde 202 de inmediato — nunca escribe síncronamente.
  3. Encola el reporte normalizado en la Queue.
  4. El consumer recibe el batch, guarda el raw en R2 y el índice en D1 (evento + agregación diaria), re-encolando con backoff ante fallos.

🧱 Stack

Capa Tecnología
Framework Nuxt 4 (Nitro) + Vue 3.5
UI Nuxt UI v4 + Tailwind CSS v4
Compute Cloudflare Workers (preset cloudflare-module)
Cola Cloudflare Queues
Almacenamiento Cloudflare R2 (raw) + Cloudflare D1 (índice SQLite)
ORM Drizzle ORM + drizzle-kit
Validación Zod
Estado Pinia + VueUse
Tests Vitest

📁 Estructura de directorios

├── app/                      # Frontend (Nuxt 4)
│   ├── components/           # Componentes UI y del dashboard
│   ├── layouts/              # default / dashboard / auth
│   ├── pages/                # /, /events, /events/[id], /applications
│   ├── app.config.ts         # Tema Nuxt UI
│   └── app.css               # Tailwind v4 + @theme
├── server/                   # Backend (Nitro)
│   ├── api/                  # Endpoints (/api/report, /api/events, …)
│   ├── services/             # Lógica: ingest, storage (R2/D1), analytics
│   ├── plugins/              # Hook cloudflare:queue (consumer)
│   ├── database/             # Schema Drizzle + migraciones
│   └── utils/                # db.ts, env.ts
├── shared/                   # Tipos y utilidades compartidas app ↔ server
├── public/                   # Assets estáticos
├── wrangler.jsonc            # Config del Worker (bindings)
├── drizzle.config.ts         # Config de migraciones
└── worker-configuration.d.ts # Tipos generados (no editar)

🚀 Requisitos previos

  • Node.js 20+ y npm
  • Cuenta de Cloudflare con Workers habilitado
  • Recursos de Cloudflare creados (ver más abajo)
  • wrangler autenticado: npx wrangler login

Crear los recursos de Cloudflare

wrangler d1 create csp-report      # → Database ID
wrangler r2 bucket create csp-reports
wrangler queues create csp-report

Completa los IDs resultantes en wrangler.jsonc (bindings DB, R2, CSP_REPORT_QUEUE).


⚙️ Instalación y configuración

npm install
npm run cf-typegen    # regenera worker-configuration.d.ts desde wrangler.jsonc

Variables de entorno

Copia .env.example a .env (solo para drizzle-kit, migraciones por HTTP) y completa:

CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_DATABASE_ID=
CLOUDFLARE_D1_TOKEN=   # Cloudflare Dashboard → My Profile → API Tokens

Para secretos del runtime usa wrangler secret put (p. ej. IP_HASH_SALT).


🖥️ Desarrollo local

npm run dev          # dev con nitro-cloudflare-dev (bindings remotos reales)
npm run preview      # build + wrangler dev local (ejercita la cola)

El config usa "remote": true en D1/R2: el desarrollo local opera contra los recursos remotos. Para aislarte, crea recursos de staging y ajusta los IDs.


🔌 Endpoint público de ingestión

POST https://<tu-dominio>/api/report

Content-Types soportados: application/reports+json, application/csp-report, application/json.

Caso Status
Aceptado 202 Accepted
Método no permitido 405
Content-Type no soportado 415
JSON inválido 400
Esquema inválido 400
Payload demasiado grande (> 64 KB) 413
Origen no autorizado (allowlist) 403
Error de encolado 500

Configuración del lado del emisor

Content-Security-Policy: ... ; report-uri https://<tu-dominio>/api/report
Report-To: { "group": "csp", "max_age": 10886400, "endpoints": [ { "url": "https://<tu-dominio>/api/report" } ] }

🎛️ Dashboard y APIs administrativas

El dashboard (Nuxt UI v4) consume las APIs internas; ambas deben protegerse con Cloudflare Access (perímetro) antes de producción.

Ruta Descripción
GET /api/events Paginado con filtros (hostname, directiva, disposición, URL, fechas)
GET /api/events/:id Detalle + raw preservado (leído de R2)
GET /api/applications Catálogo con reportes 24h y última actividad
POST /api/applications Crear aplicación autorizada
PATCH /api/applications/:id Actualizar metadatos (no la URL)
DELETE /api/applications/:id Borrado en cascada (eventos D1 + raw R2)
GET /api/stats/overview Métricas agregadas

🟢 Estados de aplicación (allowlist)

Cada origen registrado tiene uno de dos estados:

Estado Comportamiento
Activo Autorizado: acepta reportes (202)
Desactivado Suspendido: rechaza con 403 sin borrar historial

La URL (hostname + path) es inmutable tras la creación: define la identidad que se asocia a los eventos, por lo que la edición solo permite cambiar aplicación, entorno y estado. Esto evita re-etiquetar eventos históricos.


🗄️ Base de datos y migraciones

Schema Drizzle en server/database/schema.ts (tablas applications, events, daily_stats).

npm run db:generate   # genera SQL en server/database/migrations desde el schema
npm run db:migrate    # aplica migraciones a D1 (drizzle-kit, driver d1-http)
npm run db:studio     # UI visual de Drizzle
npm run db:pull       # introspect (revisión)

Nota sobre D1 y batches: D1 limita las variables enlazadas por sentencia (~100). El consumer divide los inserts multi-fila en chunks seguros para soportar batches grandes de la cola.


✅ Pruebas

npm test              # unitarias + integración + seguridad (Vitest)
npm run test:watch
npx nuxi typecheck

📦 Build y deploy

npm run build
npm run deploy        # build + wrangler deploy

🔁 Reintentos y dead-letter

El hook cloudflare:queue de Nitro envuelve la ejecución en waitUntil sin propagar errores; por ello el consumidor no lanza: ante un fallo transitorio re-encola el mensaje con backoff exponencial (hasta 5 intentos) usando delaySeconds; agotados los intentos (o ante mensajes inválidos) escribe el evento en R2 bajo el prefijo dead-letter/ para su revisión.


🔒 Seguridad

  • Collector privado: solo orígenes Activa aceptan reportes (allowlist).
  • Privacidad: IP de origen solo se guarda hasheada.
  • Limits: tamaño máximo de payload y validación estricta de esquema.
  • Dashboard protegido: diseñado para desplegarse detrás de Cloudflare Access (con Entra ID/Google como proveedor).

Mejoras recomendadas (fase 3)

  • Cloudflare Access + Entra ID sobre dashboard y APIs administrativas.
  • WAF / rate limiting sobre el endpoint público.
  • Lifecycle rules de R2 para el raw (p. ej. 90 días de retención).
  • Headers de seguridad y observabilidad/alertas.

📄 Licencia

MIT

About

Plataforma open source para recibir, normalizar, almacenar y consultar reportes CSP en el edge de Cloudflare. Un único Worker Nuxt/Nitro actúa como colector público, procesa reportes asíncronamente mediante Queues → R2 + D1 y ofrece un dashboard para visualización y gestión de orígenes autorizados.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages