Aller au contenu principal

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énementEnvoyé lorsqueGravité
security.finding.createdUn nouveau résultat ouvert apparaît : vulnérabilité de dépendance ou de conteneur, SAST, secret, politique de licencescritical, ou high si vous le choisissez
security.malware.matchedUn 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 imagecritical
security.pull.blockedUn pull de conteneur ou de paquet a été refusé par le contrôle antimalware ou par votre politique block_pull d’analyse d’imageshigh
security.policy.violationUne 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 pushhigh
security.exception.expiredUne mise à l’écart (acceptation de risque) avec une date d’expiration est arrivée à échéance et le résultat est de nouveau ouvertaucune
security.exception.requestedQuelqu’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.approvedUn 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.rejectedUn approbateur a rejeté une exception sur un résultat ; le résultat reste ouvertcelle 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
  }
}
ChampSignification
eventL’un des noms d’événement ci-dessus
event_idStable pour chaque événement. Utilisez-le pour écarter une livraison déjà traitée
occurred_atMoment où le résultat, le refus ou la violation s’est produit (UTC)
severitycritical, high ou null
sourceCe 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_keyL’identité du résultat. La même clé est utilisée pour chaque événement concernant le même résultat
console_urlLa page de la console qui l’affiche
detailsChamps 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ée dlq et 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.