From 09c6f9263f75f4be0d39b8bd5af5b1843e700e76 Mon Sep 17 00:00:00 2001 From: bmc-msft <41130664+bmc-msft@users.noreply.github.com> Date: Thu, 11 Mar 2021 17:08:13 -0500 Subject: [PATCH] sample webhook service (#666) --- .github/workflows/ci.yml | 18 ++++ contrib/webhook-teams-service/.gitignore | 43 +++++++++ contrib/webhook-teams-service/README.md | 85 ++++++++++++++++++ .../webhook-teams-service/example-message.png | Bin 0 -> 14199 bytes contrib/webhook-teams-service/host.json | 15 ++++ .../webhook-teams-service/local.settings.json | 6 ++ contrib/webhook-teams-service/mypy.ini | 14 +++ .../webhook-teams-service/requirements.txt | 4 + .../webhook-teams-service/webhook/__init__.py | 75 ++++++++++++++++ .../webhook/function.json | 19 ++++ 10 files changed, 279 insertions(+) create mode 100644 contrib/webhook-teams-service/.gitignore create mode 100644 contrib/webhook-teams-service/README.md create mode 100755 contrib/webhook-teams-service/example-message.png create mode 100644 contrib/webhook-teams-service/host.json create mode 100644 contrib/webhook-teams-service/local.settings.json create mode 100644 contrib/webhook-teams-service/mypy.ini create mode 100644 contrib/webhook-teams-service/requirements.txt create mode 100644 contrib/webhook-teams-service/webhook/__init__.py create mode 100644 contrib/webhook-teams-service/webhook/function.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4104e5566..358f9f314 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,6 +113,24 @@ jobs: # set a minimum confidence to ignore known false positives vulture --min-confidence 61 onefuzz + contrib-webhook-teams-service: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: lint + shell: bash + run: | + set -ex + cd contrib/webhook-teams-service + python -m pip install --upgrade pip isort black mypy flake8 + pip install -r requirements.txt + mypy webhook + black webhook --check + isort --profile black webhook + flake8 webhook deploy-onefuzz-via-azure-devops: runs-on: ubuntu-18.04 steps: diff --git a/contrib/webhook-teams-service/.gitignore b/contrib/webhook-teams-service/.gitignore new file mode 100644 index 000000000..fbbe2efac --- /dev/null +++ b/contrib/webhook-teams-service/.gitignore @@ -0,0 +1,43 @@ +bin +obj +csx +.vs +edge +Publish + +*.user +*.suo +*.cscfg +*.Cache +project.lock.json + +/packages +/TestResults + +/tools/NuGet.exe +/App_Data +/secrets +/data +.secrets +appsettings.json +local.settings.json + +node_modules +dist + +# Local python packages +.python_packages/ + +# Python Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class \ No newline at end of file diff --git a/contrib/webhook-teams-service/README.md b/contrib/webhook-teams-service/README.md new file mode 100644 index 000000000..70df90b74 --- /dev/null +++ b/contrib/webhook-teams-service/README.md @@ -0,0 +1,85 @@ +# Webhooks Endpoint Example + +This example endpoint takes any incoming [OneFuzz webhook](../../docs/webhooks.md) and submits it to Microsoft Teams. + +Check out [Webhook Events Details](../../docs/webhook_events.md) for the schema of all supported events. + +## Creating an Azure Function + +1. Edit `local.settings.json` and add the following to the `Values` dictionary: + * Create a [Microsoft Teams incoming webhook URL](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#setting-up-a-custom-incoming-webhook), and set this the value for `TEAMS_URL`. + * Create a random string that you generate, and set this value for `HMAC_TOKEN`. This will be used to [help secure your webhook](https://github.com/microsoft/onefuzz/blob/main/docs/webhooks.md#securing-your-webhook) +2. [Create Azure Resources for an Azure Function](https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-python?tabs=azure-cli%2Cbash%2Cbrowser#5-create-supporting-azure-resources-for-your-function). +3. Ensure your function is HTTPS only: + ```bash + az functionapp update --resource-group --name --set httpsOnly=true + ``` +4. Deploy your function + ```bash + func azure functionapp publish --publish-local-settings + ``` +5. From the previous command, write down the URL for your webhook. It should look something like this: + ``` + webhook - [httpTrigger] + Invoke url: https://.azurewebsites.net/api/webhook?code= + ``` +6. Register this new URL webhook to OneFuzz. In this example, we're registering a webhook to tell our service any time a job is created and stopped: + ```bash + onefuzz webhooks create my-webhook "https://.azurewebsites.net/api/webhook?code=" job_created job_stopped --secret_token + ``` + + > NOTE: Make sure `HMAC_TOKEN` is the value we added to `local.settings.json` earlier. + + This will respond with something akin to: + ```json + { + "event_types": [ + "job_created", + "job_stopped" + ], + "name": "my-webhook", + "webhook_id": "9db7a8bb-0680-42a9-b336-655d3654fd6c" + } + ``` +7. Using the `webhook_id` we got in response, we can test our webhook using: + ```bash + onefuzz webhooks ping 9db7a8bb-0680-42a9-b336-655d3654fd6c + ``` +8. Using `webhook_id` we got in response, we can test if OneFuzz was able to send our service webhooks: + ``` + onefuzz webhooks logs 9db7a8bb-0680-42a9-b336-655d3654fd6c + ``` + + If our webhook is successful, we'll see something akin to: + ```json + [ + { + "event": { + "ping_id": "0770679d-67a0-4a6e-a5d7-751c7f80ebab" + }, + "event_id": "0c12ca77-bff8-4f8b-ae0d-f38f64cf0247", + "event_type": "ping", + "instance_id": "833bd437-775c-4b80-be62-599a9907f0f9", + "instance_name": "YOUR-ONEFUZZ-INSTANCE-NAME", + "state": "succeeded", + "try_count": 1, + "webhook_id": "9db7a8bb-0680-42a9-b336-655d3654fd6c" + } + ] + ``` + + OneFuzz will attempt to send each event up to 5 times before giving up. Instead of `succeeded` as above, you might see `retrying` if OneFuzz is still working to send the event, or `failed` if OneFuzz has given up sending the event. +9. Check your Teams channel. If all is successful, we should see something like the following: + ![Teams message Screenshot](example-message.png) + +## Troubleshooting + +* If your function isn't working as expected, check out the logs for your Azure Functions via: + ``` + func azure functionapp logstream --browser + ``` +* If you see exceptions that say `missing HMAC_TOKEN` or `missing TEAMS_URL`, you forgot to add those settings above. +* If you see exceptions saying `missing X-Onefuzz-Digest`, you forgot to set the `--secret_token` when you during `onefuzz webhooks create` above. This can be addressed via: + ``` + onefuzz webhooks update --secret_token + ``` diff --git a/contrib/webhook-teams-service/example-message.png b/contrib/webhook-teams-service/example-message.png new file mode 100755 index 0000000000000000000000000000000000000000..c202b0bc36005702d738eb7a27536599dcb77b3a GIT binary patch literal 14199 zcmd732UL^W+Aa#Zz=D)z0cm2RNfQL=ASxgrqJTuYC`b*V_qJ4E2@<4>lpsy1k(SUw zSyB=pgcc!zfRq3sbciGb?ibhj_jdn#-#hL(&Ue1$dFFg||DHbQ zae?D3EG(P`cW#@qupCwazJET-4tyToJ<|^ySba_PZ?aVO3oQaaesk6_)?s0(O61sc zU;}<1^SWc{%fiCd!Te|Kf_!vjVNpI|a9hVb$c{2`B>T+BSl7CY-{PIs$ElBd1nrk! zz1t|2_+9A&R+-oIsz6HKMO~f?SK_K3|91L}&STe$EquQl{r2_wMTzIpPk0t}<5EdE z&Rb*DtJxDdmNDIxZH%y1Sg3P<<~34w(~4%~vlXQRhmM#-fFvwmSy_(&hZ|Ko?X1A@ zz6XyN^Ym%K--MXQuXzL}->r*bMON5{8yr`u3C#+i2o8p|>T?g`Xf+W$(^2{r7 zVzx76D`E!giN6I&TFW`1qW1U@7jW(V-lO&&x!Kd`D6_VR@V`&>E(qA;Xn#wmdycj1 zn&jWD9D+In0|7{9{F@M_koisG-BFcXgP*-pEIQ{Upy+YAPB6mW?Yf4go05j4dCl}I z(;BSubB0n!xP3_0m$k!#AS(kHi@mOW03oZCXf-_%5m z8TPw{m+Vbd6Yp$};!xfh41*^rl=z()x%P~?zwd4X6V?DIj*tVh>9%{HJjFNpB@HKCS>Kpsvo#(rdAU9#b8qFa#BFM>qu!@Z)wVD7B}IXJ(ow7DOSH_>P@s{AnmK0*OCu2zX7gV2Bi?gy3pmTi%d zdx4waoqczicw;`%LqY^vafvK8nrduz1=T;6d2#64i>MS)`g0QfS$gWkHB|Q!NuV2l zO91U$Ac%^~j7+vw4?y#g+um53eL(+}nc|qZoIxFBSVyc4tD8Rg#@i#*YEb>6>$tE_ zbNHQ$eNjXCx*V3xLEdMz=6f?QX@>5kx(*fhRN6MpzTnn6+L5?gucA2R;iuV(4&Buw zlia(9?q#bP9!}gylhlO6IVTP<`>@c&mKu_j13%Ni&)dcdd-ZG9ypqoKN^aiucp7|x zL|0f`e5Me51039H?;lKAZ`@dwPn#{XEOLtE@o)?=7?ZDi-K?|`mZ`P;yXBA0HpfYV z5dTh)elTS_!;yhHqd*>uM9o*&cTd8vqn3-xifA*d20ibsFT4LG)`J=odYQv%O7G=p z4u2&DDB*(%>}v1bT#em4MuA>JGN6WTGZRh!f&I%s=NAS(O-q@iunB2?fNRKLG^vE& z8%`GKh=;K0x<+-&HGkY&S{UKyGzqqCc_3g>Nuv-ut`pkx8Q7-G z-xK+xY%i}vb8s1=j5bj_Fs>v=)DeP7+@hpaCC8P+b&ewgZq=*19%JfKMmr`lk`Qi> z^G3Y)lBs-hN!xY&LE1-I!?pT#%G!rmd6QICW5 z<9lOa;g7k#JN zSuJZ_9;}SKy>Kh0d$hv31OH{qyFH3^I>Nr)6_2P0+utQMAQKz#yru-$!!nDWrqxz@ z-zZL-zC%fQe=Bw#QL=GdWjvCyX&X3k#?$@r(2Wm?IcomNCfW=|p+u+&WJvvBZ?mJ- z(vzdcI@@drjVx&x`_#P zi`b_!-Sy!^YDl6y{mqTv0B_L|_=xqxexh93sYb1fxGfLymeu{F6OpsO1xI*wod0V$ zo%;wVqpt?lJ;^WXw3;nFru?C>9A;|y>~+1BmL(&pnYxnvSpGnl$Ll&e)QA_I9LR@~ z8lt1f9hfB5MCGJLDkiP^Fvw`<6Gi9a*yYR)C6Ohlh&@xjC&kxeAJ(KwC!{qm}p_3syYaQIN@1ISz|KXq##Ms7lczFSzv0aVY z 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 RteNc?=-#_sdGqn}{|lOu8_ECx literal 0 HcmV?d00001 diff --git a/contrib/webhook-teams-service/host.json b/contrib/webhook-teams-service/host.json new file mode 100644 index 000000000..05291ed43 --- /dev/null +++ b/contrib/webhook-teams-service/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[1.*, 2.0.0)" + } +} \ No newline at end of file diff --git a/contrib/webhook-teams-service/local.settings.json b/contrib/webhook-teams-service/local.settings.json new file mode 100644 index 000000000..daacb2832 --- /dev/null +++ b/contrib/webhook-teams-service/local.settings.json @@ -0,0 +1,6 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "python" + } +} diff --git a/contrib/webhook-teams-service/mypy.ini b/contrib/webhook-teams-service/mypy.ini new file mode 100644 index 000000000..b4a77a9d8 --- /dev/null +++ b/contrib/webhook-teams-service/mypy.ini @@ -0,0 +1,14 @@ +[mypy] +disallow_untyped_defs = True +follow_imports = silent +check_untyped_defs = True +disallow_any_generics = True +no_implicit_reexport = True +strict_optional = True +warn_redundant_casts = True +warn_return_any = True +warn_unused_configs = True +warn_unused_ignores = True + +[mypy-azure.*] +ignore_missing_imports = True diff --git a/contrib/webhook-teams-service/requirements.txt b/contrib/webhook-teams-service/requirements.txt new file mode 100644 index 000000000..4309123a6 --- /dev/null +++ b/contrib/webhook-teams-service/requirements.txt @@ -0,0 +1,4 @@ +# Do not include azure-functions-worker as it may conflict with the Azure Functions platform + +azure-functions +aiohttp \ No newline at end of file diff --git a/contrib/webhook-teams-service/webhook/__init__.py b/contrib/webhook-teams-service/webhook/__init__.py new file mode 100644 index 000000000..0dced4afe --- /dev/null +++ b/contrib/webhook-teams-service/webhook/__init__.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# + + +import hmac +import json +import logging +import os +from hashlib import sha512 +from typing import Any, Dict + +import aiohttp +import azure.functions as func + + +def code_block(data: str) -> str: + data = data.replace("`", "``") + return "\n```\n%s\n```\n" % data + + +async def send_message(req: func.HttpRequest) -> bool: + data = req.get_json() + teams_url = os.environ.get("TEAMS_URL") + if teams_url is None: + raise Exception("missing TEAMS_URL") + + message: Dict[str, Any] = { + "@type": "MessageCard", + "@context": "https://schema.org/extensions", + "summary": data["instance_name"], + "sections": [ + { + "facts": [ + {"name": "instance", "value": data["instance_name"]}, + {"name": "event type", "value": data["event_type"]}, + ] + }, + {"text": code_block(json.dumps(data["event"], sort_keys=True))}, + ], + } + async with aiohttp.ClientSession() as client: + async with client.post(teams_url, json=message) as response: + return response.ok + + +def verify(req: func.HttpRequest) -> bool: + request_hmac = req.headers.get("X-Onefuzz-Digest") + if request_hmac is None: + raise Exception("missing X-Onefuzz-Digest") + + hmac_token = os.environ.get("HMAC_TOKEN") + if hmac_token is None: + raise Exception("missing HMAC_TOKEN") + + digest = hmac.new( + hmac_token.encode(), msg=req.get_body(), digestmod=sha512 + ).hexdigest() + if digest != request_hmac: + logging.error("invalid hmac") + return False + + return True + + +async def main(req: func.HttpRequest) -> func.HttpResponse: + if not verify(req): + return func.HttpResponse("no thanks") + + if await send_message(req): + return func.HttpResponse("unable to send message") + + return func.HttpResponse("thanks") diff --git a/contrib/webhook-teams-service/webhook/function.json b/contrib/webhook-teams-service/webhook/function.json new file mode 100644 index 000000000..ad312dfe1 --- /dev/null +++ b/contrib/webhook-teams-service/webhook/function.json @@ -0,0 +1,19 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} \ No newline at end of file