mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-21 13:51:19 +00:00
sample webhook service (#666)
This commit is contained in:
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@ -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:
|
||||
|
43
contrib/webhook-teams-service/.gitignore
vendored
Normal file
43
contrib/webhook-teams-service/.gitignore
vendored
Normal file
@ -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
contrib/webhook-teams-service/README.md
Normal file
85
contrib/webhook-teams-service/README.md
Normal file
@ -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:
|
||||

|
||||
|
||||
## 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>
|
||||
```
|
BIN
contrib/webhook-teams-service/example-message.png
Executable file
BIN
contrib/webhook-teams-service/example-message.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
15
contrib/webhook-teams-service/host.json
Normal file
15
contrib/webhook-teams-service/host.json
Normal file
@ -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
contrib/webhook-teams-service/local.settings.json
Normal file
6
contrib/webhook-teams-service/local.settings.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"FUNCTIONS_WORKER_RUNTIME": "python"
|
||||
}
|
||||
}
|
14
contrib/webhook-teams-service/mypy.ini
Normal file
14
contrib/webhook-teams-service/mypy.ini
Normal file
@ -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
contrib/webhook-teams-service/requirements.txt
Normal file
4
contrib/webhook-teams-service/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
# Do not include azure-functions-worker as it may conflict with the Azure Functions platform
|
||||
|
||||
azure-functions
|
||||
aiohttp
|
75
contrib/webhook-teams-service/webhook/__init__.py
Normal file
75
contrib/webhook-teams-service/webhook/__init__.py
Normal file
@ -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
contrib/webhook-teams-service/webhook/function.json
Normal file
19
contrib/webhook-teams-service/webhook/function.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"scriptFile": "__init__.py",
|
||||
"bindings": [
|
||||
{
|
||||
"authLevel": "function",
|
||||
"type": "httpTrigger",
|
||||
"direction": "in",
|
||||
"name": "req",
|
||||
"methods": [
|
||||
"post"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "http",
|
||||
"direction": "out",
|
||||
"name": "$return"
|
||||
}
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user