Webhooks d’alerte de sécurité
Les alertes de sécurité parviennent aux personnes par e-mail. Pour les faire aussi parvenir à votre système d’incidents ou de tickets, abonnez une destination de webhook sortant à un ou plusieurs événements de sécurité. Chaque événement est un POST HTTPS signé, renvoyé jusqu’à ce que votre récepteur l’accepte, et enregistré dans le journal des livraisons.
Deux formats de corps sont disponibles par destination :
- Générique (par défaut) : l’événement JSON fremforge décrit ci-dessous.
- ServiceNow : un enregistrement d’incident que la Table API de ServiceNow accepte tel quel. Voir ServiceNow.
Événements
| Événement | Envoyé lorsque | Gravité |
|---|---|---|
security.finding.created | Un nouveau résultat ouvert apparaît : vulnérabilité de dépendance ou de conteneur, SAST, secret, politique de licences | critical, ou high si vous le choisissez |
security.malware.matched | Un malware dans un envoi (ClamAV), ou un paquet connu comme malveillant (OSV MAL-*) dans le SBOM d’une branche par défaut ou d’une image | critical |
security.pull.blocked | Un pull de conteneur ou de paquet a été refusé par le contrôle antimalware ou par votre politique block_pull d’analyse d’images | high |
security.policy.violation | Une violation de la politique d’authentification (durée de vie de jeton, clé matérielle, certificat SSH, clé de déploiement), ou un push rejeté par la protection des push | high |
security.exception.expired | Une mise à l’écart (acceptation de risque) avec une date d’expiration est arrivée à échéance et le résultat est de nouveau ouvert | aucune |
security.exception.requested | Quelqu’un a demandé une exception sur un résultat, ou en a écarté un alors que les mises à l’écart nécessitent une approbation (le titre le précise alors) | celle du résultat, s’il en a une |
security.exception.approved | Un approbateur a approuvé une exception sur un résultat ; le résultat est écarté | celle du résultat, s’il en a une |
security.exception.rejected | Un approbateur a rejeté une exception sur un résultat ; le résultat reste ouvert | celle du résultat, s’il en a une |
Deux autres noms d’événement parviennent à une destination abonnée sans abonnement explicite :
security.summary: envoyé après une heure pendant laquelle la limite horaire a retenu des événements.security.test: envoyé par le bouton de test ou le point de terminaison de test.
Les événements de sécurité s’activent par leur nom. Une destination dont la liste d’événements est vide reçoit tous les événements Forgejo, de statut et de maintenance, mais aucun événement de sécurité. Listez explicitement les événements de sécurité que vous voulez.
Un abonnement commence au moment où vous le créez. Les résultats qui existaient avant votre abonnement ne sont pas rejoués. Utilisez l’API des résultats pour l’arriéré actuel.
Un événement par résultat. Une nouvelle analyse ne renvoie pas un résultat. Sa correlation_key reste la même pour chaque événement qui le concerne, y compris l’événement security.exception.expired lorsqu’une acceptation de risque sur ce résultat arrive à échéance.
Gravité. security.finding.created est envoyé par défaut pour les résultats critical. Réglez security_min_severity sur high pour recevoir aussi les résultats élevés. Les erreurs SAST comptent comme high. Les secrets comptent comme critical, de même que les licences interdites. Les licences restreintes comptent comme high. Les autres événements sont toujours envoyés.
Corps
{
"event": "security.finding.created",
"event_id": "3f0c2a9e5b1d4c7a8e6f1b2c3d4e5f60",
"occurred_at": "2026-10-04T10:12:44.000Z",
"org": "acme",
"severity": "critical",
"title": "CVE-2026-12345 in lodash 4.17.20",
"repository": "acme/web",
"source": "dep",
"correlation_key": "dep:acme/web:CVE-2026-12345:npm:lodash@4.17.20",
"console_url": "https://frem.sh/acme/_admin/code-security/deps",
"details": {
"finding_id": "8b8d2c7e-0f6a-4a51-9a63-0c1f6f0b2a11",
"vulnerability_id": "CVE-2026-12345",
"ecosystem": "npm",
"package": "lodash",
"version": "4.17.20",
"fixed_version": "4.17.21",
"manifest_path": "package-lock.json",
"cvss_score": 9.8
}
}| Champ | Signification |
|---|---|
event | L’un des noms d’événement ci-dessus |
event_id | Stable pour chaque événement. Utilisez-le pour écarter une livraison déjà traitée |
occurred_at | Moment où le résultat, le refus ou la violation s’est produit (UTC) |
severity | critical, high ou null |
source | Ce qui l’a produit : dep, image, sast, secret, license, malware, dependency_malware, push_protection, auth_policy, registry, ou un nom d’analyseur pour une expiration |
correlation_key | L’identité du résultat. La même clé est utilisée pour chaque événement concernant le même résultat |
console_url | La page de la console qui l’affiche |
details | Champs propres à l’événement. Les valeurs de secrets ne sont jamais incluses : un résultat de secret ne porte que la règle, le fichier et la ligne |
Signature et en-têtes
Chaque requête comporte :
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: fremforge-webhook/1
X-Fremforge-Event: security.finding.created
X-Fremforge-Delivery: ffm-attempt-6d0f…
X-Fremforge-Timestamp: 1791108764
X-Fremforge-Signature: t=1791108764,v1=5d41402abc4b2a76b9719d911017c592…v1 est le HMAC-SHA256 en hexadécimal, avec pour clé le secret de signature de la destination, calculé sur l’horodatage, un point et le corps brut : <t>.<body>. Vérifiez-le sur les octets reçus, avant de les analyser. Rejetez un horodatage vieux de plus de quelques minutes. Après une rotation du secret, X-Fremforge-Signature-Previous porte pendant 7 jours la même signature produite avec l’ancien secret.
import hmac, hashlib, time
def verify(headers, raw_body: bytes, secret: str) -> None:
parts = dict(p.split('=', 1) for p in headers['X-Fremforge-Signature'].split(','))
ts, sig = parts['t'], parts['v1']
if abs(time.time() - int(ts)) > 300:
raise ValueError('stale')
expected = hmac.new(secret.encode(), ts.encode() + b'.' + raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
raise ValueError('bad signature')Une destination peut aussi porter un en-tête supplémentaire, pour les récepteurs qui s’authentifient par en-tête plutôt que par signature. ServiceNow en fait partie : Authorization: Basic …. La valeur est stockée chiffrée et n’est plus jamais affichée. Les en-têtes qui casseraient la requête (Host, Content-Type, X-Fremforge-*, les en-têtes de proxy et assimilés) sont refusés.
Livraison
- Nouvelles tentatives : une réponse autre que
2xx, un délai dépassé ou une erreur de connexion entraîne une nouvelle tentative après 2 minutes, puis 5 et 15 minutes, 1 heure et 6 heures. Cela fait six tentatives au total sur environ 7 h 30. Après la sixième, la livraison est marquéedlqet reste dans le journal. - Cible : une URL HTTPS publique. Les adresses privées, loopback, link-local et de métadonnées sont refusées. Les requêtes sortent par le proxy sortant de fremforge.
- Santé : après cinq livraisons échouées d’affilée, les contacts de facturation de l’organisation reçoivent un e-mail nommant la destination.
- Journal : chaque tentative, avec son statut, sa réponse HTTP et son erreur, figure dans
GET /orgs/{org}/webhooks/deliveries?destination_id={id}.
Limite horaire
Chaque destination accepte au plus security_hourly_cap événements de sécurité par heure d’horloge (UTC). La valeur par défaut est 100 et le maximum 1000. Les événements au-delà de la limite ne sont pas livrés ; ils apparaissent dans le journal des livraisons comme suppressed. Après l’heure, un événement security.summary indique combien ont été retenus, par nom d’événement :
{
"event": "security.summary",
"title": "240 security event(s) held back by the hourly cap (2026-10-04T10:00 UTC)",
"details": {
"hour_start": "2026-10-04T10:00:00.000Z",
"suppressed_total": 240,
"suppressed_by_event": { "security.finding.created": 240 }
}
}La limite protège votre système de tickets lors d’une première analyse d’un grand monorepo. Les résultats eux-mêmes figurent tous dans la console et dans l’API des résultats.
Configuration dans la console
Dans l’administration de votre organisation, ouvrez Webhooks (https://frem.sh/<org>/_admin/webhooks). Ajoutez une destination, ou choisissez le démarrage rapide Incidents ServiceNow. Puis, sous Événements de sécurité sur la destination :
- cochez les événements à envoyer ;
- choisissez Résultats à envoyer : Critiques uniquement ou Critiques et élevés ;
- réglez la Limite horaire ;
- choisissez le Format du corps : JSON générique (signé) ou Incident ServiceNow (Table API), avec les champs d’incident ServiceNow facultatifs ;
- définissez éventuellement un En-tête d’authentification.
Envoyer un test met en file un événement security.test et indique s’il a été livré et ce que le récepteur a répondu. Une destination qui reçoit déjà tous les événements de dépôt (liste d’événements vide) ne peut pas être restreinte aux événements de sécurité. Créez une destination distincte pour eux.
Configuration avec l’API
Portée webhooks:write. Créez une destination abonnée aux résultats critiques et élevés et aux malwares :
curl -sS -X POST "https://frem.sh/_app/api/v1/orgs/acme/webhooks/destinations" \
-H "Authorization: Bearer $FREMFORGE_TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "SOC intake",
"target_url": "https://soc.example.com/hooks/fremforge",
"events": ["security.finding.created", "security.malware.matched", "security.pull.blocked"],
"security_min_severity": "high",
"security_hourly_cap": 200
}'La réponse contient une seule fois le secret de signature. Conservez-le avec votre récepteur.
Modifiez ensuite n’importe quel paramètre avec PATCH /orgs/{org}/webhooks/destinations/{id} : events, payload_format, security_min_severity, security_hourly_cap, servicenow, auth_header, name, target_url ou active. "auth_header": null supprime l’en-tête.
Envoyer un événement de test :
curl -sS -X POST "https://frem.sh/_app/api/v1/orgs/acme/webhooks/destinations/$ID/test" \
-H "Authorization: Bearer $FREMFORGE_TOKEN"
# {"id":"…","attempt_id":"…","event":"security.test","status":"queued"}
curl -sS "https://frem.sh/_app/api/v1/orgs/acme/webhooks/deliveries?destination_id=$ID&limit=5" \
-H "Authorization: Bearer $FREMFORGE_TOKEN"Le test suit le même chemin qu’un événement réel : le format de la destination, la signature, l’en-tête d’authentification, le proxy et les nouvelles tentatives. Le point de terminaison répond 202. Une destination en pause ("active": false) répond 409 ; réactivez-la d’abord.
Voir aussi
- ServiceNow : des incidents à partir des événements de sécurité.
- Transfert vers un SIEM : tout le journal d’audit, pour des règles de détection plutôt que des tickets.
- Webhooks : les événements de dépôt de Forgejo.