{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://reconfirm.com/schemas/webhook_payload.schema.json",
  "title": "ReConfirm EASM Scan Webhook Payload",
  "description": "Payload POSTed to a client's configured webhook URL when a ReConfirm scan finishes (see core/post_scan.py:WebhookSender.generate_payload). Sent as the JSON body of an HTTP POST, authenticated via an 'Authorization: Bearer <token>' header. All timestamps in this payload are in UTC (see $defs/isoTimestamp).",
  "type": "object",
  "required": ["scan_id", "info", "issuer", "target", "results"],
  "additionalProperties": false,
  "properties": {
    "scan_id": {
      "type": "string",
      "description": "Unique identifier of the scan that produced this payload."
    },
    "info": {
      "type": "object",
      "required": ["name", "desc", "start", "end", "success"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string", "description": "Name given to the scan configuration." },
        "desc": { "type": ["string", "null"], "description": "Description given to the scan configuration." },
        "start": { "$ref": "#/$defs/isoTimestamp", "description": "Scan start time, UTC." },
        "end": {
          "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/isoTimestamp" }],
          "description": "Scan end time, UTC. Null if the scan errored before finishing."
        },
        "success": { "type": "boolean", "description": "False if the scan status indicates an error." }
      }
    },
    "issuer": {
      "type": "object",
      "required": ["name", "email", "ip"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string", "description": "Name of the engineer/user who ran or scheduled the scan." },
        "email": { "type": ["string", "null"], "format": "email" },
        "ip": { "type": "string", "description": "Source IP that triggered the scan." }
      }
    },
    "target": {
      "type": "object",
      "required": ["domain"],
      "additionalProperties": false,
      "properties": {
        "domain": { "type": "string", "description": "Root domain that was scanned." }
      }
    },
    "results": {
      "type": "object",
      "required": ["email_security", "similar_domains", "associated_domains", "cred_leaks", "assets", "subdomains"],
      "additionalProperties": false,
      "properties": {
        "email_security": {
          "description": "Email authentication posture (SPF/DKIM/DMARC/MX). Null if this module was never run for the target.",
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "spf":   { "$ref": "#/$defs/emailSecFinding" },
                "dkim":  { "$ref": "#/$defs/emailSecFinding" },
                "dmarc": { "$ref": "#/$defs/emailSecFinding" },
                "mx": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "value":  { "type": ["array", "null"], "items": { "type": "string" } },
                    "remark": { "type": ["string", "null"] }
                  }
                }
              }
            }
          ]
        },

        "similar_domains": {
          "type": "object",
          "description": "Lookalike/typosquat domains found, keyed by domain name.",
          "additionalProperties": { "$ref": "#/$defs/similarDomain" }
        },

        "associated_domains": {
          "type": "object",
          "description": "Domains seen associated with the target (e.g. via shared assets/services), keyed by domain name.",
          "additionalProperties": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "time_seen":   { "type": "integer", "description": "Number of times this domain was observed during the scan." },
              "url":         { "type": ["string", "null"] },
              "status_code": { "type": ["integer", "null"] },
              "page_title":  { "type": ["string", "null"] }
            }
          }
        },

        "cred_leaks": {
          "type": "object",
          "description": "Leaked-credential findings, keyed by leaked email address. Empty object if no data-leaks scan has run or nothing was found.",
          "additionalProperties": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "password":     { "type": ["string", "null"] },
                "sources":      { "type": "array", "items": { "type": "string" }, "description": "Breach/source names the credential was found in." },
                "last_breach":  { "type": ["string", "null"] }
              }
            }
          }
        },

        "assets": {
          "type": "object",
          "description": "Discovered IP assets, keyed by IP address.",
          "additionalProperties": {
            "type": "object",
            "required": ["ports", "user_tags", "vulns"],
            "additionalProperties": false,
            "properties": {
              "ports": {
                "type": "object",
                "description": "Keyed by port number (as a string).",
                "additionalProperties": { "$ref": "#/$defs/assetPort" }
              },
              "user_tags": { "type": "array", "items": { "type": "string" } },
              "vulns": {
                "type": "object",
                "description": "Vulnerabilities found directly on this IP (not tied to a specific subdomain/hostname), keyed by finding ID. Never includes an 'ip' field since the IP is the parent key.",
                "additionalProperties": { "$ref": "#/$defs/vulnerability" }
              }
            }
          }
        },

        "subdomains": {
          "type": "object",
          "required": ["unresolved", "offline", "active"],
          "additionalProperties": false,
          "properties": {
            "unresolved": {
              "type": ["array", "null"],
              "items": { "type": "string" },
              "description": "Subdomains that were discovered but did not resolve to an IP."
            },
            "offline": {
              "type": "array",
              "description": "Subdomains that resolved but were not reachable/probeable. Each entry is a [subdomain, ip] pair.",
              "items": {
                "type": "array",
                "prefixItems": [{ "type": "string" }, { "type": "string" }],
                "minItems": 2,
                "maxItems": 2
              }
            },
            "active": {
              "type": "object",
              "description": "Subdomains that were successfully probed, keyed by subdomain/hostname.",
              "additionalProperties": { "$ref": "#/$defs/activeSubdomain" }
            }
          }
        }
      }
    }
  },

  "$defs": {
    "isoTimestamp": {
      "type": "string",
      "format": "date-time",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+00:00$",
      "description": "ISO-8601 UTC timestamp, second precision (no fractional seconds), with an explicit '+00:00' offset rather than 'Z' — e.g. '2026-07-09T13:00:11+00:00'.",
      "examples": ["2026-07-09T13:00:11+00:00"]
    },

    "emailSecFinding": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "value":  { "type": ["string", "null"] },
        "remark": { "type": ["string", "null"] }
      }
    },

    "sslCertInfo": {
      "description": "Certificate details. Shape comes from the underlying TLS probe and may include additional fields beyond those listed; 'expiration' is always ISO-8601 UTC when present.",
      "type": "object",
      "additionalProperties": true,
      "properties": {
        "tls_version": { "type": ["string", "null"] },
        "issuer_cn":   { "type": ["string", "null"] },
        "issuer_o":    { "type": ["string", "null"] },
        "issuer_ou":   { "type": ["string", "null"] },
        "subject_cn":  { "type": ["string", "null"] },
        "subject_o":   { "type": ["string", "null"] },
        "subject_ou":  { "type": ["string", "null"] },
        "self_signed": { "type": ["boolean", "null"] },
        "expiration":  { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/isoTimestamp" }], "description": "Certificate expiration time, UTC." }
      }
    },

    "similarDomain": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "dns": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "a":  { "type": ["array", "null"], "items": { "type": "string" } },
            "mx": { "type": ["array", "null"], "items": { "type": "string" } },
            "ns": { "type": ["array", "null"], "items": { "type": "string" } }
          }
        },
        "geoip":       { "type": ["string", "null"] },
        "page_title":  { "type": ["string", "null"] },
        "status_code": { "type": ["integer", "null"] },
        "ssl":         { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/sslCertInfo" }] },
        "mx_ssl": {
          "description": "Keyed by mail server hostname.",
          "oneOf": [
            { "type": "null" },
            { "type": "object", "additionalProperties": { "$ref": "#/$defs/sslCertInfo" } }
          ]
        }
      }
    },

    "assetPort": {
      "description": "Port entry as it appears under results.assets.<ip>.ports. NOTE: the shape of 'service' differs from the subdomain-level port entry (see activeSubdomainPort) — here it is a nested object.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "name":        { "type": ["string", "null"], "description": "Service name per nmap/port fingerprint db, e.g. 'https'." },
        "description": { "type": ["string", "null"] },
        "service": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "name":    { "type": ["string", "null"], "description": "Detected product/software name, e.g. 'nginx'." },
            "version": { "type": ["string", "null"] }
          }
        }
      }
    },

    "activeSubdomainPort": {
      "description": "Port entry as it appears under results.subdomains.active.<host>.ports. NOTE: unlike assetPort, 'service' here is a plain string (the product name), with 'version' as a sibling field rather than nested.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "name":        { "type": ["string", "null"] },
        "description": { "type": ["string", "null"] },
        "service":     { "type": ["string", "null"], "description": "Detected product/software name, e.g. 'nginx'." },
        "version":     { "type": ["string", "null"] }
      }
    },

    "vulnerability": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "url":       { "type": ["string", "null"] },
        "severity": {
          "type": ["integer", "null"],
          "enum": [-1, 0, 1, 2, 3, 4, null],
          "description": "-1=unknown, 0=info, 1=low, 2=medium, 3=high, 4=critical."
        },
        "name":         { "type": ["string", "null"], "description": "Name of the detection rule/template." },
        "desc":         { "type": ["string", "null"] },
        "references":   { "type": ["array", "null"], "items": { "type": "string" } },
        "result":       { "type": ["array", "null"], "items": { "type": "string" }, "description": "Raw matcher output/evidence from the detection template." },
        "user_tags":    { "type": "array", "items": { "type": "string" } },
        "mitigation":   { "type": ["string", "null"] },
        "cve_id":       { "type": ["string", "null"] },
        "cwe_id":       { "type": ["string", "null"] },
        "cvss_metrics": { "type": ["string", "null"], "description": "CVSS vector string, e.g. 'CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N'." },
        "cvss_score":   { "type": ["number", "null"] },
        "epss_score":   { "type": ["number", "null"] },
        "epss_percentile": { "type": ["number", "null"] },
        "cpe":          { "type": ["string", "null"] },
        "ip": {
          "type": ["string", "null"],
          "description": "Only present on findings under results.subdomains.active.<host>.vulns — the IP the finding was made against. Absent for findings under results.assets.<ip>.vulns, since the IP is already the parent key there."
        }
      }
    },

    "activeSubdomain": {
      "type": "object",
      "required": ["url", "page_title", "status_code", "user_tags", "ips", "ports", "ssl", "headers", "vulns", "suffixes"],
      "additionalProperties": false,
      "properties": {
        "url":         { "type": ["string", "null"] },
        "page_title":  { "type": ["string", "null"] },
        "status_code": { "type": ["integer", "null"] },
        "user_tags":   { "type": "array", "items": { "type": "string" } },
        "ips":         { "type": ["array", "null"], "items": { "type": "string" } },
        "ports": {
          "type": "object",
          "description": "Keyed by port number (as a string).",
          "additionalProperties": { "$ref": "#/$defs/activeSubdomainPort" }
        },
        "ssl": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "grade": { "type": ["string", "null"], "description": "Qualitative SSL/TLS grade, e.g. 'A', 'B', 'F'." },
            "info": {
              "description": "Certificate details; empty object if none available. Same field set as sslCertInfo.",
              "type": "object",
              "additionalProperties": true
            }
          }
        },
        "headers": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "found": {
              "type": "object",
              "description": "HTTP response headers observed, keyed by header name.",
              "additionalProperties": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "value": { "type": "string" },
                  "security_impact": {
                    "type": "integer",
                    "enum": [-1, 0, 1],
                    "description": "-1 = header decreases security, 0 = neutral, 1 = header increases security."
                  }
                }
              }
            },
            "missing": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "grade":   { "type": ["string", "null"], "description": "Grade for missing security headers, e.g. 'A'-'F'." },
                "headers": { "type": "array", "items": { "type": "string" }, "description": "Names of expected security headers that were not present." }
              }
            }
          }
        },
        "vulns": {
          "type": "object",
          "description": "Vulnerabilities found on this subdomain, keyed by finding ID. Each entry includes an 'ip' field.",
          "additionalProperties": { "$ref": "#/$defs/vulnerability" }
        },
        "suffixes": {
          "type": "object",
          "description": "Discovered directory/file suffixes (e.g. from content discovery), keyed by suffix/path.",
          "additionalProperties": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "size":        { "type": "string", "description": "Human-readable size, e.g. '12 KB'." },
              "status_code": { "type": ["integer", "null"] }
            }
          }
        }
      }
    }
  }
}
