I made some improvements to the NetBox webhook listener presented in my earlier post. Here is the new one:
import hmac
import json
import logging
from flask import Flask, request
from flask_restplus import Api, Resource, fields
app = Flask(__name__)
api = Api(app, version="1.1", title="NetBox Webhook Listener",
description="Tested with NetBox 2.6.6")
ns = api.namespace("webhook")
APP_NAME = "webhook-listener"
WEBHOOK_SECRET = "My precious"
logger = logging.getLogger(APP_NAME)
logger.setLevel(logging.INFO)
formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s: %(message)s")
file_logging = logging.FileHandler("{}.log".format(APP_NAME))
file_logging.setFormatter(formatter)
logger.addHandler(file_logging)
webhook_request = api.model("Webhook request from NetBox", {
'username': fields.String,
'data': fields.Raw(description="The object data from NetBox"),
'event': fields.String,
'timestamp': fields.String,
'model': fields.String,
'request_id': fields.String,
})
def do_something_with_the_event(data):
logger.info(data)
pass
@ns.route("/")
class WebhookListener(Resource):
@ns.expect(webhook_request)
@ns.doc(responses={200: "Ok", 400: "Bad request"})
def post(self):
if request.content_length > 1_000_000:
# To prevent memory allocation attacks
logger.error("Content too long ({})".format(
request.content_length
))
return {"result": "Content too long"}, 400
input_data = request.get_data()
x_hook_signature = request.headers.get("X-Hook-Signature")
if x_hook_signature:
input_hmac = hmac.new(
key=WEBHOOK_SECRET.encode(),
msg=input_data,
digestmod="sha512"
)
if not hmac.compare_digest(
input_hmac.hexdigest(),
x_hook_signature):
logger.error("Invalid message signature")
return {"result": "Invalid message signature"}, 400
logger.info("Message signature checked ok")
else:
logger.info("No message signature to check")
try:
input_dict = json.loads(input_data)
except json.JSONDecodeError:
input_dict = None
if not input_dict or "model" not in input_dict:
logger.error("Invalid input: {}".format(input_data))
return {"result":"Invalid input"}, 400
do_something_with_the_event(input_dict)
return {"result":"ok"}, 200
if __name__ == "__main__":
app.run(host="0.0.0.0")
Example from the log file:
2019-10-13 15:06:22,932 webhook-listener INFO: Message signature checked ok
2019-10-13 15:06:22,932 webhook-listener INFO: {'request_id': 'f731df57-08a4-4eff-b07e-3633770e449c', 'event': 'updated', 'data': {'slug': 'my-testing-tenant', 'group': None, 'tags': ['Finland', 'Torilla tavataan', 'Suomi mainittu'], 'name': 'My Testing Tenant', 'id': 7, 'description': '', 'created': '2019-10-12', 'custom_fields': {}, 'last_updated': '2019-10-13T12:06:22.839338Z', 'comments': ''}, 'username': 'admin', 'model': 'tenant', 'timestamp': '2019-10-13 12:06:22.903111'}
Notable changes:
- Added the message digest verification (
X-Hook-Signatureheader sent by NetBox) - Added content length checking as an example of all the worries you need to take care of when publishing an interface
Links: