Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[outputs] add Microsoft Teams as an alerting output #1079

Merged
merged 4 commits into from
Jan 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion conf/outputs.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@
],
"slack": [
"sample-channel"
],
"teams": [
"sample-webhook"
]
}
}
3 changes: 2 additions & 1 deletion docs/source/outputs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Out of the box, StreamAlert supports:
* **PagerDuty**
* **Phantom**
* **Slack**
* **Microsoft Teams**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also update service type teams to line 41 on this page?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider it done :)


StreamAlert can be extended to support any API. Creating a new output to send alerts to is easily accomplished through inheritance from the ``StreamOutputBase`` class. More on that in the `Adding Support for New Services`_ section below.

Expand All @@ -41,7 +42,7 @@ Adding a new configuration for a currently supported service is handled using ``
- ``<SERVICE_NAME>`` above should be one of the following supported service identifiers:
``aws-cloudwatch-log``, ``aws-firehose``, ``aws-lambda``, ``aws-s3``, ``aws-sns``, ``aws-sqs``,
``carbonblack``, ``github``, ``jira``, ``komand``, ``pagerduty``, ``pagerduty-incident``,
``pagerduty-v2``, ``phantom``, ``slack``
``pagerduty-v2``, ``phantom``, ``slack``, ``teams``

For example:
- ``python manage.py output slack``
Expand Down
1 change: 1 addition & 0 deletions requirements-top-level.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pathlib2
policyuniverse
pyfakefs
pylint==2.3.1
pymsteams
requests
Sphinx
sphinx-rtd-theme
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pycparser==2.19
pyflakes==2.1.1
Pygments==2.4.2
PyJWT==1.7.1
pymsteams==0.1.12
pyparsing==2.4.2
pyrsistent==0.15.5
pytest==5.0.0
Expand Down
260 changes: 260 additions & 0 deletions streamalert/alert_processor/outputs/teams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
"""
Copyright 2017-present, Airbnb Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from collections import OrderedDict

import pymsteams
from pymsteams import TeamsWebhookException

from streamalert.alert_processor.helpers import compose_alert
from streamalert.alert_processor.outputs.output_base import (
OutputDispatcher,
OutputProperty,
StreamAlertOutput,
)
from streamalert.shared.logger import get_logger

LOGGER = get_logger(__name__)


@StreamAlertOutput
class TeamsOutput(OutputDispatcher):
"""TeamsOutput handles all alert dispatching for Microsoft Teams"""

__service__ = "teams"

@classmethod
def get_user_defined_properties(cls):
"""Get properties that must be assigned by the user when configuring a new Microsoft Teams
output. This should be sensitive or unique information for this use-case that needs
to come from the user.

Every output should return a dict that contains a 'descriptor' with a description of the
integration being configured.

Microsoft Teams also requires a user provided 'webhook' url that is composed of the Team's
api url and the unique integration key for this output. This value should be should be
masked during input and is a credential requirement.

Returns:
OrderedDict: Contains various OutputProperty items
"""
return OrderedDict(
[
(
"descriptor",
OutputProperty(
description="a short and unique descriptor for this service configuration "
"(ie: name of Team the webhook relates too)"
),
),
(
"url",
OutputProperty(
description="the full teams webhook url, including the secret",
mask_input=True,
cred_requirement=True,
),
),
]
)

@classmethod
def _format_message(cls, alert, publication, webhook_url):
"""Format the message to be sent to Teams

Args:
alert (Alert): The alert
publication (dict): Alert relevant to the triggered rule
jack1902 marked this conversation as resolved.
Show resolved Hide resolved
webhook_url (str): The webhook_url to send the card too

Returns:
pymsteams.connectorcard: The message to be sent to Teams
The card will look like (by Default):
StreamAlert Rule Triggered: rule_name
Rule Description:
This will be the docstring from the rule, sent as the rule_description

Record:
key value
key value
...
"""
# Presentation defaults
default_title = "StreamAlert Rule Triggered: {}".format(alert.rule_name)
default_description = alert.rule_description
default_color = "E81123" # Red in Hexstring format
jack1902 marked this conversation as resolved.
Show resolved Hide resolved

# Special field that Publishers can use to customize the message
title = publication.get("@teams.title", default_title)
description = publication.get("@teams.description", default_description)
card_color = publication.get("@teams.card_color", default_color)
with_record = publication.get("@teams.with_record", True)

# Instantiate the card with the url
teams_card = pymsteams.connectorcard(webhook_url)

# Set the cards title, text and color
teams_card.title(title)
teams_card.text(description)
teams_card.color(card_color)

# Add the Alert Section
teams_card.addSection(cls._generate_alert_section(alert))

if with_record:
# Add the record Section
teams_card.addSection(cls._generate_record_section(alert.record))

if "@teams.additional_card_sections" in publication:
teams_card = cls._add_additional_sections(
teams_card, publication["@teams.additional_card_sections"]
)

return teams_card

@classmethod
def _generate_record_section(cls, record):
"""Generate the record section

This adds the entire record to a section as key/value pairs

Args:
record (dict): asd
Returns:
record_section (pymsteams.cardsection): record section for the outgoing card
"""
# Instantiate the card section
record_section = pymsteams.cardsection()

# Set the title
record_section.activityTitle("StreamAlert Alert Record")

# Add the record as key/value pairs
for key, value in record.items():
record_section.addFact(key, str(value))

return record_section

@classmethod
def _generate_alert_section(cls, alert):
"""Generate the alert section

Args:
alert (Alert): The alert
Returns:
alert_section (pymsteams.cardsection): alert section for the outgoing card
"""

# Instantiate the card
alert_section = pymsteams.cardsection()

# Set the title
alert_section.activityTitle("Alert Info")

# Add basic information to the alert section
alert_section.addFact("rule_name", alert.rule_name)
alert_section.addFact("alert_id", alert.alert_id)

return alert_section

@staticmethod
def _add_additional_sections(teams_card, additional_sections):
"""Add additional card sections to the teams card

Args:
teams_card (pymsteams.connectorcard): Teams connector card
additional_sections (list[pymsteams.cardsection]):
Additional sections to be added to the card. Each section should be of
type: pymsteams.cardsection and have their relevant fields filled out.
Please review the pymsteams documentation for additional information.

Returns:
teams_card (pymsteams.connectorcard): teams_card with additional sections added
"""
if not isinstance(additional_sections, list):
LOGGER.debug("additional_sections is not a list, converting")

additional_sections = [additional_sections]

for additional_section in additional_sections:
if not isinstance(additional_section, pymsteams.cardsection):
LOGGER.error(
"additional_section: %s is not an instance of %s",
additional_section,
pymsteams.cardsection,
)
continue

teams_card.addSection(additional_section)

return teams_card

def _dispatch(self, alert, descriptor):
"""Sends the Teams Card to Teams

Publishing:
By default the teams output sends a teams card comprising some default intro text
and a section containing:
* title with rule name
* alert description
* alert record (as a section of key/value pairs)

To override this behavior use the following fields:

- @teams.title (str):
Replaces the title of the teams connector card.

- @teams.description (str):
Replaces the text of the team connector card

- @teams.card_color (str):
Replaces the default color of the connector card (red)
Note: colors are represented by hex string

- @teams.with_record (bool):
Set to False, to remove the alert record section. Useful if you want to have a
more targeted approach for the alert

- @teams.additional_card_sections (list[pymsteams.cardsection]):
Pass in additional sections you want to send on the message.

@see cls._add_additional_sections() for more info

Args:
alert (Alert): Alert instance which triggered a rule
descriptor (str): Output descriptor

Returns:
bool: True if alert was sent successfully, False otherwise
"""
creds = self._load_creds(descriptor)
if not creds:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would log an error message if no creds provided.

Copy link
Contributor Author

@jack1902 jack1902 Jan 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chunyong-lin I copied the styling from:

creds = self._load_creds(descriptor)

As this returns True/False can add the log message if you want though?

LOGGER.error("No credentials found for descriptor: %s", descriptor)
return False

# Create the publication
publication = compose_alert(alert, self, descriptor)

# Format the message
teams_card = self._format_message(alert, publication, creds["url"])

try:
teams_card.send()
except TeamsWebhookException as err:
LOGGER.error("Error Sending Alert to Teams: %s", err)
return False

return True
3 changes: 2 additions & 1 deletion streamalert_cli/manage_lambda/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class LambdaPackage:
'netaddr': 'netaddr==0.7.19',
'policyuniverse': 'policyuniverse==1.3.2.1',
'requests': 'requests==2.22.0',
'pymsteams': 'pymsteams==0.1.12'
}

def __init__(self, config):
Expand Down Expand Up @@ -226,7 +227,7 @@ class AlertProcessorPackage(LambdaPackage):
'streamalert/shared'
}
package_name = 'alert_processor'
package_libs = {'cbapi', 'netaddr', 'requests'}
package_libs = {'cbapi', 'netaddr', 'pymsteams', 'requests'}


class AlertMergerPackage(LambdaPackage):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ def test_output_loading():
'pagerduty-v2',
'pagerduty-incident',
'phantom',
'slack'
'slack',
'teams'
}
assert_count_equal(loaded_outputs, expected_outputs)

Expand Down
Loading