Third Party Risk Management

Production engineering guide for third party risk management covering patterns, implementation strategies, and operational best practices.

Third party risk management ensures that systems, processes, and data flowing through external vendors are trustworthy, compliant, and resilient. It matters when a service depends on a cloud provider, a SaaS platform, or a specialized API—especially when failures in a third party directly impact customer-facing systems, regulatory compliance, or data integrity. This is not just onboarding a vendor; it is the continuous validation of their security posture, service level agreements, and operational stability.

Onboarding a Third Party

Onboarding begins with defining a standardized onboarding checklist that captures the vendor’s operational and technical profile. The process starts with a vendor questionnaire delivered as a JSON document via HTTP POST to /api/v1/vendors/onboard.

{
  "vendor_name": "DataFlow Inc.",
  "contact_email": "[email protected]",
  "service_type": "API-based data pipeline",
  "data_classification": ["PII", "PHI", "Financial"],
  "service_level_agreement": {
    "uptime": "99.9%",
    "response_time": "150ms p95",
    "data_latency": "10s max"
  },
  "security_requirements": [
    "SOC 2 Type II",
    "ISO 27001",
    "GDPR Compliance",
    "Encryption at rest and in transit"
  ],
  "integration_type": "REST API",
  "api_endpoint": "https://api.dataflow-inc.com/v1",
  "authentication": "OAuth 2.0 with JWT tokens"
}

The onboarding service validates this payload using a schema defined in schema/vendor-onboarding.json:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["vendor_name", "contact_email", "service_type"],
  "properties": {
    "vendor_name": { "type": "string", "minLength": 1 },
    "contact_email": { "type": "string", "format": "email" },
    "service_type": { "type": "string", "enum": ["API", "SaaS", "Data Warehouse", "Hosting"] },
    "data_classification": {
      "type": "array",
      "items": { "type": "string", "enum": ["PII", "PHI", "Financial", "Confidential"] }
    },
    "service_level_agreement": {
      "type": "object",
      "required": ["uptime", "response_time"],
      "properties": {
        "uptime": { "type": "string", "pattern": "^\\d+(\\.\\d+)?%$" },
        "response_time": { "type": "string", "pattern": "^\\d+ms p\\d{1,3}$" },
        "data_latency": { "type": "string", "pattern": "^\\d+s max$" }
      }
    }
  }
}

Validation fails with a 422 Unprocessable Entity response body:

{
  "error": "Validation failed",
  "details": [
    {
      "field": "service_level_agreement.uptime",
      "message": "Invalid uptime format. Expected: 99.9% or 99.99%",
      "value": "99.95"
    },
    {
      "field": "data_classification",
      "message": "Invalid classification value: 'CustomerData'",
      "value": "CustomerData"
    }
  ]
}

Once the onboarding is accepted, the system generates a vendor ID and triggers a POST /api/v1/vendors/{id}/assign-roles request with a list of roles and responsibilities.

Continuous Vendor Health Monitoring

Monitoring begins with scheduled HTTP probes to the vendor’s API endpoint using curl-based scripts executed every 15 minutes via systemctl-managed jobs.


[Unit]
Description=Health check for third-party vendors
After=network.target

[Service]
Type=oneshot
ExecStart=/opt/scripts/health-check.sh
User=monitoring
Environment=LOG_LEVEL=info

[Install]
WantedBy=multi-user.target

The script health-check.sh performs a series of checks:

#!/bin/bash
set -euo pipefail

LOG_FILE="/var/log/vendor-health-check.log"
VENDOR_CONFIG="/etc/vendor-config.json"

while IFS= read -r vendor_id; do
  vendor_data=$(jq -c -r --arg id "$vendor_id" '
    .vendors[] | select(.id == $id) | .api_endpoint
  ' "$VENDOR_CONFIG")

  endpoint=$(echo "$vendor_data" | jq -r .)

  # Test basic connectivity
  if ! curl -f -s -o /dev/null -H "User-Agent: Health-Checker/1.0" "$endpoint"; then
    echo "$(date): [FAIL] $vendor_id: API endpoint unreachable" >> "$LOG_FILE"
    continue
  fi

  # Test SLA compliance: response time
  start_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
  response=$(curl -s -w "  %T" -o /dev/null -H "X-Request-ID: $vendor_id-$start_time" "$endpoint")

  response_time=$(echo "$response" | awk '{print $NF}')

  if (( $(echo "$response_time > 0.150" | bc -l) )); then
    echo "$(date): [WARN] $vendor_id: Response time $response_time s" >> "$LOG_FILE"
  fi

  # Test data latency: check if data from last hour is available
  latest_timestamp=$(curl -s "$endpoint/data/latest" | jq -r .timestamp)
  now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

  duration=$(echo "$now $latest_timestamp" | awk '{
    split($1, a, "-"); split($2, b, "-");
    a[2]--; a[1]--; a[3]--; b[2]--; b[1]--; b[3]--;
    days = (a[1]-b[1])*365 + (a[2]-b[2])*30 + (a[3]-b[3]);
    hours = (a[4]-b[4]); minutes = (a[5]-b[5]); seconds = (a[6]-b[6]);
    total_seconds = days*86400 + hours*3600 + minutes*60 + seconds;
    print total_seconds
  }')

  if (( $(echo "$duration > 3600" | bc -l) )); then
    echo "$(date): [CRITICAL] $vendor_id: Data latency exceeds 1 hour ($duration seconds)" >> "$LOG_FILE"
  fi

done < <(jq -r '.vendors[].id' "$VENDOR_CONFIG")

Results are logged to /var/log/vendor-health-check.log and ingested into a PostgreSQL database using psql:

CREATE TABLE third_party_health (
  id BIGSERIAL PRIMARY KEY,
  vendor_id TEXT NOT NULL,
  timestamp TIMESTAMPTZ NOT NULL,
  status TEXT NOT NULL,
  response_time_ms NUMERIC,
  data_latency_seconds INTEGER,
  error_message TEXT,
  metadata JSONB
);

INSERT INTO third_party_health (
  vendor_id,
  timestamp,
  status,
  response_time_ms,
  data_latency_seconds,
  error_message,
  metadata
) VALUES (
  'dfi-123',
  '2023-10-05T12:30:00Z',
  'CRITICAL',
  210.3,
  4200,
  'Data not updated in last hour',
  '{"request_id": "dfi-123-20231005T123000Z"}'
);

Risk Assessment and Scoring

Risk assessment is driven by a periodic job that computes a risk score per vendor using a formula based on uptime, data latency, security compliance, and audit readiness.

The assessment is triggered daily by a cron job:

0 2 * * * /opt/scripts/risk-assessment.sh

The script pulls data from the health database and applies scoring rules defined in a YAML configuration file.

# /etc/risk-assessment-config.yaml
thresholds:
  uptime:
    critical: "99.0%",
    warning: "99.5%",
    target: "99.9%"
  data_latency:
    critical: 3600,
    warning: 1800,
    target: 900

scoring:
  uptime:
    weight: 0.4
    scale:
      "99.9%": 100
      "99.5%": 75
      "99.0%": 50
      "98.0%": 25
      "97.0%": 10
  data_latency:
    weight: 0.3
    scale:
      900: 100
      1800: 75
      3600: 50
      7200: 25
  compliance:
    weight: 0.2
    criteria:
      - "SOC 2 Type II"
      - "ISO 27001"
      - "GDPR Compliance"
      - "Encryption at rest and in transit"
  audit_readiness:
    weight: 0.1
    criteria:
      - "Last audit within 12 months"
      - "Audit report available via public URL"
      - "Contactable point of escalation"

The assessment script computes risk scores using jq and python:

#!/usr/bin/env python3
import json
import psycopg2
from datetime import datetime
import yaml

with open("/etc/risk-assessment-config.yaml", "r") as f:
    config = yaml.safe_load(f)

conn = psycopg2.connect("dbname=compliance user=monitoring")
cur = conn.cursor()

cur.execute("""
    SELECT vendor_id, 
           AVG(response_time_ms) AS avg_response_time,
           MAX(data_latency_seconds) AS max_data_latency,
           COUNT(*) AS checks
    FROM third_party_health
    WHERE timestamp >= NOW() - INTERVAL '30 days'
    GROUP BY vendor_id
""")

for vendor_id, avg_response, max_latency, check_count in cur.fetchall():
    score = 0
    total_weight = 0

    # Uptime score
    uptime = (check_count / 30) * 100  # 1 check per day
    if uptime >= 99.9:
        uptime_score = 100
    elif uptime >= 99.5:
        uptime_score = 75
    elif uptime >= 99.0:
        uptime_score = 50
    else:
        uptime_score = 25
    score += uptime_score * config["scoring"]["uptime"]["weight"]
    total_weight += config["scoring"]["uptime"]["weight"]

    # Data latency score
    if max_latency <= 900:
        latency_score = 100
    elif max_latency <= 1800:
        latency_score = 75
    elif max_latency <= 3600:
        latency_score = 50
    else:
        latency_score = 25
    score += latency_score * config["scoring"]["data_latency"]["weight"]
    total_weight += config["scoring"]["data_latency"]["weight"]

    # Compliance score
    compliance_score = 0
    required_compliance = config["scoring"]["compliance"]["criteria"]
    for criterion in required_compliance:
        # Simulate check against vendor metadata
        if criterion in ["SOC 2 Type II", "ISO 27001", "GDPR Compliance"]:
            compliance_score += 1
    compliance_score = (compliance_score / len(required_compliance)) * 100
    score += compliance_score * config["scoring"]["compliance"]["weight"]
    total_weight += config["scoring"]["compliance"]["weight"]

    # Audit readiness
    audit_score = 0
    if check_count >= 25:  # At least 25 days of health checks
        audit_score += 25
    if True:  # Assume public audit report exists
        audit_score += 25
    if True:  # Assume contact available
        audit_score += 50
    score += audit_score * config["scoring"]["audit_readiness"]["weight"]
    total_weight += config["scoring"]["audit_readiness"]["weight"]

    final_score = score / total_weight

    cur.execute("""
        INSERT INTO third_party_risk (vendor_id, score, calculated_at)
        VALUES (%s, %s, %s)
        ON CONFLICT (vendor_id) DO UPDATE
        SET score = EXCLUDED.score, calculated_at = EXCLUDED.calculated_at
    """, (vendor_id, final_score, datetime.utcnow()))

conn.commit()
cur.close()
conn.close()

Risk Alerting and Escalation

Alerts are generated when a vendor’s risk score drops below a threshold or when a critical health check fails.

Alerts are published to a central event bus using curl:

# Alert on critical risk score
if (( $(echo "$final_score < 70" | bc -l) )); then
  curl -X POST \
       -H "Content-Type: application/json" \
       -d '{
         "event_type": "third_party_risk_alert",
         "vendor_id": "dfi-123",
         "severity": "CRITICAL",
         "title": "Vendor Risk Score Dropped Below 70",
         "description": "Current score: 68.3",
         "details": {
           "risk_score": 68.3,
           "uptime": 98.4,
           "data_latency": 4500,
           "compliance": 80,
           "audit_readiness": 95
         },
         "timestamp": "2023-10-05T14:00:00Z"
       }' \
       http://event-bus.internal/api/v1/events
fi

The event bus routes alerts to teams via Slack and email. An alert includes a link to a dashboard showing historical risk scores and health metrics.

Silent Failures and Sharp Edges

Vendor Risk Review and Remediation

Monthly reviews are triggered by a systemd timer that runs a script to generate a report.

The report is a PDF document generated using weasyprint from a Jinja2 template and a JSON payload from the database.

<!-- /templates/vendor-report.html -->
<html>
  <head>
    <title>Third Party Risk Report: {{ vendor.name }}</title>
    <style>
      body { font-family: "Helvetica", sans-serif; }
      .score { font-size: 1.5em; color: #2c3e50; }
    </style>
  </head>
  <body>
    <h1>Third Party Risk Report: {{ vendor.name }}</h1>
    <p>Generated on {{ now }}</p>
    <div class="score">Risk Score: {{ score }}</div>
    <ul>
      <li>Uptime: {{ uptime }}%</li>
      <li>Data Latency: {{ latency }}s</li>
      <li>Compliance: {{ compliance }}%</li>
      <li>Audit Readiness: {{ audit }}%</li>
    </ul>
    <h2>Recent Health Events</h2>
    <table>
      <tr><th>Timestamp</th><th>Status</th><th>Response Time (ms)</th><th>Data Latency (s)</th></tr>
      {% for event in recent_events %}
      <tr>
        <td>{{ event.timestamp }}</td>
        <td>{{ event.status }}</td>
        <td>{{ event.response_time_ms }}</td>
        <td>{{ event.data_latency_seconds }}</td>
      </tr>
      {% endfor %}
    </table>
  </body>
</html>

The weasyprint command generates the PDF:

weasyprint \
  --cover /templates/vendor-report.html \
  --title "Third Party Risk Report" \
  --css /styles/report.css \
  /tmp/vendor-report.html \
  /reports/2023-10-05/dfi-123.pdf

The report is then archived and linked in the vendor’s profile, with a POST /api/v1/vendors/{id}/review request that includes the PDF path and a review summary.

{
  "reviewer": "[email protected]",
  "review_date": "2023-10-05",
  "summary": "Data latency exceeds threshold; compliance score is strong.",
  "recommendations": [
    "Require quarterly audit reports",
    "Implement SLA monitoring for response time"
  ],
  "attachment": "/reports/2023-10-05/dfi-123.pdf"
}

This completes the loop: onboarding, monitoring, assessment, alerting, and review—each step grounded in real, executable, and observable processes.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.