Skip to content

Webhook Integration

Eric Fitzgerald edited this page Dec 15, 2025 · 2 revisions

Webhook Integration

This page explains how to set up and use TMI's webhook system to receive real-time notifications when threat models, diagrams, and other resources change.

Overview

TMI's webhook system allows you to subscribe to events and receive HTTP POST notifications when those events occur. This enables real-time integrations with external systems.

Common use cases:

  • Notify team channels (Slack, Teams) when threats are identified
  • Create tickets in issue trackers when threats are created
  • Trigger CI/CD pipelines when threat models are updated
  • Send alerts to SIEM systems for critical threats
  • Archive threat models to external storage
  • Synchronize threat data with other security tools

Architecture

The webhook system uses a worker-based architecture for reliable delivery:

Event Occurs → Redis Stream → Worker Processes → HTTP POST → Your Endpoint
                                                             ↓
                                                   Status Callback (optional)

Components:

  1. Event Emitter: Publishes events to Redis Streams when resources change
  2. Event Consumer: Reads events from stream and creates delivery records
  3. Challenge Worker: Verifies new subscriptions using challenge-response
  4. Delivery Worker: Delivers webhooks with retry and exponential backoff
  5. Cleanup Worker: Removes old delivery records and inactive subscriptions

See the operator webhook configuration guide for deployment details.

Quick Start

There is an example webhook application, written in Python and designed to be hosted in AWS Lambda, available here that you can clone/fork and use as a basis to implement your own webhook applications.

1. Create a Webhook Endpoint

Your endpoint must accept POST requests and respond with 200 OK:

Minimal example (Express.js):

const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/tmi', (req, res) => {
    const event = req.body;
    console.log('Received event:', event.event_type, event);

    // Process event asynchronously
    processEvent(event).catch(console.error);

    // Respond immediately
    res.status(200).send('OK');
});

app.listen(3000);

Important: Always respond quickly (within 30 seconds) to avoid timeouts.

2. Register Webhook Subscription

Create a webhook subscription via the API:

curl -X POST https://api.tmi.dev/webhook/subscriptions \
  -H "Authorization: Bearer $YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Integration",
    "url": "https://your-domain.com/webhooks/tmi",
    "events": ["threat_model.created", "threat_model.updated"],
    "secret": "your-shared-secret-for-hmac"
  }'

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "pending_verification",
  "name": "My Integration",
  "url": "https://your-domain.com/webhooks/tmi",
  "events": ["threat_model.created", "threat_model.updated"]
}

3. Respond to Challenge

TMI will send a challenge request to verify your endpoint:

Challenge request:

{
  "type": "webhook.challenge",
  "challenge": "abc123def456"
}

Required response:

{
  "challenge": "abc123def456"
}

Example handler:

app.post('/webhooks/tmi', (req, res) => {
    const event = req.body;

    // Handle challenge
    if (event.type === 'webhook.challenge') {
        return res.json({ challenge: event.challenge });
    }

    // Handle other events
    processEvent(event);
    res.status(200).send('OK');
});

After successful verification, your subscription becomes active and starts receiving events.

Event Types

Threat Model Events

threat_model.created:

{
  "event_type": "threat_model.created",
  "threat_model_id": "550e8400-e29b-41d4-a716-446655440000",
  "resource_id": "550e8400-e29b-41d4-a716-446655440000",
  "resource_type": "threat_model",
  "owner_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2025-01-15T14:30:00Z",
  "data": {
    "name": "Production API Threat Model",
    "description": "Threat model for production API services",
    "owner": "[email protected]"
  }
}

threat_model.updated:

{
  "event_type": "threat_model.updated",
  "threat_model_id": "550e8400-e29b-41d4-a716-446655440000",
  "resource_id": "550e8400-e29b-41d4-a716-446655440000",
  "resource_type": "threat_model",
  "owner_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2025-01-15T15:45:00Z",
  "data": {
    "name": "Production API Threat Model - Updated",
    "description": "Updated threat model",
    "modified_at": "2025-01-15T15:45:00Z"
  }
}

threat_model.deleted:

{
  "event_type": "threat_model.deleted",
  "threat_model_id": "550e8400-e29b-41d4-a716-446655440000",
  "resource_id": "550e8400-e29b-41d4-a716-446655440000",
  "resource_type": "threat_model",
  "owner_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2025-01-15T16:00:00Z",
  "data": {
    "name": "Production API Threat Model"
  }
}

Diagram Events

diagram.created, diagram.updated, diagram.deleted:

{
  "event_type": "diagram.created",
  "threat_model_id": "550e8400-e29b-41d4-a716-446655440000",
  "resource_id": "7d8f6e5c-4b3a-2190-8765-fedcba987654",
  "resource_type": "diagram",
  "owner_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2025-01-15T14:35:00Z",
  "data": {
    "title": "System Architecture",
    "diagram_type": "data_flow"
  }
}

Document Events

document.created, document.updated, document.deleted:

{
  "event_type": "document.created",
  "threat_model_id": "550e8400-e29b-41d4-a716-446655440000",
  "resource_id": "abc-123-def-456",
  "resource_type": "document",
  "owner_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2025-01-15T14:40:00Z",
  "data": {
    "name": "Security Requirements.pdf",
    "content_type": "application/pdf",
    "size": 524288
  }
}

Security

URL Validation

All webhook URLs must:

  • Use HTTPS (required in production)
  • Have valid DNS hostname
  • Pass deny list checks

Blocked patterns:

  • Localhost: 127.0.0.1, ::1, localhost
  • Private IPs: 10.*, 192.168.*, 172.16-31.*
  • Link-local: 169.254.*, fe80::
  • Cloud metadata endpoints (AWS, GCP, Azure, etc.)

Administrators can add custom deny list patterns via the deny list API.

HMAC Signature Verification

Always verify signatures to ensure webhooks are from TMI:

Signature header:

X-Webhook-Signature: sha256=5d41402abc4b2a76b9719d911017c592

Verification example (Node.js):

const crypto = require('crypto');

function verifyWebhook(payloadBody, signature, secret) {
    const hmac = crypto.createHmac('sha256', secret);
    hmac.update(payloadBody);
    const expectedSig = `sha256=${hmac.digest('hex')}`;

    // Constant-time comparison
    return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSig)
    );
}

app.post('/webhooks/tmi', (req, res) => {
    const signature = req.headers['x-webhook-signature'];
    const payloadBody = JSON.stringify(req.body);

    if (!verifyWebhook(payloadBody, signature, WEBHOOK_SECRET)) {
        return res.status(401).send('Invalid signature');
    }

    // Process webhook...
    res.status(200).send('OK');
});

Verification example (Python):

import hmac
import hashlib

def verify_webhook(payload_body, signature, secret):
    """Verify webhook HMAC signature"""
    expected = hmac.new(
        secret.encode('utf-8'),
        payload_body.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    expected_sig = f"sha256={expected}"

    # Constant-time comparison
    return hmac.compare_digest(signature, expected_sig)

@app.route('/webhooks/tmi', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature')
    payload_body = request.get_data(as_text=True)

    if not verify_webhook(payload_body, signature, WEBHOOK_SECRET):
        return 'Unauthorized', 401

    # Process webhook...
    return 'OK', 200

Webhook Headers

Every webhook request includes:

POST /webhooks/tmi HTTP/1.1
Host: your-domain.com
Content-Type: application/json
X-Webhook-Event: threat_model.created
X-Webhook-Delivery-Id: 7fa85f64-5717-4562-b3fc-2c963f66afa6
X-Webhook-Subscription-Id: 7d8f6e5c-4b3a-2190-8765-fedcba987654
X-Webhook-Signature: sha256=5d41402abc4b2a76b9719d911017c592
User-Agent: TMI-Webhook/1.0

Delivery Guarantees

Retry Logic

Failed deliveries are retried with exponential backoff:

  1. Attempt 1: Immediate
  2. Attempt 2: After 1 minute
  3. Attempt 3: After 5 minutes
  4. Attempt 4: After 15 minutes
  5. Attempt 5: After 30 minutes

After 5 attempts, delivery is marked failed.

Success Criteria

A delivery succeeds when:

  • HTTP status code is 2xx (200-299)
  • Response received within 30 seconds

Idempotency

Use X-Webhook-Delivery-Id header to detect duplicate deliveries:

const processedDeliveries = new Set();

app.post('/webhooks/tmi', (req, res) => {
    const deliveryId = req.headers['x-webhook-delivery-id'];

    // Check if already processed
    if (processedDeliveries.has(deliveryId)) {
        console.log('Duplicate delivery, skipping');
        return res.status(200).send('OK');
    }

    // Process webhook
    processEvent(req.body);

    // Mark as processed
    processedDeliveries.add(deliveryId);

    res.status(200).send('OK');
});

Rate Limits

Default Quotas (Per Owner)

  • Max Subscriptions: 10 active subscriptions
  • Subscription Creation: 5 requests/minute, 100 requests/day
  • Event Publication: 100 events/minute

Custom Quotas

Administrators can configure custom quotas:

curl -X POST https://api.tmi.dev/webhook/quotas \
  -H "Authorization: Bearer $ADMIN_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "owner_id": "user-uuid",
    "max_subscriptions": 50,
    "max_subscription_requests_per_minute": 10,
    "max_subscription_requests_per_day": 500,
    "max_events_per_minute": 1000
  }'

API Endpoints

Subscription Management

Create subscription:

POST /webhook/subscriptions

List subscriptions:

GET /webhook/subscriptions

Get subscription details:

GET /webhook/subscriptions/{id}

Delete subscription:

DELETE /webhook/subscriptions/{id}

Delivery History

List deliveries:

GET /webhook/deliveries

Get delivery details:

GET /webhook/deliveries/{id}

Admin Endpoints

Manage deny list:

GET /webhook/deny-list
POST /webhook/deny-list
DELETE /webhook/deny-list/{id}

Manage quotas:

GET /webhook/quotas/{owner_id}
POST /webhook/quotas
DELETE /webhook/quotas/{owner_id}

See REST-API-Reference for complete API documentation.

Best Practices

Endpoint Implementation

  1. Respond Quickly: Return 200 OK immediately, process asynchronously

    app.post('/webhooks/tmi', (req, res) => {
        queue.add('process-webhook', req.body);
        res.status(200).send('OK');
    });
  2. Implement Idempotency: Use delivery ID to prevent duplicate processing

  3. Verify Signatures: Always verify HMAC signatures

  4. Handle Errors Gracefully: Return 2xx for success, 4xx/5xx for errors

  5. Log Everything: Log all webhook receipts for debugging

Security

  1. HTTPS Only: Never expose HTTP endpoints (TMI will only accept https endpoints when not in development mode)

  2. Strong Secrets: Use strong, random secrets (min 32 characters)

    # Generate strong secret
    openssl rand -base64 32
  3. IP Allowlisting: Consider restricting to TMI server IPs

  4. Input Validation: Validate all incoming data

  5. Rate Limiting: Implement rate limiting on your endpoint

Monitoring

  1. Track Failures: Monitor subscription publication_failures

  2. Check Last Success: Monitor last_successful_use timestamp

  3. Query Deliveries: Review delivery status for debugging

    curl https://api.tmi.dev/webhook/deliveries \
      -H "Authorization: Bearer $TOKEN"
  4. Set Up Alerts: Alert on high failure rates

Performance

  1. Async Processing: Process webhooks asynchronously

  2. Queue System: Use message queue (RabbitMQ, Redis, SQS)

  3. Batch Operations: Batch database operations when possible

  4. Caching: Cache frequently accessed data

  5. Horizontal Scaling: Deploy multiple webhook handler instances

Integration Examples

Slack Notifications

Send threat notifications to Slack:

const { WebClient } = require('@slack/web-api');
const slack = new WebClient(process.env.SLACK_TOKEN);

async function notifySlack(event) {
    if (event.event_type === 'threat_model.created') {
        await slack.chat.postMessage({
            channel: '#security',
            text: `New threat model created: ${event.data.name}`,
            blocks: [
                {
                    type: 'section',
                    text: {
                        type: 'mrkdwn',
                        text: `*New Threat Model*\n${event.data.name}\n${event.data.description}`
                    }
                },
                {
                    type: 'actions',
                    elements: [
                        {
                            type: 'button',
                            text: { type: 'plain_text', text: 'View in TMI' },
                            url: `https://tmi.dev/threat-models/${event.threat_model_id}`
                        }
                    ]
                }
            ]
        });
    }
}

Email Notifications

Send email when critical threats are identified:

import smtplib
from email.mime.text import MIMEText

def send_email_notification(event):
    if event['event_type'] == 'threat.created':
        threat = event['data']

        if threat.get('severity') == 'critical':
            msg = MIMEText(f"""
            Critical threat identified:

            Title: {threat['title']}
            Description: {threat['description']}
            Threat Model: {threat['threat_model_name']}

            View in TMI: https://tmi.dev/threats/{event['resource_id']}
            """)

            msg['Subject'] = f"[CRITICAL] {threat['title']}"
            msg['From'] = '[email protected]'
            msg['To'] = '[email protected]'

            with smtplib.SMTP('smtp.example.com', 587) as smtp:
                smtp.starttls()
                smtp.login('username', 'password')
                smtp.send_message(msg)

Logging to SIEM

Forward events to SIEM system:

import requests

def forward_to_siem(event):
    """Forward webhook event to SIEM"""
    siem_endpoint = 'https://siem.example.com/api/events'

    siem_event = {
        'source': 'TMI',
        'event_type': event['event_type'],
        'timestamp': event['timestamp'],
        'severity': map_severity(event),
        'resource_id': event['resource_id'],
        'resource_type': event['resource_type'],
        'owner_id': event['owner_id'],
        'data': event['data']
    }

    response = requests.post(
        siem_endpoint,
        json=siem_event,
        headers={'Authorization': f'Bearer {SIEM_TOKEN}'}
    )
    response.raise_for_status()

Troubleshooting

Subscription Not Receiving Events

Checks:

  1. Subscription status is active (not pending_verification)
  2. Event types in subscription match events being triggered
  3. Threat model filter (threat_model_id) is correct
  4. Rate limit not exceeded
  5. Endpoint is responding with 2xx status

Debug:

# Check subscription status
curl https://api.tmi.dev/webhook/subscriptions/{id} \
  -H "Authorization: Bearer $TOKEN"

# Check recent deliveries
curl https://api.tmi.dev/webhook/deliveries \
  -H "Authorization: Bearer $TOKEN"

Verification Failed

Common issues:

  1. Endpoint not returning challenge in response body
  2. Response takes >30 seconds
  3. Non-2xx HTTP status code
  4. Network connectivity issues

Fix:

// Correct challenge response
app.post('/webhooks/tmi', (req, res) => {
    if (req.body.type === 'webhook.challenge') {
        return res.json({ challenge: req.body.challenge });
    }
    // ...
});

High Failure Rate

Diagnose:

# Check failure reasons
curl https://api.tmi.dev/webhook/deliveries \
  -H "Authorization: Bearer $TOKEN" | jq '.[] | select(.status=="failed")'

Common causes:

  1. Endpoint unavailable or slow
  2. Invalid HMAC signature verification
  3. Endpoint returning 4xx/5xx errors
  4. Network issues (firewall, DNS)

Solutions:

  1. Improve endpoint reliability
  2. Fix signature verification logic
  3. Return 2xx for successful processing
  4. Check network connectivity

Related Documentation

Clone this wiki locally