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
Para crear elementos de trabajo de Jira a partir de webhooks en cualquier plan, consulta Jira Automation.
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
cside firma cada solicitud con el encabezado x-cside-signature, pero no puede agregar encabezados personalizados. Si tu receptor se autentica con su propio encabezado, comprueba si también acepta el token en la URL, como hace Jira Automation.
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.
Cada payload JSON incluye además dos campos legibles para personas, summary (un título de
una línea) y description (detalles en varias líneas), tanto en el nivel superior como
repetidos dentro de data. Úsalos para rellenar un ticket o un mensaje de chat sin tener que
construir tu propio texto para cada evento. Se repiten dentro de data porque
Jira Automation trata de forma especial
una clave data de nivel superior.
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, security_headers.changed y pci_report.available respetan los
formatos Slack y Discord. Los demás 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.
Su summary empieza por [Test]. Security Headers Changed envía en su lugar un payload
security_headers.changed de ejemplo. Si tu endpoint responde con un error, el resultado de
la prueba muestra el código de estado HTTP que devolvió.
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 |
summary | string | Título de una línea, por ejemplo [cside] Malicious Script: {ruleset} on {domain}. Es el mismo texto que la integración de Jira usa como título del issue |
description | string | Explicación de la alerta en texto plano |
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 |
|---|---|---|
summary | string | Igual que el summary de nivel superior |
description | string | Igual que el description de nivel superior |
domain | string | Igual que el domain de nivel superior |
trigger_type | string | Igual que el trigger_type de nivel superior |
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",
"summary": "[cside] High-risk Vulnerability: CVE-2024-1234 on checkout.example.com",
"description": "cside detected a high-risk vulnerability on checkout.example.com: lodash. Severity: High.\n\nThis is a high-risk finding (malware or a high-severity vulnerability). Investigate it promptly: if it is malware, remove the script immediately and rotate any credentials it may have accessed; otherwise prioritize updating or removing the affected dependency.",
"data": {
"summary": "[cside] High-risk Vulnerability: CVE-2024-1234 on checkout.example.com",
"description": "cside detected a high-risk vulnerability on checkout.example.com: lodash. Severity: High.\n\nThis is a high-risk finding (malware or a high-severity vulnerability). Investigate it promptly: if it is malware, remove the script immediately and rotate any credentials it may have accessed; otherwise prioritize updating or removing the affected dependency.",
"domain": "checkout.example.com",
"trigger_type": "high_risk_vulnerability_detected",
"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 |
summary | string | [cside] Security headers changed on {domain} |
description | string | Una línea por cada cabecera que cambió: - {header}: {from} -> {to} |
data.summary | string | Igual que summary |
data.description | string | Igual que description |
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 el informe del periodo está listo, para los dominios en
alcance que tienen rutas de checkout configuradas. Se envía una vez por informe; una entrega
rechazada no se reintenta. Cada solicitud incluye además un encabezado
x-cside-delivery-id que no cambia si el mismo informe se entrega de nuevo, así que puedes
usarlo para eliminar duplicados.
| 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 |
summary | string | [cside / PCI DSS]: New PCI Compliance Report for {domains} (los tres primeros dominios y después “and N more”) |
description | string | Una frase que indica los dominios y las fechas, seguida del enlace al informe |
data.summary | string | Igual que summary |
data.description | string | Igual que description |
data.report_id | string | ID del informe |
data.report_url | string | Enlace para descargar el informe desde el panel de control de cside |
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" | "custom" | La duración del periodo del informe. custom se usa para un informe que cubre cualquier otro rango |
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 |
summary | string | Título de una línea del resumen |
description | string | Desglose del resumen en varias líneas |
data.summary | string | Igual que summary |
data.description | string | Igual que description |
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 |
summary | string | Título de una línea del resumen |
description | string | Desglose del resumen en varias líneas |
data.summary | string | Igual que summary |
data.description | string | Igual que description |
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 |
summary | string | Título de una línea del resumen |
description | string | Desglose del resumen en varias líneas |
data.summary | string | Igual que summary |
data.description | string | Igual que description |
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 |
summary | string | Título de una línea del resumen |
description | string | Desglose del resumen en varias líneas |
data.summary | string | Igual que summary |
data.description | string | Igual que description |
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!