1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

Add Microsoft Teams webhook support

This commit is contained in:
checktheroads
2020-06-26 12:23:11 -07:00
parent 04ff7568eb
commit 2af58d092c
8 changed files with 136 additions and 38 deletions

View File

@@ -57,10 +57,11 @@ If http logging is enabled, an HTTP POST will be sent to the configured target e
### Supported Providers
| Provider | Parameter Value |
| :-------------------------- | --------------: |
| [Slack](https://slack.com/) | `'slack'` |
| Generic | `'generic'` |
| Provider | Parameter Value |
| :--------------------------------------------------------------------------------------------------- | --------------: |
| Generic | `'generic'` |
| [Microsoft Teams](https://www.microsoft.com/en-us/microsoft-365/microsoft-teams/group-chat-software) | `'msteams'` |
| [Slack](https://slack.com/) | `'slack'` |
### Authentication
@@ -87,10 +88,7 @@ If the `provider` field is set to `'generic'`, the webhook will POST JSON data i
"query_vrf": "default",
"query_target": "1.1.1.0/24",
"headers": {
"content-length": "103",
"accept": "application/json, text/plain, */*",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36",
"content-type": "application/json;charset=UTF-8",
"referer": "http://lg.example.com/",
"accept-encoding": "gzip, deflate, br",
"accept-language": "en-US,en;q=0.9,fr;q=0.8,lb;q=0.7,la;q=0.6"

View File

@@ -126,14 +126,25 @@ class Query(BaseModel):
"""Get this query's device object by query_location."""
return getattr(devices, self.query_location)
def export_dict(self):
def export_dict(self, pretty=False):
"""Create dictionary representation of instance."""
return {
"query_location": self.query_location,
"query_type": self.query_type,
"query_vrf": self.query_vrf.name,
"query_target": str(self.query_target),
}
if pretty:
loc = getattr(devices, self.query_location)
query_type = getattr(params.queries, self.query_type)
items = {
"query_location": loc.display_name,
"query_type": query_type.display_name,
"query_vrf": self.query_vrf.display_name,
"query_target": str(self.query_target),
}
else:
items = {
"query_location": self.query_location,
"query_type": self.query_type,
"query_vrf": self.query_vrf.name,
"query_target": str(self.query_target),
}
return items
def export_json(self):
"""Create JSON representation of instance."""

View File

@@ -4,6 +4,7 @@
import os
import json
import time
from datetime import datetime
# Third Party
from fastapi import HTTPException, BackgroundTasks
@@ -25,7 +26,7 @@ from hyperglass.api.models.cert_import import EncodedRequest
APP_PATH = os.environ["hyperglass_directory"]
async def send_webhook(query_data: Query, request: Request) -> int:
async def send_webhook(query_data: Query, request: Request, timestamp: datetime) -> int:
"""If webhooks are enabled, get request info and send a webhook.
Args:
@@ -47,10 +48,11 @@ async def send_webhook(query_data: Query, request: Request) -> int:
async with Webhook(params.logging.http) as hook:
await hook.send(
query={
**query_data.export_dict(),
**query_data.export_dict(pretty=True),
"headers": headers,
"source": request.client.host,
"network": network_info,
"timestamp": timestamp,
}
)
@@ -58,7 +60,8 @@ async def send_webhook(query_data: Query, request: Request) -> int:
async def query(query_data: Query, request: Request, background_tasks: BackgroundTasks):
"""Ingest request data pass it to the backend application to perform the query."""
background_tasks.add_task(send_webhook, query_data, request)
timestamp = datetime.utcnow()
background_tasks.add_task(send_webhook, query_data, request, timestamp)
# Initialize cache
cache = Cache(db=params.cache.database, **REDIS_CONFIG)

View File

@@ -53,7 +53,7 @@ class Http(HyperglassModelExtra):
"""HTTP logging parameters."""
enable: StrictBool = True
provider: constr(regex=r"(slack|generic)") = "generic"
provider: constr(regex=r"(msteams|slack|generic)") = "generic"
host: AnyHttpUrl
authentication: Optional[HttpAuth]
headers: Dict[StrictStr, Union[StrictStr, StrictInt, StrictBool, None]] = {}

26
hyperglass/external/msteams.py vendored Normal file
View File

@@ -0,0 +1,26 @@
"""Session handler for Microsoft Teams API."""
# Project
from hyperglass.log import log
from hyperglass.models import Webhook
from hyperglass.external._base import BaseExternal
class MSTeams(BaseExternal, name="MSTeams"):
"""Microsoft Teams session handler."""
def __init__(self, config):
"""Initialize external base class with Microsoft Teams connection details."""
super().__init__(
base_url="https://outlook.office.com", config=config, parse=False
)
async def send(self, query):
"""Send an incoming webhook to Microsoft Teams."""
payload = Webhook(**query)
log.debug("Sending query data to Microsoft Teams:\n{}", payload)
return await self._apost(endpoint=self.config.host.path, data=payload.msteams())

View File

@@ -5,10 +5,12 @@ from hyperglass.exceptions import HyperglassError
from hyperglass.external._base import BaseExternal
from hyperglass.external.slack import SlackHook
from hyperglass.external.generic import GenericHook
from hyperglass.external.msteams import MSTeams
PROVIDER_MAP = {
"slack": SlackHook,
"generic": GenericHook,
"msteams": MSTeams,
"slack": SlackHook,
}

View File

@@ -3,6 +3,7 @@
# Standard Library
import re
from typing import TypeVar, Optional
from datetime import datetime
# Third Party
from pydantic import HttpUrl, BaseModel, StrictInt, StrictStr, StrictFloat
@@ -13,6 +14,9 @@ from hyperglass.util import clean_name
IntFloat = TypeVar("IntFloat", StrictInt, StrictFloat)
_WEBHOOK_TITLE = "hyperglass received a valid query with the following data"
_ICON_URL = "https://res.cloudinary.com/hyperglass/image/upload/v1593192484/icon.png"
class HyperglassModel(BaseModel):
"""Base model for all hyperglass configuration models."""
@@ -160,10 +164,7 @@ class StrictBytes(bytes):
class WebhookHeaders(HyperglassModel):
"""Webhook data model."""
content_length: Optional[StrictStr]
accept: Optional[StrictStr]
user_agent: Optional[StrictStr]
content_type: Optional[StrictStr]
referer: Optional[StrictStr]
accept_encoding: Optional[StrictStr]
accept_language: Optional[StrictStr]
@@ -174,9 +175,7 @@ class WebhookHeaders(HyperglassModel):
"""Pydantic model config."""
fields = {
"content_length": "content-length",
"user_agent": "user-agent",
"content_type": "content-type",
"accept_encoding": "accept-encoding",
"accept_language": "accept-language",
"x_real_ip": "x-real-ip",
@@ -201,6 +200,66 @@ class Webhook(HyperglassModel):
headers: WebhookHeaders
source: StrictStr
network: WebhookNetwork
timestamp: datetime
def msteams(self):
"""Format the webhook data as a Microsoft Teams card."""
def code(value):
"""Wrap argument in backticks for markdown inline code formatting."""
return f"`{str(value)}`"
try:
header_data = [
{"name": k, "value": code(v)}
for k, v in self.headers.dict(by_alias=True).items()
]
time_fmt = self.timestamp.strftime("%Y %m %d %H:%M:%S")
payload = {
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": "118ab2",
"summary": _WEBHOOK_TITLE,
"sections": [
{
"activityTitle": _WEBHOOK_TITLE,
"activitySubtitle": f"{time_fmt} UTC",
"activityImage": _ICON_URL,
"facts": [
{"name": "Query Location", "value": self.query_location},
{"name": "Query Target", "value": code(self.query_target)},
{"name": "Query Type", "value": self.query_type},
{"name": "Query VRF", "value": self.query_vrf},
],
},
{"markdown": True, "text": "**Source Information**"},
{"markdown": True, "text": "---"},
{
"markdown": True,
"facts": [
{"name": "Source IP", "value": code(self.source)},
{
"name": "Source Prefix",
"value": code(self.network.prefix),
},
{"name": "Source ASN", "value": code(self.network.asn)},
],
},
{"markdown": True, "text": "**Request Headers**"},
{"markdown": True, "text": "---"},
{"markdown": True, "facts": header_data},
],
}
log.debug("Created MS Teams webhook: {}", str(payload))
except Exception as err:
log.error("Error while creating webhook: {}", str(err))
payload = {}
return payload
def slack(self):
"""Format the webhook data as a Slack message."""
@@ -216,16 +275,18 @@ class Webhook(HyperglassModel):
field = make_field(k, v, code=True)
header_data.append(field)
query_details = (
("Query Location", self.query_location),
("Query Type", self.query_type),
("Query VRF", self.query_vrf),
("Query Target", self.query_target),
)
query_data = []
for k, v in query_details:
field = make_field(k, v)
query_data.append({"type": "mrkdwn", "text": field})
query_data = [
{
"type": "mrkdwn",
"text": make_field("Query Location", self.query_location),
},
{
"type": "mrkdwn",
"text": make_field("Query Target", self.query_target, code=True),
},
{"type": "mrkdwn", "text": make_field("Query Type", self.query_type)},
{"type": "mrkdwn", "text": make_field("Query VRF", self.query_vrf)},
]
source_details = (
("Source IP", self.source),
@@ -239,7 +300,7 @@ class Webhook(HyperglassModel):
source_data.append({"type": "mrkdwn", "text": field})
payload = {
"text": "hyperglass received a valid query with the following data",
"text": _WEBHOOK_TITLE,
"blocks": [
{"type": "section", "fields": query_data},
{"type": "divider"},

View File

@@ -824,10 +824,7 @@ async def process_headers(headers):
"""Filter out unwanted headers and return as a dictionary."""
headers = dict(headers)
header_keys = (
"content-length",
"accept",
"user-agent",
"content-type",
"referer",
"accept-encoding",
"accept-language",