Files
securelens-backend/app/services/webhook_dispatcher.py
2026-05-07 10:35:36 +05:30

65 lines
2.0 KiB
Python

"""
Webhook Dispatcher
==================
Shared utility for firing HMAC-signed webhook POST requests.
Previously the dispatch logic lived inline inside scan.py. Moving it here
means both the scan router and the background scheduler can call the same
function without creating a circular import.
"""
import hashlib
import hmac
import json
import logging
import httpx
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.webhook import Webhook
logger = logging.getLogger(__name__)
async def dispatch_webhooks(user_id: str, scan_data: dict, db: AsyncSession) -> None:
"""
Fetch all active webhooks for a user and POST the scan_data payload to each.
The payload is JSON-encoded and signed with HMAC-SHA256 if the webhook has
a secret key set. The signature is sent in the X-SecureLens-Signature header
so the receiving server can verify the request is genuine.
Failures are logged but never re-raised — a broken webhook should never
crash or block the scan response.
"""
result = await db.execute(
select(Webhook).where(
Webhook.user_id == user_id,
Webhook.is_active == True, # noqa: E712
)
)
hooks = result.scalars().all()
if not hooks:
return
payload = json.dumps(scan_data).encode("utf-8")
async with httpx.AsyncClient() as client:
for hook in hooks:
headers = {"Content-Type": "application/json"}
if hook.secret_key:
sig = hmac.new(
hook.secret_key.encode(), payload, hashlib.sha256
).hexdigest()
headers["X-SecureLens-Signature"] = sig
try:
await client.post(
hook.target_url, content=payload, headers=headers, timeout=5.0
)
logger.debug(f"Webhook fired: {hook.target_url}")
except Exception as e:
logger.warning(f"Webhook {hook.target_url} failed: {e}")