mirror of
https://github.com/Rarebuffalo/securelens-backend.git
synced 2026-06-19 07:00:30 +00:00
65 lines
2.0 KiB
Python
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}")
|