Webhook'lar
Kendi tarafınızda bir HTTPS uç noktası açın; ilgilendiğiniz olaylar gerçekleştiğinde SecureCodingHub buraya imzalı bir JSON zarfı POST eder. Her teslimat HMAC-SHA256 ile imzalanır; böylece sahte bir kaynaktan değil bizden geldiğini doğrulayabilirsiniz.
Webhook'ları ne zaman kullanmalısınız
- Bir geliştirici zorunlu bir atamayı tamamladığında (veya başarısız olduğunda) Jira ticket'ı açın.
- Bir kategori tamamlama sertifikası verildiğinde İK kaydını güncelleyin.
- CI'dan gelen bir SARIF çalıştırması eğitim atamalarını otomatik oluşturduğunda Slack'i bilgilendirin.
- REST API'yi yoklamadan SCH durumunu kendi veri ambarınıza aynalayın.
Uç nokta oluşturma
Kurum admin'i olarak oturum açın ve Organization → Webhooks sayfasını açın.
+ Add endpoint düğmesine tıklayın. Teslimatları alacak public HTTPS URL'sini girin. Lokal geliştirme için ngrok / cloudflared tünelleri uygundur.
Abone olmak istediğiniz olayları işaretleyin (bunu daha sonra değiştirebilirsiniz).
Create endpoint düğmesine tıklayın. Bir whsec_… imzalama secret'ı yalnızca bir kez gösterilir. Secrets manager'ınıza kopyalayın — bu her teslimatı doğrulamak için kullanacağınız anahtardır.
Olay kataloğu
v1, dört olay türü yayınlar. Katalog ayrıca GET /api/public/v1/webhook-endpoints/events adresinde programatik olarak da mevcuttur.
| Olay | Tetiklenme zamanı | Payload data yapısı |
|---|---|---|
assignment.created | Bir atama oluşturulduğunda — admin tarafından manuel, public API üzerinden veya SARIF içeri alımının yan etkisi olarak. | POST /assignments yanıtı ile aynı yapı. |
assignment.completed | Bir kullanıcı kendisine verilen bir atamada %100 tamamlanmaya ulaştığında. | Atama özetine ek olarak userId, userName, userEmail, completedAt. |
sarif.ingested | Bir SARIF çalıştırması işlendiğinde (üzerinden herhangi bir atama oluşturulup oluşturulmadığından bağımsız). | Senkron POST /sarif yanıtı ile aynı yapı. |
certificate.issued | Bir kullanıcıya kategori tamamlama sertifikası otomatik verildiğinde. | certificateId, certificateNumber, kullanıcı bilgileri, categoryId, categoryTitle, issuedAt. |
İstek yapısı
Her teslimat şu header'lar ve bir JSON gövde ile bir POST isteğidir:
POST /your-endpoint HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: SecureCodingHub-Webhooks/1.0
X-SecureCodingHub-Event: assignment.completed
X-SecureCodingHub-Event-Id: evt_8a3d…
X-SecureCodingHub-Delivery: 9c1b…
X-SecureCodingHub-Signature: t=1717003245,v1=ad8c…
{
"id": "evt_8a3d…",
"type": "assignment.completed",
"createdAt": "2026-05-29T13:45:12.412Z",
"orgId": "e188fc87-…",
"data": { … }
}Zarf tüm olay türlerinde aynıdır. Yalnızca type ve içerideki data değişir.
İmzayı doğrulama
X-SecureCodingHub-Signature header'ı t=<unix_seconds>,v1=<hex> formatına sahiptir. Doğrulamak için:
Header'dan t ve v1'i ayrıştırın.
HMAC-SHA256("<t>.<raw_body>", secret) değerini küçük harfli hex dizgisi olarak hesaplayın.
Sabit zamanda v1 ile karşılaştırın.
Zaman damgası sunucu saatinizden 5 dakikadan fazla saparsa reddedin (replay saldırılarına karşı korur).
Her zaman ham istek gövdesi üzerinde doğrulayın — JSON'u yeniden serileştirmek byte sırasını değiştirir ve imzayı geçersiz kılar.
Node.js örneği
import crypto from 'crypto'
import express from 'express'
const app = express()
const SECRET = process.env.SCH_WEBHOOK_SECRET
app.post('/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const header = req.header('X-SecureCodingHub-Signature') || ''
const parts = Object.fromEntries(
header.split(',').map(p => p.trim().split('='))
)
const ts = parseInt(parts.t, 10)
const sig = parts.v1
if (!ts || !sig) return res.status(400).send('bad signature header')
if (Math.abs(Date.now() / 1000 - ts) > 300) {
return res.status(400).send('stale')
}
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${ts}.${req.body.toString('utf8')}`)
.digest('hex')
const ok = crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(sig, 'hex')
)
if (!ok) return res.status(401).send('bad signature')
const event = JSON.parse(req.body.toString('utf8'))
console.log(`✓ ${event.type} ${event.id}`)
res.json({ received: true })
})
app.listen(7777)Python (Flask) örneği
import hashlib, hmac, os, time
from flask import Flask, request, abort
SECRET = os.environ['SCH_WEBHOOK_SECRET'].encode()
app = Flask(__name__)
@app.post('/webhook')
def webhook():
header = request.headers.get('X-SecureCodingHub-Signature', '')
parts = dict(p.split('=') for p in header.split(','))
try:
ts = int(parts['t']); sig = parts['v1']
except (KeyError, ValueError):
abort(400, 'bad signature header')
if abs(time.time() - ts) > 300:
abort(400, 'stale')
raw = request.get_data() # raw bytes
expected = hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401, 'bad signature')
event = request.get_json()
print('OK', event['type'], event['id'])
return {'received': True}Go örneği
package main
import (
"crypto/hmac"; "crypto/sha256"; "encoding/hex"; "io"
"net/http"; "os"; "strconv"; "strings"; "time"
)
var secret = []byte(os.Getenv("SCH_WEBHOOK_SECRET"))
func webhook(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get("X-SecureCodingHub-Signature")
var ts int64; var sig string
for _, p := range strings.Split(header, ",") {
kv := strings.SplitN(strings.TrimSpace(p), "=", 2)
if len(kv) != 2 { continue }
if kv[0] == "t" { ts, _ = strconv.ParseInt(kv[1], 10, 64) }
if kv[0] == "v1" { sig = kv[1] }
}
if ts == 0 || sig == "" { http.Error(w, "bad header", 400); return }
if abs(time.Now().Unix() - ts) > 300 { http.Error(w, "stale", 400); return }
body, _ := io.ReadAll(r.Body)
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(strconv.FormatInt(ts, 10) + "."))
mac.Write(body)
if !hmac.Equal(mac.Sum(nil), mustHex(sig)) {
http.Error(w, "bad signature", 401); return
}
w.Write([]byte(`{"received":true}`))
}
func mustHex(s string) []byte { b, _ := hex.DecodeString(s); return b }
func abs(x int64) int64 { if x < 0 { return -x }; return x }
func main() {
http.HandleFunc("/webhook", webhook)
http.ListenAndServe(":7777", nil)
}Teslimat garantileri ve yeniden denemeler
Webhook teslimatları sıraya alınır ve bir arka plan worker'ı tarafından eşzamansız olarak gönderilir. en az bir kez teslim edilirler — nadir durumlarda (zaman aşımı, yanıt yarışı) aynı olay iki kez teslim edilebilir. Tekrarları ayıklamak için zarftaki id alanını (ve X-SecureCodingHub-Event-Id header'ını) kullanın.
Uç noktanız 15 saniye içinde herhangi bir 2xx durumu ile yanıt vermelidir. Bunun dışındaki her şey başarısızlık sayılır ve üstel geri çekilme ile yeniden denemeyi tetikler:
| Deneme | Bir öncekinden sonraki süre |
|---|---|
| 1 (ilk) | kaynak olaydan hemen sonra sıraya alınır |
| 2 | +1 dakika |
| 3 | +5 dakika |
| 4 | +30 dakika |
| 5 (son) | +2 saat |
5. başarısızlıktan sonra teslimat exhausted olarak işaretlenir ve uç nokta otomatik olarak devre dışı bırakılır. Başarısızlıkları ve otomatik devre dışı bırakma zaman damgasını Organization → Webhooks → View deliveries altında görebilirsiniz.
En iyi uygulamalar
- Her zaman imzayı doğrulayın. Aksi halde URL'nizi tahmin eden herhangi biri pipeline'ınıza sahte olaylar yağdırabilir.
- Hızlı yanıt verin. İşi bir kuyruğa devredin ve milisaniyeler içinde
200dönün — handler içinde senkron işleme yapmayın. event.idile tekrar ayıklayın. Görülmüş ID'leri en az 7 gün saklayın.- IP tabanlı filtrelemeye güvenmeyin. Cloudflare çıkış IP'leri önceden haber verilmeksizin değişebilir; imza tek kararlı güven çapasıdır.
- Canlıya almadan önce test edin. Yerel bir alıcıyı dışarı açmak için cloudflared (
cloudflared tunnel --url http://localhost:7777) veya ngrok kullanın, ardından uçtan uca bir teslimatı yakalamak için API üzerinden bir olay tetikleyin.
Son teslimatları inceleme
Admin konsolu, uç nokta başına en son 50 teslimatı durum, deneme sayısı, yanıt kodu ve varsa hata mesajı ile listeler. Programatik erişim şu adreste mevcuttur:
GET /api/public/v1/webhook-endpoints/{id}/deliveriesBu işlem webhook:manage kapsamını gerektirir.