Skip to content
This repository has been archived by the owner on Nov 1, 2023. It is now read-only.

Commit

Permalink
sample webhook service (#666)
Browse files Browse the repository at this point in the history
  • Loading branch information
bmc-msft authored Mar 11, 2021
1 parent 0378455 commit 09c6f92
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 0 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
43 changes: 43 additions & 0 deletions contrib/webhook-teams-service/.gitignore
Original file line number Diff line number Diff line change
@@ -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
85 changes: 85 additions & 0 deletions contrib/webhook-teams-service/README.md
Original file line number Diff line number Diff line change
@@ -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 <RESOURCE_GROUP> --name <FUNCTION_APP_NAME> --set httpsOnly=true
```
4. Deploy your function
```bash
func azure functionapp publish <FUNCTION_APP_NAME> --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://<FUNCTION_APP_NAME>.azurewebsites.net/api/webhook?code=<BASE64_ENCODED_STRING>
```
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://<FUNCTION_APP_NAME>.azurewebsites.net/api/webhook?code=<BASE64_ENCODED_STRING>" job_created job_stopped --secret_token <HMAC_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 <FUNCTION_APP_NAME> --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 <HMAC_TOKEN> <webhook_id>
```
Binary file added contrib/webhook-teams-service/example-message.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions contrib/webhook-teams-service/host.json
Original file line number Diff line number Diff line change
@@ -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)"
}
}
6 changes: 6 additions & 0 deletions contrib/webhook-teams-service/local.settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "python"
}
}
14 changes: 14 additions & 0 deletions contrib/webhook-teams-service/mypy.ini
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions contrib/webhook-teams-service/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Do not include azure-functions-worker as it may conflict with the Azure Functions platform

azure-functions
aiohttp
75 changes: 75 additions & 0 deletions contrib/webhook-teams-service/webhook/__init__.py
Original file line number Diff line number Diff line change
@@ -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")
19 changes: 19 additions & 0 deletions contrib/webhook-teams-service/webhook/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}

0 comments on commit 09c6f92

Please sign in to comment.