Webhooks voor beveiligingsmeldingen
Beveiligingsmeldingen bereiken mensen per e-mail. Wil je ook je incident- of ticketsysteem bereiken, abonneer dan een uitgaande webhookbestemming op een of meer beveiligingsgebeurtenissen. Elke gebeurtenis is één ondertekende HTTPS-POST, die opnieuw wordt geprobeerd tot je ontvanger hem accepteert, en die in de afleverlog wordt vastgelegd.
Per bestemming zijn er twee formaten voor de body:
- Generiek (standaard): de JSON-gebeurtenis van fremforge die hieronder wordt beschreven.
- ServiceNow: een incidentrecord dat de Table API van ServiceNow zonder aanpassingen accepteert. Zie ServiceNow.
Gebeurtenissen
| Gebeurtenis | Verstuurd wanneer | Ernst |
|---|---|---|
security.finding.created | Er verschijnt een nieuwe openstaande bevinding: kwetsbaarheid in een afhankelijkheid of container, SAST, secret, licentiebeleid | critical, of high als je dat kiest |
security.malware.matched | Malware in een upload (ClamAV), of een bekend kwaadaardig pakket (OSV MAL-*) in een SBOM van de standaardbranch of van een image | critical |
security.pull.blocked | Een pull van een container of pakket werd geweigerd door de malwarepoort of door je imagescanbeleid block_pull | high |
security.policy.violation | Een schending van het authenticatiebeleid (levensduur van tokens, hardwaresleutel, SSH-certificaat, deploy key), of een push die door push-bescherming werd geweigerd | high |
security.exception.expired | Het negeren van een bevinding (risicoacceptatie) met een vervaldatum is verlopen en de bevinding staat weer open | geen |
security.exception.requested | Iemand vroeg een uitzondering aan op een bevinding, of negeerde er een terwijl negeren goedkeuring vereist (de titel zegt dat dan) | die van de bevinding, als ze er een heeft |
security.exception.approved | Een goedkeurder keurde een uitzondering op een bevinding goed; de bevinding wordt genegeerd | die van de bevinding, als ze er een heeft |
security.exception.rejected | Een goedkeurder wees een uitzondering op een bevinding af; de bevinding blijft openstaan | die van de bevinding, als ze er een heeft |
Nog twee gebeurtenisnamen bereiken een geabonneerde bestemming zonder dat je je erop abonneert:
security.summary: verstuurd na een uur waarin de limiet per uur gebeurtenissen tegenhield.security.test: verstuurd door de testknop of het testendpoint.
Je abonneert je op beveiligingsgebeurtenissen bij naam. Een bestemming met een lege lijst gebeurtenissen ontvangt elke gebeurtenis van Forgejo, van de status en van onderhoud, maar geen beveiligingsgebeurtenissen. Noem de beveiligingsgebeurtenissen die je wilt expliciet.
Een abonnement begint zodra je het aanmaakt. Bevindingen die al bestonden voordat je je abonneerde, worden niet opnieuw verstuurd. Gebruik de findings-API voor de huidige achterstand.
Eén gebeurtenis per bevinding. Een nieuwe scan verstuurt een bevinding niet nog eens. Haar correlation_key blijft gelijk voor elke gebeurtenis over haar, ook voor de gebeurtenis security.exception.expired als een risicoacceptatie op haar verloopt.
Ernst. security.finding.created wordt standaard verstuurd voor bevindingen met ernst critical. Zet security_min_severity op high om ook bevindingen met ernst high te ontvangen. SAST-fouten tellen als high. Secrets tellen als critical, en verboden licenties ook. Beperkte licenties tellen als high. De andere gebeurtenissen worden altijd verstuurd.
Body
{
"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
}
}| Veld | Betekenis |
|---|---|
event | Een van de gebeurtenisnamen hierboven |
event_id | Stabiel per gebeurtenis. Gebruik het om een aflevering die je al hebt verwerkt, te negeren |
occurred_at | Wanneer de bevinding, weigering of schending plaatsvond (UTC) |
severity | critical, high of null |
source | Wat de gebeurtenis voortbracht: dep, image, sast, secret, license, malware, dependency_malware, push_protection, auth_policy, registry, of de naam van een scanner bij een verlopen uitzondering |
correlation_key | De identiteit van de bevinding. Dezelfde sleutel wordt gebruikt voor elke gebeurtenis over dezelfde bevinding |
console_url | De pagina in de console die haar toont |
details | Velden die bij de gebeurtenis horen. Waarden van secrets worden nooit meegestuurd: een secretbevinding bevat alleen de regel, het bestand en het regelnummer |
Handtekening en headers
Elk verzoek bevat:
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 is de hexadecimale HMAC-SHA256, met het ondertekeningssecret van de bestemming als sleutel, over het tijdstempel, een punt en de onbewerkte body: <t>.<body>. Verifieer hem over de bytes die je hebt ontvangen, voordat je ze parset. Weiger een tijdstempel dat meer dan een paar minuten oud is. Na een rotatie van het secret bevat X-Fremforge-Signature-Previous 7 dagen lang dezelfde handtekening, gemaakt met het oude 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')Een bestemming kan ook één extra header meesturen, voor ontvangers die op een header authenticeren in plaats van op een handtekening. ServiceNow is er een van: Authorization: Basic …. De waarde wordt versleuteld opgeslagen en nooit meer getoond. Headers die het verzoek zouden breken (Host, Content-Type, X-Fremforge-*, proxyheaders en dergelijke) worden geweigerd.
Aflevering
- Nieuwe pogingen: een antwoord dat geen
2xxis, een time-out of een verbindingsfout wordt na 2 minuten opnieuw geprobeerd, daarna na 5 en 15 minuten, 1 uur en 6 uur. Dat zijn in totaal zes pogingen over ongeveer 7,5 uur. Na de zesde wordt de aflevering gemarkeerd alsdlqen blijft ze in de log staan. - Doel: een openbare HTTPS-URL. Private, loopback-, link-local- en metadata-adressen worden geweigerd. Verzoeken gaan via de uitgaande proxy van fremforge naar buiten.
- Gezondheid: na vijf mislukte afleveringen op rij krijgen de factuurcontactpersonen van de organisatie een e-mail die de bestemming noemt.
- Log: elke poging, met haar status, HTTP-antwoord en fout, staat in
GET /orgs/{org}/webhooks/deliveries?destination_id={id}.
Limiet per uur
Elke bestemming accepteert hooguit security_hourly_cap beveiligingsgebeurtenissen per klokuur (UTC). De standaard is 100 en het maximum 1000. Gebeurtenissen boven de limiet worden niet afgeleverd; ze verschijnen in de afleverlog als suppressed. Na het uur zegt één gebeurtenis security.summary hoeveel er per gebeurtenisnaam zijn tegengehouden:
{
"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 }
}
}De limiet beschermt je ticketsysteem tegen de eerste scan van een grote monorepo. De bevindingen zelf staan allemaal in de console en in de findings-API.
Instellen in de console
Open in het beheer van je organisatie Webhooks (https://frem.sh/<org>/_admin/webhooks). Voeg een bestemming toe, of kies de snelstart ServiceNow-incidenten. Daarna, onder Beveiligingsgebeurtenissen bij de bestemming:
- vink de gebeurtenissen aan die je wilt versturen;
- kies Te versturen bevindingen: Alleen kritieke of Kritieke en hoge;
- stel de Limiet per uur in;
- kies het Formaat: Generieke JSON (ondertekend) of ServiceNow-incident (Table API), met de optionele velden voor het ServiceNow-incident;
- stel eventueel een Auth-header (optioneel) in.
Test verzenden zet een gebeurtenis security.test in de wachtrij en laat zien of die is afgeleverd en wat de ontvanger antwoordde. Een bestemming die al alle repositorygebeurtenissen ontvangt (een lege lijst gebeurtenissen), kan niet worden beperkt tot beveiligingsgebeurtenissen. Maak daarvoor een aparte bestemming aan.
Instellen met de API
Scope webhooks:write. Maak een bestemming aan die is geabonneerd op bevindingen met ernst critical en high en op malware:
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
}'Het antwoord bevat het ondertekeningssecret secret één keer. Bewaar het bij je ontvanger.
Wijzig later een instelling met PATCH /orgs/{org}/webhooks/destinations/{id}: events, payload_format, security_min_severity, security_hourly_cap, servicenow, auth_header, name, target_url of active. "auth_header": null verwijdert de header.
Een testgebeurtenis versturen:
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"De test volgt hetzelfde pad als een echte gebeurtenis: het formaat, de handtekening, de auth-header, de proxy en de nieuwe pogingen van de bestemming. Het endpoint antwoordt met 202. Een gepauzeerde bestemming ("active": false) antwoordt met 409, dus hervat haar eerst.
Gerelateerd
- ServiceNow: incidenten uit beveiligingsgebeurtenissen.
- Doorsturen naar SIEM: de hele auditlog, voor detectieregels in plaats van tickets.
- Webhooks: repositorygebeurtenissen uit Forgejo.