Webhook Introduction

Webhook is a HTTP request from Gauntlet to any URL you configure to notify you when an event occurs. Those events are configured on Gauntlet notifications.

 

Characteristics of a Webhook

Webhooks have a few peculiarities that you should be aware:

  • Request User-Agent: every request uses the Gauntlet value in the User-Agent header;
  • Request Method: webhook uses the POST verb;
  • Retries: Gauntlet will try to reach the endpoint and expect a successful response (HTTP 200 OK) up to 10 times and will increase the time interval between attempts;
  • Timeout: webhooks have a 3 minutes timeout before considering the attempt as a failed one. It's important to know this because your application logic must be indempotent;
  • Certificate Agnostic: Gauntlet will reach the URL you set whether it be HTTP, HTTPS with a valid certificate or HTTPS with an invalid certificate (expired, revoked or self-signed);
  • JSON Request Body: the webhook request will contain a JSON with the event details;
  • Signed Requests: all webhook requests are signed, check below how to verify the authenticity of them.

 

Cryptographic Signature

To guarantee that the webhook request came from Gauntlet and not from anyone else, you can rely on Gauntlet signature that comes along with the webhook.

The signature is present in the header X-Gauntlet-Signature. It uses RSA encryption and can be verified using the following public key:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4g/GXMTEdRYkrBUpP5eZ
rksBLeWdYXq/VaDviz4vdeEH2hMMk0RtVZEYA5sn+PMufdrqa9Y1V4LRwOgN0v3M
BgMAxBMknC1j3GUAxDyrrn50w+iOGsJa3Rl2v/fsN+u1208h5uAgk4j+ctVeMirQ
r4QvEkFvtU5u+wgI5r5vq69QrtTg1iwYpPIl6icgPUUvQq1LoICve6Lswmm53Fg+
0ZHrnObrj6LjhtW+smiGjO7mnUd5OBBTIwgDP+WJp4Kv/rDZHbWi4LCuaQOn1nz2
uI7iP8JnnslL6hOJFjmi/uB0LUliYmbwUBqFOprzt617w9a92dtNRMIg4X98NJQK
OQIDAQAB
-----END PUBLIC KEY-----

With this public RSA key in hands you can use the following algorithm (in Ruby for this example) to verify the authenticity of the webhook:

require 'openssl'
require 'base64'

# Get the RAW request body and put it into a variable
# We'll name this variable as 'raw_request_body'
# The raw request is only the JSON data you'll receive
#
#
# Then we generate the SHA 256 hex digest of it
#
computed_payload_digest = Digest::SHA256.hexdigest raw_request_body

# Now we load the public key
# Just copy the contents of the RSA Public Key above in here
public_key_contents = <<-HEREDOC
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4g/GXMTEdRYkrBUpP5eZ
rksBLeWdYXq/VaDviz4vdeEH2hMMk0RtVZEYA5sn+PMufdrqa9Y1V4LRwOgN0v3M
BgMAxBMknC1j3GUAxDyrrn50w+iOGsJa3Rl2v/fsN+u1208h5uAgk4j+ctVeMirQ
r4QvEkFvtU5u+wgI5r5vq69QrtTg1iwYpPIl6icgPUUvQq1LoICve6Lswmm53Fg+
0ZHrnObrj6LjhtW+smiGjO7mnUd5OBBTIwgDP+WJp4Kv/rDZHbWi4LCuaQOn1nz2
uI7iP8JnnslL6hOJFjmi/uB0LUliYmbwUBqFOprzt617w9a92dtNRMIg4X98NJQK
OQIDAQAB
-----END PUBLIC KEY-----
HEREDOC
public_key = OpenSSL::PKey::RSA.new(public_key_contents)

# Then we:
# 1) Decode the Base64 encoding
# 2) Decrypt the signature to get:
#    - the SHA 256 hex digest computed by Gauntlet
#
received_signature = request["headers"]["X-Gauntlet-Signature"]
received_signature_raw = Base64.decode64(received_signature)
gauntlet_payload_digest = public_key.public_decrypt(received_signature_raw)

# Is the request authentic?
# It's time to verify:
#     our computed payload digest against
#     the one present in the signature
#
if computed_payload_digest == gauntlet_payload_digest
    puts "This is a valid request from Gauntlet"
else
    puts "Oh my god. Are you a hacker?!!"
end

 

Example

Below you'll find an example of a webhook triggered when a scan starts. Important: don't try to match the signature of this example, as it used our development RSA key pair and it has been slightly modified for presentation purposes.

POST / HTTP/1.1
Accept: */*; q=0.5, application/xml
Accept-Encoding: gzip, deflate
Content-Type: application/json
User-Agent: Gauntlet
X-Gauntlet-Signature: Ol1hgJiLnRTi1lm+DbGfpur++4TF8SihJI/hUimzREEpVKV5/RacGRct/J8H6YBKiSZZ/UI+p14hlL7vMOH3mxj8m1oFUfkSAAC74vW47g9jU1I3Is75ug68/V7gHgLCBHe/WlwGIgrhHq75c79K+LdLeezysbi9bAlfU0yiD1CBEiHuYc2ZbmaRW5Qzcm6gG/H7LlDfJZtZ0gnrF+GlnfQb4ywWBAo22aSmSCdJyhQIi33eaTrnIeXkd3bap4Cf+MjZUCrnXpIeePSZzRyrZzEqEXN0CreD61Zb3Hgk22aKkAgUo4SNLi3F3m3qqWF0jNvsipU2n8ffED3sNkoRFg==
X-Gauntlet-Verification-Help: "Access https://gauntlet.io/en/product/docs/webhook to know how to validate this signature"
X-Gauntlet-Report-Abuse: "Didn't you request this webhook? Report to contact@gauntlet.io"
Content-Length: 1650
Host: webhook.mywebsite.com

{
    "webhook": {
        "sent_at": "2016-11-25T11:55:41.610Z"
    },
    "event": {
        "id": 1,
        "name": "Start Scan",
        "family": "Scan Management"
    },
    "organization": {
        "id": 11,
        "name": "Gauntlet.io",
        "created_at": "2016-02-28T03:42:03.000Z",
        "updated_at": "2016-11-03T12:12:57.000Z"
    },
    "application": {
        "id": 32,
        "name": "Gauntlet",
        "url": "https://gauntlet.io:443",
        "self_signed_certificate": true,
        "internal": false,
        "proxy_code": null,
        "business_criticality": "Medium",
        "certificate_expiration_compliant_status": true,
        "certificate_expiration_compliant_date": "2016-11-24T13:05:21.000Z",
        "max_class_compliant_status": false,
        "max_class_compliant_date": "2016-06-06T22:57:24.000Z",
        "max_fix_compliant_status": false,
        "max_fix_compliant_date": "2016-06-06T22:55:38.000Z",
        "max_issues_compliant_status": true,
        "max_issues_compliant_date": "2016-06-06T22:49:52.000Z",
        "min_scan_compliant_status": false,
        "min_scan_compliant_date": "2016-06-06T22:58:45.000Z",
        "created_at": "2016-03-10T03:46:50.000Z",
        "updated_at": "2016-11-24T13:05:21.000Z"
    },
    "scan": {
        "id": 229,
        "scan_type": "DAST",
        "foreign_report_id": null,
        "status": 2,
        "progress": 0,
        "started_at": null,
        "finished_at": null,
        "application_id": 32,
        "droplet_ip": null,
        "proxy_ip": null,
        "proxy_tunnel_established": false,
        "start_path": "/",
        "max_execution_time": null,
        "extra_request_headers": {},
        "package_delay": null,
        "virtual_appliance": null,
        "report": null,
        "scan_scanners": [{
            "id": 737,
            "scanner": {
                "id": 3,
                "name": "Wapiti"
            },
            "progress": null,
            "started_at": null,
            "finished_at": null,
            "status": 1,
            "duplicated_issue_ids": [],
            "issues": []
        }, {
            "id": 738,
            "scanner": {
                "id": 5,
                "name": "ZAP"
            },
            "progress": null,
            "started_at": null,
            "finished_at": null,
            "status": 1,
            "duplicated_issue_ids": [],
            "issues": []
        }]
    },
    "user": {
        "id": 11,
        "full_name": "Anderson Dadario"
    }
}