Webhooks für Sicherheitsalarme
Sicherheitsalarme erreichen Menschen per E-Mail. Damit sie auch dein Incident- oder Ticketsystem erreichen, abonnierst du mit einem ausgehenden Webhook-Ziel ein oder mehrere Sicherheitsereignisse. Jedes Ereignis ist ein signierter HTTPS-POST, der wiederholt wird, bis dein Empfänger ihn annimmt, und im Zustellprotokoll festgehalten wird.
Pro Ziel stehen zwei Body-Formate zur Verfügung:
- Generisch (Standard): das unten beschriebene JSON-Ereignis von fremforge.
- ServiceNow: ein Incident-Datensatz, den die Table API von ServiceNow unverändert annimmt. Siehe ServiceNow.
Ereignisse
| Ereignis | Gesendet, wenn | Schweregrad |
|---|---|---|
security.finding.created | Ein neuer offener Befund erscheint: Schwachstelle in einer Abhängigkeit oder einem Container, SAST, Secret, Lizenzrichtlinie | critical, oder high, wenn du das wählst |
security.malware.matched | Malware in einem Upload (ClamAV) oder ein bekanntermaßen bösartiges Paket (OSV MAL-*) in einer SBOM des Standard-Branches oder eines Images | critical |
security.pull.blocked | Ein Pull eines Containers oder Pakets wurde vom Malware-Gate oder von deiner Image-Scan-Richtlinie block_pull abgewiesen | high |
security.policy.violation | Ein Verstoß gegen eine Authentifizierungsrichtlinie (Token-Lebensdauer, Hardware-Schlüssel, SSH-Zertifikat, Deploy Key) oder ein vom Push-Schutz abgewiesener Push | high |
security.exception.expired | Ein Verwerfen (Risikoakzeptanz) mit Ablaufdatum ist abgelaufen, und der Befund ist wieder offen | keiner |
security.exception.requested | Jemand hat eine Ausnahme für einen Befund beantragt oder einen Befund verworfen, während Verwerfen eine Freigabe erfordert (der Titel sagt das dann) | der des Befunds, sofern er einen hat |
security.exception.approved | Eine freigebende Person hat eine Ausnahme für einen Befund freigegeben; der Befund ist verworfen | der des Befunds, sofern er einen hat |
security.exception.rejected | Eine freigebende Person hat eine Ausnahme für einen Befund abgelehnt; der Befund bleibt offen | der des Befunds, sofern er einen hat |
Zwei weitere Ereignisnamen erreichen ein Ziel mit Abonnement, ohne dass sie abonniert sein müssen:
security.summary: gesendet nach einer Stunde, in der das Stundenlimit Ereignisse zurückgehalten hat.security.test: gesendet von der Test-Schaltfläche oder dem Test-Endpunkt.
Sicherheitsereignisse werden namentlich abonniert. Ein Ziel mit leerer Ereignisliste erhält jedes Forgejo-, Status- und Wartungsereignis, aber keine Sicherheitsereignisse. Führe die gewünschten Sicherheitsereignisse ausdrücklich auf.
Ein Abonnement beginnt, wenn du es anlegst. Befunde, die vor dem Abonnement bestanden, werden nicht nachgeliefert. Für den aktuellen Bestand nutze die Findings-API.
Ein Ereignis pro Befund. Ein erneuter Scan sendet einen Befund nicht noch einmal. Sein correlation_key bleibt für jedes Ereignis zu ihm gleich, auch für das Ereignis security.exception.expired, wenn eine Risikoakzeptanz für ihn abläuft.
Schweregrad. security.finding.created wird standardmäßig für Befunde mit critical gesendet. Setze security_min_severity auf high, um auch hohe Befunde zu erhalten. SAST-Fehler zählen als high. Secrets zählen als critical, ebenso verbotene Lizenzen. Eingeschränkte Lizenzen zählen als high. Die übrigen Ereignisse werden immer gesendet.
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
}
}| Feld | Bedeutung |
|---|---|
event | Einer der oben genannten Ereignisnamen |
event_id | Stabil pro Ereignis. Damit verwirfst du eine Zustellung, die du schon verarbeitet hast |
occurred_at | Wann der Befund, die Abweisung oder der Verstoß auftrat (UTC) |
severity | critical, high oder null |
source | Was es erzeugt hat: dep, image, sast, secret, license, malware, dependency_malware, push_protection, auth_policy, registry oder bei einem Ablauf ein Scanner-Name |
correlation_key | Die Identität des Befunds. Für jedes Ereignis zum selben Befund wird derselbe Schlüssel verwendet |
console_url | Die Konsolenseite, die ihn zeigt |
details | Ereignisspezifische Felder. Secret-Werte sind nie enthalten: Ein Secret-Befund trägt nur Regel, Datei und Zeile |
Signatur und Header
Jede Anfrage trägt:
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 ist der hexkodierte HMAC-SHA256, geschlüsselt mit dem Signing-Secret des Ziels, über den Zeitstempel, einen Punkt und den rohen Body: <t>.<body>. Prüfe ihn über die empfangenen Bytes, bevor du sie parst. Weise einen Zeitstempel zurück, der älter als ein paar Minuten ist. Nach einer Rotation des Secrets trägt X-Fremforge-Signature-Previous 7 Tage lang dieselbe Signatur, erstellt mit dem alten 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')Ein Ziel kann zusätzlich einen weiteren Header tragen, für Empfänger, die sich per Header statt per Signatur authentifizieren. ServiceNow ist so einer: Authorization: Basic …. Der Wert wird verschlüsselt gespeichert und nie wieder angezeigt. Header, die die Anfrage zerstören würden (Host, Content-Type, X-Fremforge-*, Proxy-Header und ähnliche), werden abgewiesen.
Zustellung
- Wiederholungen: Eine Antwort, die nicht
2xxist, ein Timeout oder ein Verbindungsfehler wird nach 2 Minuten wiederholt, dann nach 5 und 15 Minuten, 1 Stunde und 6 Stunden. Das sind insgesamt sechs Versuche über etwa 7,5 Stunden. Nach dem sechsten wird die Zustellung alsdlqmarkiert und bleibt im Protokoll. - Ziel: eine öffentliche HTTPS-URL. Private, Loopback-, Link-Local- und Metadaten-Adressen werden abgewiesen. Anfragen verlassen fremforge über dessen ausgehenden Proxy.
- Zustand: Nach fünf fehlgeschlagenen Zustellungen in Folge erhalten die Abrechnungskontakte der Organisation eine E-Mail, die das Ziel nennt.
- Protokoll: Jeder Versuch mit Status, HTTP-Antwort und Fehler steht in
GET /orgs/{org}/webhooks/deliveries?destination_id={id}.
Stundenlimit
Jedes Ziel nimmt höchstens security_hourly_cap Sicherheitsereignisse pro voller Stunde (UTC) an. Der Standard ist 100, das Maximum 1000. Ereignisse über dem Limit werden nicht zugestellt; sie erscheinen im Zustellprotokoll als suppressed. Nach Ablauf der Stunde meldet ein einzelnes Ereignis security.summary, wie viele pro Ereignisname zurückgehalten wurden:
{
"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 }
}
}Das Limit schützt dein Ticketsystem vor dem ersten Scan eines großen Monorepos. Die Befunde selbst stehen alle in der Konsole und in der Findings-API.
In der Konsole einrichten
Öffne im Admin-Bereich deiner Organisation Webhooks (https://frem.sh/<org>/_admin/webhooks). Füge ein Ziel hinzu oder wähle den Schnellstart ServiceNow-Incidents. Dann unter Sicherheitsereignisse am Ziel:
- hake die zu sendenden Ereignisse an;
- wähle unter Zu sendende Befunde: Nur kritische oder Kritische und hohe;
- setze das Stundenlimit;
- wähle das Format: Generisches JSON (signiert) oder ServiceNow-Incident (Table API), mit den optionalen ServiceNow-Incident-Feldern;
- setze optional einen Auth-Header.
Test senden stellt ein Ereignis security.test in die Warteschlange und zeigt, ob es zugestellt wurde und was der Empfänger geantwortet hat. Ein Ziel, das bereits alle Repository-Ereignisse erhält (leere Ereignisliste), lässt sich nicht auf Sicherheitsereignisse einschränken. Lege dafür ein eigenes Ziel an.
Mit der API einrichten
Scope webhooks:write. Lege ein Ziel an, das kritische und hohe Befunde sowie Malware abonniert:
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
}'Die Antwort enthält das Signing-secret genau einmal. Hinterlege es bei deinem Empfänger.
Jede Einstellung änderst du später mit PATCH /orgs/{org}/webhooks/destinations/{id}: events, payload_format, security_min_severity, security_hourly_cap, servicenow, auth_header, name, target_url oder active. "auth_header": null entfernt den Header.
Ein Testereignis senden:
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"Der Test nimmt denselben Weg wie ein echtes Ereignis: Format, Signatur, Auth-Header, Proxy und Wiederholungen des Ziels. Der Endpunkt antwortet mit 202. Ein pausiertes Ziel ("active": false) antwortet mit 409, also setze es zuerst fort.
Verwandte Themen
- ServiceNow: Incidents aus Sicherheitsereignissen.
- SIEM-Weiterleitung: das gesamte Audit-Log, für Erkennungsregeln statt Tickets.
- Webhooks: Repository-Ereignisse aus Forgejo.