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:
@@ -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"
|
||||
|
@@ -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."""
|
||||
|
@@ -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)
|
||||
|
@@ -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
26
hyperglass/external/msteams.py
vendored
Normal 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())
|
4
hyperglass/external/webhooks.py
vendored
4
hyperglass/external/webhooks.py
vendored
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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"},
|
||||
|
@@ -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",
|
||||
|
Reference in New Issue
Block a user