Skip to content

Commit

Permalink
webhook sink (#192)
Browse files Browse the repository at this point in the history
* webhook sink
  • Loading branch information
arikalon1 authored Feb 3, 2022
1 parent c9a1914 commit 61a2001
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 15 deletions.
9 changes: 9 additions & 0 deletions docs/catalog/sinks/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The following sinks are supported:
* Datadog - send playbooks results to the Datadog events API
* OpsGenie - send playbooks results to the OpsGenie alerts API
* :ref:`Telegram` - send playbooks results to Telegram group or private conversation
* Webhook - send playbooks results to a webhook

**Need support for something not listed here?** `Tell us and we'll add it to the code. <https://github.com/robusta-dev/robusta/issues/new?assignees=&labels=&template=feature_request.md&title=New%20Sink:>`_

Expand Down Expand Up @@ -69,6 +70,14 @@ The output from ``resource_babysitter`` looks like this in the different sinks:
:width: 600
:align: center

**Webhook:**

.. admonition:: This example is sending Robusta notifications to ntfy.sh, push notification service

.. image:: /images/deployment-babysitter-webhook.png
:width: 600
:align: center

Default sinks
-------------
If a playbook doesn't specify a sink then output will be sent to the default sinks. A sink is considered default
Expand Down
Binary file added docs/images/deployment-babysitter-webhook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion docs/user-guide/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,16 @@ Here is a full example showing how to configure all possible sinks:
api_key: "datadog api key"
default: false
- opsgenie_sink:
name: ops_genie_ui_sink
name: ops_genie_sink
api_key: OpsGenie integration API key # configured from OpsGenie team integration
teams:
- "noc"
- "sre"
tags:
- "prod a"
- webhook_sink:
name: webhook_sink
url: "https://my-webhook-service.com/robusta-alerts"
Configuration secrets
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
2 changes: 2 additions & 0 deletions src/robusta/core/model/runner_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pydantic import BaseModel, SecretStr, validator

from ..playbooks.playbook_utils import replace_env_vars_values
from ..sinks.webhook.webhook_sink_params import WebhookSinkConfigWrapper
from ..sinks.telegram.telegram_sink_params import TelegramSinkConfigWrapper
from ...model.playbook_definition import PlaybookDefinition
from ..sinks.datadog.datadog_sink_params import DataDogSinkConfigWrapper
Expand Down Expand Up @@ -32,6 +33,7 @@ class RunnerConfig(BaseModel):
MsTeamsSinkConfigWrapper,
OpsGenieSinkConfigWrapper,
TelegramSinkConfigWrapper,
WebhookSinkConfigWrapper,
]
]
]
Expand Down
4 changes: 4 additions & 0 deletions src/robusta/core/sinks/sink_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from .opsgenie.opsgenie_sink_params import OpsGenieSinkConfigWrapper
from .telegram.telegram_sink import TelegramSink
from .telegram.telegram_sink_params import TelegramSinkConfigWrapper
from .webhook.webhook_sink import WebhookSink
from .webhook.webhook_sink_params import WebhookSinkConfigWrapper


class SinkFactory:
Expand All @@ -35,5 +37,7 @@ def create_sink(
return OpsGenieSink(sink_config, cluster_name)
elif isinstance(sink_config, TelegramSinkConfigWrapper):
return TelegramSink(sink_config, cluster_name)
elif isinstance(sink_config, WebhookSinkConfigWrapper):
return WebhookSink(sink_config, cluster_name)
else:
raise Exception(f"Sink not supported {type(sink_config)}")
35 changes: 21 additions & 14 deletions src/robusta/core/sinks/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,33 @@


class Transformer:

@staticmethod
def to_github_markdown(markdown_data: str, add_angular_brackets: bool = True) -> str:
"""Transform all occurrences of slack markdown, <URL|LINK TEXT>, to github markdown [LINK TEXT](URL)."""
def get_markdown_links(markdown_data: str) -> List[str]:
regex = "<.*?\\|.*?>"
matches = re.findall(regex, markdown_data)
# some markdown parsers doesn't support angular brackets on links
OPENING_ANGULAR = "<" if add_angular_brackets else ""
CLOSING_ANGULAR = ">" if add_angular_brackets else ""
links = []
if matches:
matches = [
links = [
match for match in matches if len(match) > 1
] # filter out illegal matches
for match in matches:
# take only the data between the first '<' and last '>'
splits = match[1:-1].split("|")
if len(splits) == 2: # don't replace unexpected strings
parsed_url = urllib.parse.urlparse(splits[0])
parsed_url = parsed_url._replace(path=urllib.parse.quote_plus(parsed_url.path, safe="/"))
replacement = f"[{splits[1]}]({OPENING_ANGULAR}{parsed_url.geturl()}{CLOSING_ANGULAR})"
markdown_data = markdown_data.replace(match, replacement)
return links

@staticmethod
def to_github_markdown(markdown_data: str, add_angular_brackets: bool = True) -> str:
"""Transform all occurrences of slack markdown, <URL|LINK TEXT>, to github markdown [LINK TEXT](URL)."""
# some markdown parsers doesn't support angular brackets on links
OPENING_ANGULAR = "<" if add_angular_brackets else ""
CLOSING_ANGULAR = ">" if add_angular_brackets else ""
matches = Transformer.get_markdown_links(markdown_data)
for match in matches:
# take only the data between the first '<' and last '>'
splits = match[1:-1].split("|")
if len(splits) == 2: # don't replace unexpected strings
parsed_url = urllib.parse.urlparse(splits[0])
parsed_url = parsed_url._replace(path=urllib.parse.quote_plus(parsed_url.path, safe="/"))
replacement = f"[{splits[1]}]({OPENING_ANGULAR}{parsed_url.geturl()}{CLOSING_ANGULAR})"
markdown_data = markdown_data.replace(match, replacement)
return re.sub(r"\*([^\*]*)\*", r"**\1**", markdown_data)

@classmethod
Expand Down
Empty file.
71 changes: 71 additions & 0 deletions src/robusta/core/sinks/webhook/webhook_sink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import textwrap

import requests
from typing import List

from .webhook_sink_params import WebhookSinkConfigWrapper
from ..transformer import Transformer
from ...reporting import HeaderBlock, ListBlock, JsonBlock, KubernetesDiffBlock, MarkdownBlock
from ...reporting.base import Finding, BaseBlock
from ..sink_base import SinkBase


class WebhookSink(SinkBase):
def __init__(self, sink_config: WebhookSinkConfigWrapper, cluster_name: str):
super().__init__(sink_config.webhook_sink)
self.cluster_name = cluster_name
self.url = sink_config.webhook_sink.url
self.size_limit = sink_config.webhook_sink.size_limit

def write_finding(self, finding: Finding, platform_enabled: bool):
message_lines: List[str] = [finding.title]
if platform_enabled:
message_lines.append(f"Investigate: {finding.investigate_uri}")
message_lines.append(f"Source: {self.cluster_name}")
message_lines.append(finding.description)

message = ""

for enrichment in finding.enrichments:
for block in enrichment.blocks:
message_lines.extend(self.__to_unformatted_text(block))

for line in [line for line in message_lines if line]:
wrapped = textwrap.dedent(
f"""
{line}
"""
)
if len(message) + len(wrapped) >= self.size_limit:
break
message += wrapped

requests.post(self.url, data=message)

@classmethod
def __to_clear_text(cls, markdown_text: str) -> str:
# just create a readable links format
links = Transformer.get_markdown_links(markdown_text)
for link in links:
# take only the data between the first '<' and last '>'
splits = link[1:-1].split("|")
if len(splits) == 2: # don't replace unexpected strings
replacement = f"{splits[1]}: {splits[0]}"
markdown_text = markdown_text.replace(link, replacement)

return markdown_text

def __to_unformatted_text(cls, block: BaseBlock) -> List[str]:
lines = []
if isinstance(block, HeaderBlock):
lines.append(block.text)
elif isinstance(block, ListBlock):
lines.extend([cls.__to_clear_text(item) for item in block.items])
elif isinstance(block, MarkdownBlock):
lines.append(cls.__to_clear_text(block.text))
elif isinstance(block, JsonBlock):
lines.append(block.json_str)
elif isinstance(block, KubernetesDiffBlock):
for diff in block.diffs:
lines.append(f"*{'.'.join(diff.path)}*: {diff.other_value} ==> {diff.value}")
return lines
15 changes: 15 additions & 0 deletions src/robusta/core/sinks/webhook/webhook_sink_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from ..sink_config import SinkConfigBase
from ..sink_base_params import SinkBaseParams


class WebhookSinkParams(SinkBaseParams):
url: str
size_limit: int = 4096


class WebhookSinkConfigWrapper(SinkConfigBase):
webhook_sink: WebhookSinkParams

def get_params(self) -> SinkBaseParams:
return self.webhook_sink

0 comments on commit 61a2001

Please sign in to comment.