mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-21 21:54:26 +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
|
# set a minimum confidence to ignore known false positives
|
||||||
vulture --min-confidence 61 onefuzz
|
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:
|
deploy-onefuzz-via-azure-devops:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-18.04
|
||||||
steps:
|
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