Skip to content

Addon System

Eric Fitzgerald edited this page Jan 23, 2026 · 3 revisions

Addon System

This page explains TMI's addon system, which allows administrators to register webhook-based extensions that users can invoke on-demand to enhance threat modeling workflows.

Overview

What are addons?

Addons are webhook-based integrations that extend TMI's functionality. Unlike standard webhooks that react to events, addons are user-invoked and can process threat model data on-demand.

Example addons:

  • STRIDE analysis automation
  • Compliance framework checking
  • Threat intelligence enrichment
  • Diagram validation
  • Custom report generation
  • AI-powered threat suggestions

Key characteristics:

  • Administrator-registered: Only admins can create/delete addons
  • User-invoked: Any authenticated user can trigger addons
  • Asynchronous: Long-running operations with status updates
  • Webhook-based: External services receive invocation requests
  • Rate-limited: Prevents abuse with per-user quotas

Architecture

User Invokes Addon
       ↓
   TMI Server
       ↓
 Rate Limit Check (Redis)
       ↓
Create Invocation (Redis)
       ↓
 Addon Worker → HTTP POST → External Service
                                    ↓
                          Process Asynchronously
                                    ↓
                          POST /invocations/{id}/status
                                    ↓
                          Update Status (Redis)
                                    ↓
                          User Polls Status

Components:

  1. Addons Table (PostgreSQL): Addon registrations
  2. Invocation Store (Redis): Ephemeral invocation state (7-day TTL)
  3. Rate Limiter (Redis): Per-user quotas
  4. Addon Worker: Delivers invocation requests to webhooks
  5. Status Update Handler: Receives status updates from external services

For Users

Discovering Addons

List available addons:

GET /addons

# Filter by threat model
GET /addons?threat_model_id={threat_model_id}

Response:

{
  "addons": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "STRIDE Analysis",
      "description": "Automated STRIDE threat analysis",
      "icon": "material-symbols:security",
      "objects": ["threat_model", "asset"],
      "threat_model_id": null,
      "created_at": "2025-01-15T10:00:00Z"
    },
    {
      "id": "7d8f6e5c-4b3a-2190-8765-fedcba987654",
      "name": "Compliance Checker",
      "description": "Check against NIST/ISO frameworks",
      "icon": "material-symbols:shield_lock",
      "objects": ["threat_model"],
      "created_at": "2025-01-15T11:00:00Z"
    }
  ],
  "total": 2
}

Invoking an Addon

Trigger an addon to process your threat model:

POST /addons/{addon_id}/invoke

{
  "threat_model_id": "threat-model-uuid",
  "object_type": "asset",          # Optional
  "object_id": "asset-uuid",       # Optional
  "payload": {                     # Custom data, max 1KB
    "analysis_type": "full",
    "include_recommendations": true
  }
}

Response (202 Accepted):

{
  "invocation_id": "abc-123-def-456",
  "status": "pending",
  "created_at": "2025-01-15T12:00:00Z"
}

Checking Invocation Status

Poll for status updates:

GET /invocations/{invocation_id}

Response:

{
  "id": "abc-123-def-456",
  "addon_id": "550e8400-e29b-41d4-a716-446655440000",
  "threat_model_id": "threat-model-uuid",
  "status": "in_progress",
  "status_percent": 75,
  "status_message": "Analyzing assets...",
  "created_at": "2025-01-15T12:00:00Z",
  "status_updated_at": "2025-01-15T12:02:30Z"
}

Status values:

  • pending: Queued for processing
  • in_progress: Currently processing
  • completed: Successfully finished
  • failed: Processing failed

Listing Your Invocations

View your invocation history:

GET /invocations

# Filter by status
GET /invocations?status=completed

# Filter by addon
GET /invocations?addon_id={addon_id}

Response:

{
  "invocations": [
    {
      "id": "abc-123",
      "addon_id": "550e8400-...",
      "status": "completed",
      "status_percent": 100,
      "status_message": "Analysis complete",
      "created_at": "2025-01-15T12:00:00Z"
    }
  ],
  "total": 10,
  "limit": 50,
  "offset": 0
}

Rate Limits

Default quotas per user:

  • Active invocations: 3 concurrent (pending or in_progress)
  • Hourly rate: 10 invocations per hour

Rate limit error (429 Too Many Requests):

When you exceed the active invocation limit, the response includes details about blocking invocations:

{
  "error": "rate_limit_exceeded",
  "error_description": "Active invocation limit reached: 3/3 concurrent invocations.",
  "details": {
    "context": {
      "limit": 3,
      "current": 3,
      "retry_after": 542,
      "blocking_invocations": [
        {
          "invocation_id": "abc-123",
          "addon_id": "def-456",
          "status": "in_progress",
          "created_at": "2025-01-15T12:00:00Z",
          "expires_at": "2025-01-15T12:15:00Z",
          "seconds_remaining": 542
        }
      ]
    },
    "suggestion": "Wait for an existing invocation to complete, or retry after 542 seconds when the oldest will timeout."
  }
}

The response also includes a Retry-After HTTP header with the recommended wait time in seconds.

Contact your administrator if you need higher limits.

For Administrators

Prerequisites

Before creating addons, you need:

  1. Administrator privileges (configured in YAML):

    administrators:
      - subject: "[email protected]"
        subject_type: "user"
      - subject: "security-team"
        subject_type: "group"
  2. Active webhook subscription:

    POST /webhook/subscriptions
    {
      "name": "STRIDE Analyzer Service",
      "url": "https://analyzer.example.com/webhooks/tmi",
      "events": [],  # Addons don't need event subscriptions
      "secret": "your-hmac-secret"
    }

Registering an Addon

Create addon registration:

POST /addons

{
  "name": "STRIDE Analysis",
  "webhook_id": "webhook-uuid",
  "description": "Automated STRIDE threat analysis",
  "icon": "material-symbols:security",
  "objects": ["threat_model", "asset"],
  "threat_model_id": null  # Optional: scope to specific threat model
}

Response (201 Created):

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "STRIDE Analysis",
  "webhook_id": "webhook-uuid",
  "description": "Automated STRIDE threat analysis",
  "icon": "material-symbols:security",
  "objects": ["threat_model", "asset"],
  "created_at": "2025-01-15T10:00:00Z"
}

Field validation:

icon (optional):

  • Material Symbols: material-symbols:icon_name
  • FontAwesome: fa-solid fa-icon-name
  • Max length: 60 characters

objects (optional):

  • Valid types: threat_model, diagram, asset, threat, document, note, repository, metadata
  • Used as UI hint (not enforced on invocation)

threat_model_id (optional):

  • Scope addon to specific threat model
  • Only visible/invocable within that threat model

Deleting an Addon

DELETE /addons/{addon_id}

Response: 204 No Content (success)

Deletion rules:

  • Blocked if active invocations exist (status: pending or in_progress)
  • Error (409 Conflict): "Cannot delete addon - X active invocations exist"
  • Allowed once all invocations complete or fail
  • Cascaded when webhook is deleted (ON DELETE CASCADE)

Managing Quotas

Set custom quotas for specific users:

Database update (no API yet):

INSERT INTO addon_invocation_quotas
  (owner_id, max_active_invocations, max_invocations_per_hour)
VALUES
  ('user-uuid', 5, 50)
ON CONFLICT (owner_id) DO UPDATE
SET max_active_invocations = EXCLUDED.max_active_invocations,
    max_invocations_per_hour = EXCLUDED.max_invocations_per_hour;

Check quota usage:

-- View custom quotas
SELECT owner_id, max_active_invocations, max_invocations_per_hour
FROM addon_invocation_quotas;

-- Check current usage
SELECT owner_id, COUNT(*) as active_invocations
FROM addon_invocations
WHERE status IN ('pending', 'in_progress')
GROUP BY owner_id;

Monitoring Invocations

Administrators can view all invocations:

GET /invocations
# Returns all users' invocations for admins

Useful queries:

# Active invocations
GET /invocations?status=in_progress

# Failed invocations (last hour)
GET /invocations?status=failed

# By addon
GET /invocations?addon_id={addon_id}

Database queries:

-- Invocation statistics
SELECT addon_id, status, COUNT(*)
FROM addon_invocations
WHERE created_at > NOW() - INTERVAL '24 hours'
GROUP BY addon_id, status;

-- Top users by invocations
SELECT invoked_by, COUNT(*) as count
FROM addon_invocations
WHERE created_at > NOW() - INTERVAL '7 days'
GROUP BY invoked_by
ORDER BY count DESC
LIMIT 10;

For Addon Developers

Webhook Service Requirements

Your webhook service must:

  1. Accept POST requests at the registered URL
  2. Verify HMAC signatures for security
  3. Respond with 200 OK within 30 seconds
  4. Process asynchronously (don't block response)
  5. Choose a callback mode (auto-complete or async callbacks)

Callback Modes

TMI supports two callback modes, controlled by the X-TMI-Callback response header:

Auto-Complete Mode (Default)

When your webhook returns a 2xx response without the X-TMI-Callback header, TMI automatically marks the invocation as completed. Use this when:

  • Your webhook handles work synchronously
  • You don't need to report progress
  • The invocation is "fire and forget"
# Auto-complete mode - invocation marked complete immediately
return '', 200

Async Callback Mode

When your webhook returns X-TMI-Callback: async, TMI marks the invocation as in_progress and expects callbacks. Use this when:

  • Processing takes significant time
  • You want to report progress percentages
  • You need to report success/failure after processing
# Async mode - you must call back with status updates
return '', 200, {'X-TMI-Callback': 'async'}

Important: If using async mode without calling back, the invocation times out after 15 minutes and is marked failed.

Receiving Invocations

When a user invokes your addon, you receive:

POST /webhooks/tmi
Content-Type: application/json
X-Webhook-Event: addon_invocation
X-Invocation-Id: abc-123-def-456
X-Addon-Id: 550e8400-e29b-41d4-a716-446655440000
X-Webhook-Signature: sha256=abc123...
User-Agent: TMI-Addon-Worker/1.0

{
  "event_type": "addon_invocation",
  "invocation_id": "abc-123-def-456",
  "addon_id": "550e8400-e29b-41d4-a716-446655440000",
  "threat_model_id": "threat-model-uuid",
  "object_type": "asset",
  "object_id": "asset-uuid",
  "timestamp": "2025-01-15T12:00:00Z",
  "payload": {
    "analysis_type": "full",
    "include_recommendations": true
  },
  "callback_url": "https://api.tmi.dev/invocations/abc-123-def-456/status"
}

Verifying Signatures

Always verify HMAC signatures:

import hmac
import hashlib

def verify_signature(payload_bytes, signature, secret):
    expected = hmac.new(
        secret.encode('utf-8'),
        payload_bytes,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)

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

    if not verify_signature(payload_bytes, signature, WEBHOOK_SECRET):
        return 'Unauthorized', 401

    # Process invocation
    invocation = request.json
    queue.enqueue(process_invocation, invocation)

    return '', 200

Updating Status

Call back to TMI to update status:

import requests
import hmac
import hashlib
import json

def update_status(invocation_id, status, percent, message=''):
    payload = {
        'status': status,
        'status_percent': percent,
        'status_message': message
    }

    payload_str = json.dumps(payload)

    # Generate HMAC signature
    signature = hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        payload_str.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    response = requests.post(
        f'https://api.tmi.dev/invocations/{invocation_id}/status',
        data=payload_str,
        headers={
            'Content-Type': 'application/json',
            'X-Webhook-Signature': f'sha256={signature}'
        }
    )
    return response.status_code == 200

# Usage during processing
update_status(invocation_id, 'in_progress', 10, 'Starting analysis...')
# ... do work ...
update_status(invocation_id, 'in_progress', 50, 'Analyzing assets...')
# ... do more work ...
update_status(invocation_id, 'completed', 100, 'Analysis complete')

Status update validation:

  • status: Must be in_progress, completed, or failed
  • status_percent: Must be 0-100
  • status_message: Optional, max 255 characters

Valid transitions:

  • pendingin_progresscompleted
  • pendingin_progressfailed
  • Cannot transition from completed or failed back to in_progress

Processing Flow Example

Complete Python example:

from flask import Flask, request
import hmac
import hashlib
import json
import requests

app = Flask(__name__)
WEBHOOK_SECRET = 'your-webhook-secret'

@app.route('/webhooks/tmi', methods=['POST'])
def handle_invocation():
    # Verify signature
    signature = request.headers.get('X-Webhook-Signature')
    payload_bytes = request.get_data()

    if not verify_signature(payload_bytes, signature, WEBHOOK_SECRET):
        return 'Unauthorized', 401

    # Queue for async processing
    invocation = request.json
    queue.enqueue(process_invocation, invocation)

    return '', 200

def process_invocation(invocation):
    invocation_id = invocation['invocation_id']
    threat_model_id = invocation['threat_model_id']
    user_payload = invocation['payload']

    try:
        # Start processing
        update_status(invocation_id, 'in_progress', 0, 'Starting STRIDE analysis')

        # Fetch threat model data from TMI API
        threat_model = fetch_threat_model(threat_model_id)

        # Perform analysis
        update_status(invocation_id, 'in_progress', 25, 'Analyzing threats')
        threats = analyze_stride(threat_model)

        update_status(invocation_id, 'in_progress', 50, 'Analyzing assets')
        asset_risks = analyze_assets(threat_model)

        update_status(invocation_id, 'in_progress', 75, 'Generating report')
        report = generate_report(threats, asset_risks)

        # Complete
        update_status(invocation_id, 'completed', 100, 'Analysis complete')

    except Exception as e:
        # Report failure
        update_status(invocation_id, 'failed', 0, f'Error: {str(e)}')

def fetch_threat_model(threat_model_id):
    response = requests.get(
        f'https://api.tmi.dev/api/v1/threat-models/{threat_model_id}',
        headers={'Authorization': f'Bearer {TMI_TOKEN}'}
    )
    response.raise_for_status()
    return response.json()

if __name__ == '__main__':
    app.run(port=5000)

Testing Your Addon

  1. Local development:

    # Expose local service with ngrok
    ngrok http 5000
    
    # Use ngrok URL for webhook registration
  2. Register webhook and addon:

    # Create webhook
    curl -X POST https://api.tmi.dev/webhook/subscriptions \
      -H "Authorization: Bearer $TOKEN" \
      -d '{"name":"Test","url":"https://abc.ngrok.io/webhooks/tmi","events":[],"secret":"test"}'
    
    # Create addon (admin token required)
    curl -X POST https://api.tmi.dev/addons \
      -H "Authorization: Bearer $ADMIN_TOKEN" \
      -d '{"name":"Test Addon","webhook_id":"webhook-uuid"}'
  3. Invoke and monitor:

    # Invoke addon
    curl -X POST https://api.tmi.dev/addons/{addon_id}/invoke \
      -H "Authorization: Bearer $TOKEN" \
      -d '{"threat_model_id":"tm-uuid","payload":{}}'
    
    # Check status
    curl https://api.tmi.dev/invocations/{invocation_id} \
      -H "Authorization: Bearer $TOKEN"

Best Practices

For Users

  1. Check status regularly: Poll invocations endpoint for updates
  2. Handle rate limits: Don't invoke excessively
  3. Provide context: Use payload to give addon context
  4. Monitor failures: Check failed invocations for errors

For Administrators

  1. Vet addon services: Only register trusted webhook services
  2. Set appropriate quotas: Balance usage and system load
  3. Monitor performance: Track invocation success rates
  4. Clean up unused addons: Delete addons no longer needed
  5. Document addons: Provide users with addon documentation

For Developers

  1. Respond quickly: Return 200 OK within 30 seconds
  2. Process asynchronously: Don't block webhook response
  3. Update status frequently: Keep users informed of progress
  4. Handle errors gracefully: Report failures with helpful messages
  5. Implement timeouts: Don't let processing run indefinitely
  6. Verify signatures: Always validate HMAC signatures
  7. Use idempotency: Handle duplicate invocations gracefully

Troubleshooting

Invocations Not Starting

Check:

  1. Addon exists and is accessible
  2. Rate limits not exceeded
  3. Webhook subscription is active
  4. Webhook service is reachable

Debug:

# Check addon
GET /addons/{addon_id}

# Check webhook status
GET /webhook/subscriptions/{webhook_id}

# Check invocation
GET /invocations/{invocation_id}

Status Not Updating

Causes:

  1. Invalid HMAC signature on status update
  2. Invalid status transition
  3. Invocation expired (7-day TTL)
  4. Network issues from addon service

Debug:

# Check server logs
grep "invocation" /var/log/tmi/server.log

# Verify signature generation
# Test status update manually
curl -X POST https://api.tmi.dev/invocations/{id}/status \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: sha256=..." \
  -d '{"status":"completed","status_percent":100}'

High Failure Rate

Investigate:

# Check failed invocations
GET /invocations?status=failed

# Review error messages

Common causes:

  1. Webhook service errors
  2. Timeout processing
  3. Invalid data from TMI API
  4. External dependency failures

Related Documentation

Clone this wiki locally