Webhooks
Configura destinos de webhook para recibir alertas de seguridad de cside mediante solicitudes HTTP POST con verificación de firma HMAC.
Cómo funcionan los webhooks
Los webhooks te permiten recibir notificaciones de cside como solicitudes HTTP POST a una URL que especifiques. Cuando se activa un disparador, cside envía un payload JSON a tu endpoint de webhook con detalles sobre el evento.
Puedes configurar webhooks en tres formatos:
- JSON - payload JSON estándar para integraciones personalizadas
- Slack - preformateado para canales de Slack
- Discord - preformateado para canales de Discord
Agregar un destino de webhook
Para agregar un webhook como destino en una configuración de notificación:
- Abre el panel de control y navega a Team Settings > Notifications
- Crea una nueva configuración de notificación o edita una existente
- En Send To, haz clic en Add destination y selecciona Webhook
- En el panel de Configuration, completa:
- URL: la URL donde quieres recibir las notificaciones de webhook (por ejemplo,
https://example.com/cside/notify) - Secret: una clave secreta usada para firmar las solicitudes de webhook (usada para la verificación HMAC)
- Format: elige JSON, Slack o Discord
- URL: la URL donde quieres recibir las notificaciones de webhook (por ejemplo,
- Haz clic en Save o Save & Test
El payload del webhook
cside envía una solicitud HTTP POST por evento, con Content-Type: application/json y un
encabezado x-cside-signature (consulta la verificación HMAC). Cada
payload incluye un campo event de nivel superior que identifica el tipo de evento, una
marca de tiempo, el equipo o dominio al que pertenece el evento y un objeto data
específico del evento.
El campo que contiene la marca de tiempo varía según el evento: alert.created y los
eventos de resumen usan timestamp, mientras que security_headers.changed usa sent_at.
Lee primero event y después analiza el resto del payload según las tablas siguientes.
Trata los campos desconocidos como opcionales para que los campos nuevos no rompan tu
integración.
IDs de eventos
| ID | Enviado por | Campo de marca de tiempo | Objeto data |
|---|---|---|---|
alert.created | Disparadores inmediatos, y el disparador Vulnerable Script Detected en su programación inmediata | timestamp | Objeto de alerta |
security_headers.changed | Security Headers Changed | sent_at | Cambio de cabeceras de seguridad |
pci_report.available | PCI Report (diario o semanal) | timestamp | Informe PCI |
csp_block_digest | Script Blocked by CSP (diario o semanal) | timestamp | Resumen de bloqueos CSP |
csp_policy_digest | CSP Policy Digest (diario o semanal) | timestamp | Resumen de política CSP |
vulnerable_script_digest | Vulnerable Script Detected (diario o semanal) | timestamp | Resumen de vulnerabilidades |
outbound_connections_pending_review | Outbound Connections Pending Review (diario o semanal) | timestamp | Conexiones salientes pendientes |
Solo alert.created y security_headers.changed respetan los formatos Slack y
Discord. Los eventos de resumen se envían siempre como JSON, sea cual sea el formato
configurado en el destino.
Save & Test envía un payload alert.created de ejemplo con el trigger_type del
disparador de la regla, incluso para los disparadores que normalmente entregan un resumen.
Todas las marcas de tiempo son cadenas ISO 8601 en UTC. Los IDs son cadenas decimales.
alert.created
Se envía por cada alerta que genera un disparador inmediato: Script Threat Detected, High-risk Vulnerability, Managed Flagged, Malicious Tag Manager, Malicious Outbound Connection y Vulnerable Script Detected cuando la regla está configurada para entregar de inmediato.
| Campo | Tipo | Descripción |
|---|---|---|
event | "alert.created" | El ID del evento |
timestamp | string | Cuándo se envió el webhook |
domain_id | string | ID del dominio en el que se produjo la alerta |
domain | string | Hostname del dominio en el que se produjo la alerta |
trigger_type | string | El disparador que produjo la alerta, consulta la tabla siguiente |
data | object | El objeto de alerta |
destination_id | string | ID del destino de webhook que recibió el evento |
Valores de trigger_type:
trigger_type | Disparador |
|---|---|
script_threat_detected_by_rules_engine | Script Threat Detected |
high_risk_vulnerability_detected | High-risk Vulnerability |
vulnerable_script_detected | Vulnerable Script Detected (programación inmediata) |
cside_managed_list_flagged | Managed Flagged |
malicious_tag_manager | Malicious Tag Manager |
malicious_outbound_connection | Malicious Outbound Connection |
Objeto de alerta
| Campo | Tipo | Descripción |
|---|---|---|
target_type | "URL" | "HASH" | "IP" | "DOMAIN" | "HOSTNAME" | "VENDOR" | Qué contiene target |
target | string | El objeto de la alerta: la URL de un script, el hash de un script, o la IP, el dominio o el hostname marcados |
action | "alert" | La acción registrada para la alerta. Actualmente siempre es "alert"; trátala como una cadena para seguir siendo compatible con acciones futuras |
ruleset_id | string | ID del ruleset que coincidió |
ruleset_name | string | Nombre del ruleset que coincidió |
ruleset_description | string | Descripción del ruleset que coincidió |
script_urls | string[] | Las URL de los scripts que activaron la alerta; puede estar vacío |
vuln_cve_id | string | null | ID de la vulnerabilidad (CVE o GHSA); null en alertas que no son de vulnerabilidad |
vuln_package | string | null | Paquete afectado; null en alertas que no son de vulnerabilidad |
vuln_severity | string | null | Severidad reportada, como HIGH; null en alertas que no son de vulnerabilidad |
vuln_reference_url | string | null | Enlace a la vulnerabilidad en osv.dev; null cuando no hay vuln_cve_id |
Los cuatro campos vuln_* solo se rellenan para vulnerable_script_detected y
high_risk_vulnerability_detected. Para cualquier otro trigger_type son null.
{
"event": "alert.created",
"timestamp": "2026-09-17T10:15:30.123456Z",
"domain_id": "1897425263682514945",
"domain": "checkout.example.com",
"trigger_type": "high_risk_vulnerability_detected",
"data": {
"target_type": "URL",
"target": "https://cdn.example.com/lodash.js",
"action": "alert",
"ruleset_id": "ruleset_vulnerability_detected",
"ruleset_name": "Vulnerability Detected",
"ruleset_description": "A script with a known vulnerability was loaded",
"script_urls": ["https://cdn.example.com/lodash.js"],
"vuln_cve_id": "CVE-2024-1234",
"vuln_package": "lodash",
"vuln_severity": "HIGH",
"vuln_reference_url": "https://osv.dev/vulnerability/CVE-2024-1234"
},
"destination_id": "1897425263682514950"
}
security_headers.changed
Lo envía Security Headers Changed cuando las cabeceras de seguridad observadas en las páginas de checkout de un dominio difieren entre dos ventanas de observación consecutivas.
| Campo | Tipo | Descripción |
|---|---|---|
event | "security_headers.changed" | El ID del evento |
sent_at | string | Cuándo se envió el webhook |
team_id | string | ID del equipo propietario del dominio |
data.domain_id | string | ID del dominio |
data.domain | string | Hostname del dominio |
data.checkout_paths | string[] | Los selectores de páginas de checkout que se observaron |
data.changes | object[] | Una entrada por cada cabecera que cambió |
data.changes[].header_name | string | La cabecera, por ejemplo content-security-policy |
data.changes[].from_value | string | El valor anterior; vacío cuando la cabecera se añadió |
data.changes[].to_value | string | El valor nuevo; vacío cuando la cabecera desapareció |
data.changes[].from_date | string | Inicio de la ventana de observación en la que se vio el valor anterior |
data.changes[].to_date | string | Inicio de la ventana de observación en la que se vio el valor nuevo |
pci_report.available
Lo envía PCI Report cuando se ha puesto en cola una ejecución diaria o semanal del informe para los dominios en alcance que tienen rutas de checkout configuradas.
| Campo | Tipo | Descripción |
|---|---|---|
event | "pci_report.available" | El ID del evento |
timestamp | string | Cuándo se envió el webhook |
organization_id | string | ID de la organización |
team_id | string | ID del equipo |
data.domain_ids | string[] | IDs de los dominios que cubre el informe |
data.domains | string[] | Hostnames de los dominios que cubre el informe, en el mismo orden |
data.starting_date | string | Inicio del periodo del informe |
data.ending_date | string | Fin del periodo del informe |
data.period | "daily" | "weekly" | La programación de la regla |
csp_block_digest
Lo envía Script Blocked by CSP con los informes de violación de Content Security Policy recopilados en el periodo. No se envía nada si no hubo violaciones.
| Campo | Tipo | Descripción |
|---|---|---|
event | "csp_block_digest" | El ID del evento |
timestamp | string | Cuándo se envió el webhook |
organization_id | string | ID de la organización |
team_id | string | ID del equipo |
data.period | "daily" | "weekly" | La programación de la regla |
data.start_date | string | Inicio del periodo del resumen |
data.end_date | string | Fin del periodo del resumen |
data.domains | object[] | Recuentos por dominio, ordenados por número de violaciones |
data.domains[].domain_id | string | ID del dominio |
data.domains[].domain | string | Hostname del dominio |
data.domains[].violation_count | number | Violaciones registradas para el dominio |
data.top_blocked_uris | string[] | Las URI bloqueadas con más frecuencia |
data.top_violated_directives | string[] | Las directivas violadas con más frecuencia |
data.total_violations | number | Total de violaciones en todos los dominios en alcance |
csp_policy_digest
Lo envía CSP Policy Digest por cada dominio en alcance cuya Content Security Policy recomendada cambió en el periodo. No se envía nada si ningún dominio cambió.
| Campo | Tipo | Descripción |
|---|---|---|
event | "csp_policy_digest" | El ID del evento |
timestamp | string | Cuándo se envió el webhook |
organization_id | string | ID de la organización |
team_id | string | ID del equipo |
data.period | "daily" | "weekly" | La programación de la regla |
data.domains | object[] | Una entrada por cada dominio cuya política cambió |
data.domains[].domain | string | Hostname del dominio |
data.domains[].domain_id | string | ID del dominio |
data.domains[].added | string[] | Hosts añadidos a la política recomendada |
data.domains[].removed | string[] | Hosts eliminados de la política recomendada |
data.domains[].flagged | string[] | Hosts eliminados que están en la lista de amenazas gestionada por cside |
data.domains[].host_count | number | Número de hosts en la política recomendada |
data.domains[].policy_header | string | El valor completo recomendado de la cabecera Content-Security-Policy |
vulnerable_script_digest
Lo envía Vulnerable Script Detected en una programación diaria o semanal con los hallazgos de vulnerabilidades rutinarias (no de alto riesgo) del periodo. No se envía nada si no hubo hallazgos.
| Campo | Tipo | Descripción |
|---|---|---|
event | "vulnerable_script_digest" | El ID del evento |
timestamp | string | Cuándo se envió el webhook |
organization_id | string | ID de la organización |
team_id | string | ID del equipo |
data.period | "daily" | "weekly" | La programación de la regla |
data.start_date | string | Inicio del periodo del resumen |
data.end_date | string | Fin del periodo del resumen |
data.domains | object[] | Recuentos por dominio |
data.domains[].domain_id | string | ID del dominio |
data.domains[].domain | string | Hostname del dominio |
data.domains[].vulnerability_count | number | Hallazgos registrados para el dominio |
data.top_vulnerabilities | object[] | Los hallazgos más frecuentes |
data.top_vulnerabilities[].vulnerability_id | string | ID de la vulnerabilidad (CVE o GHSA) |
data.top_vulnerabilities[].package | string | Paquete afectado |
data.top_vulnerabilities[].severity | string | Severidad reportada |
data.top_vulnerabilities[].occurrence_count | number | Cuántas veces se produjo el hallazgo |
data.total_vulnerabilities | number | Total de hallazgos en todos los dominios en alcance |
outbound_connections_pending_review
Lo envía Outbound Connections Pending Review con el número de hostnames salientes que tu equipo todavía no ha aprobado en Resources inventory. No se envía nada si no hay nada pendiente.
| Campo | Tipo | Descripción |
|---|---|---|
event | "outbound_connections_pending_review" | El ID del evento |
timestamp | string | Cuándo se envió el webhook |
organization_id | string | ID de la organización |
team_id | string | ID del equipo |
data.period | "daily" | "weekly" | La programación de la regla |
data.pending_hostname_count | number | Hostnames salientes distintos pendientes de revisión |
data.domain_count | number | Número de dominios en alcance en los que se observaron |
Verificación HMAC
Cada solicitud de webhook incluye un encabezado x-cside-signature que contiene un hash HMAC SHA256 del cuerpo de la solicitud, firmado con el secret que proporcionaste al crear el destino de webhook.
Siempre debes verificar esta firma para confirmar que la solicitud proviene de cside y prevenir ataques de repetición.
Cómo verificar la firma
Si estás usando JavaScript, usa nuestro paquete JavaScript para verificación integrada.
Para otros lenguajes, calcula el hash HMAC SHA256 del cuerpo crudo de la solicitud usando tu secret de webhook, y luego compáralo con el valor del encabezado x-cside-signature.
async function verifyHmac(body: string, signature: string, secret: string) {
const encoder = new TextEncoder();
const encodedBody = encoder.encode(body);
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signatureBuffer = await crypto.subtle.sign("HMAC", key, encodedBody);
const finalSig = Array.from(new Uint8Array(signatureBuffer))
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
return signature.toLowerCase() === finalSig;
}fn verify_hmac(body: &str, signature: &str, secret: &str) -> Result<(), &'static str> {
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).map_err(|_| "Invalid secret")?;
mac.update(body.as_bytes());
let result = mac.finalize();
let code_bytes = result.into_bytes();
let final_sig = hex::encode(code_bytes);
if signature.to_lowercase() == final_sig {
Ok(())
} else {
Err("Invalid signature")
}
}
// Add to Cargo.toml:
// [dependencies]
// hmac = "0.12"
// sha2 = "0.10"
// hex = "0.4"package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
)
func verifyHmac(body, signature, secret string) bool {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(body))
finalSig := hex.EncodeToString(h.Sum(nil))
return strings.ToLower(signature) == finalSig
}defmodule HmacVerifier do
def verify_hmac(body, signature, secret) do
final_sig =
:crypto.mac(:hmac, :sha256, secret, body)
|> Base.encode16(case: :lower)
String.downcase(signature) == final_sig
end
endimport hmac
import hashlib
import secrets
def verify_hmac(body: str, signature: str, secret: str) -> bool:
final_sig = hmac.new(
secret.encode('utf-8'),
body.encode('utf-8'),
hashlib.sha256
).hexdigest()
return signature.lower() == final_sigThanks for your feedback!