Initial implementation

This commit is contained in:
Jasmine Schladen
2020-10-16 10:40:11 -07:00
parent ea915282b2
commit a04cce6044
13 changed files with 330 additions and 104 deletions

View File

@ -20,6 +20,15 @@ class NotificationPlugin(Plugin):
def send(self, notification_type, message, targets, options, **kwargs):
raise NotImplementedError
def filter_recipients(self, options, excluded_recipients):
"""
Given a set of options (which should include configured recipient info), filters out recipients that
we do NOT want to notify.
For any notification types where recipients can't be dynamically modified, this returns an empty list.
"""
return []
class ExpirationNotificationPlugin(NotificationPlugin):
"""
@ -50,5 +59,5 @@ class ExpirationNotificationPlugin(NotificationPlugin):
def options(self):
return self.default_options + self.additional_options
def send(self, notification_type, message, targets, options, **kwargs):
def send(self, notification_type, message, excluded_targets, options, **kwargs):
raise NotImplementedError

View File

@ -32,13 +32,14 @@
.. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
.. moduleauthor:: Harm Weites <harm@weites.com>
"""
from acme.errors import ClientError
from flask import current_app
from lemur.extensions import sentry, metrics
from lemur.plugins import lemur_aws as aws
from lemur.extensions import sentry, metrics
from lemur.plugins import lemur_aws as aws, ExpirationNotificationPlugin
from lemur.plugins.bases import DestinationPlugin, ExportDestinationPlugin, SourcePlugin
from lemur.plugins.lemur_aws import iam, s3, elb, ec2
from lemur.plugins.lemur_aws import iam, s3, elb, ec2, sns
def get_region_from_dns(dns):
@ -406,3 +407,55 @@ class S3DestinationPlugin(ExportDestinationPlugin):
self.get_option("encrypt", options),
account_number=self.get_option("accountNumber", options),
)
class SNSNotificationPlugin(ExpirationNotificationPlugin):
title = "AWS SNS"
slug = "aws-sns"
description = "Sends notifications to AWS SNS"
version = aws.VERSION
author = "Jasmine Schladen <jschladen@netflix.com>"
author_url = "https://github.com/Netflix/lemur"
additional_options = [
{
"name": "accountNumber",
"type": "str",
"required": True,
"validation": "[0-9]{12}",
"helpMessage": "A valid AWS account number with permission to access the SNS topic",
},
{
"name": "region",
"type": "str",
"required": True,
"validation": "[0-9a-z\\-]{1,25}",
"helpMessage": "Region in which the SNS topic is located, e.g. \"us-east-1\"",
},
{
"name": "Topic Name",
"type": "str",
"required": True,
# base topic name is 1-256 characters (alphanumeric plus underscore and hyphen)
"validation": "^[a-zA-Z0-9_\\-]{1,256}$",
"helpMessage": "The name of the topic to use for expiration notifications",
}
]
def send(self, notification_type, message, excluded_targets, options, **kwargs):
"""
While we receive a `targets` parameter here, it is unused, as the SNS topic is pre-configured in the
plugin configuration, and can't reasonably be changed dynamically.
"""
topic_arn = "arn:aws:sns:{0}:{1}:{2}".format(self.get_option("region", options),
self.get_option("accountNumber", options),
self.get_option("Topic Name", options))
current_app.logger.debug("Publishing {0} notification to topic {1}".format(notification_type, topic_arn))
print("ALPACA: Trying to send {0} SNS notification to topic {1}".format(notification_type, topic_arn))
try:
sns.publish(topic_arn, message, notification_type, region_name=self.get_option("region", options))
except Exception:
current_app.logger.exception("Error publishing {0} notification to topic {1}".format(notification_type, topic_arn))

View File

@ -0,0 +1,56 @@
"""
.. module: lemur.plugins.lemur_aws.sts
:platform: Unix
:copyright: (c) 2020 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import json
import arrow
import boto3
from flask import current_app
def publish(topic_arn, certificates, notification_type, **kwargs):
sns_client = boto3.client("sns", **kwargs)
print("ALPACA: SNS client: {0}, certificates: {1}".format(sns_client, certificates))
message_ids = {}
for certificate in certificates:
message_ids[certificate["name"]] = publish_single(sns_client, topic_arn, certificate, notification_type)
return message_ids
def publish_single(sns_client, topic_arn, certificate, notification_type):
response = sns_client.publish(
TopicArn=topic_arn,
Message=format_message(certificate, notification_type),
)
response_code = response["ResponseMetadata"]["HTTPStatusCode"]
if response_code != 200:
raise Exception("Failed to publish notification to SNS, response code was {}".format(response_code))
current_app.logger.debug(
"AWS SNS message published to topic [{0}]: [{1}]".format(topic_arn, response)
)
return response["MessageId"]
def create_certificate_url(name):
return "https://{hostname}/#/certificates/{name}".format(
hostname=current_app.config.get("LEMUR_HOSTNAME"), name=name
)
def format_message(certificate, notification_type):
json_message = {
"notification_type": notification_type,
"certificate_name": certificate["name"],
"expires": arrow.get(certificate["validityEnd"]).format("dddd, MMMM D, YYYY"),
"endpoints_detected": len(certificate["endpoints"]),
"details": create_certificate_url(certificate["name"])
}
return json.dumps(json_message)

View File

@ -0,0 +1,50 @@
from moto import mock_sts, mock_sns, mock_sqs
import boto3
import json
import arrow
from lemur.plugins.lemur_aws.sns import format_message
from lemur.plugins.lemur_aws.sns import publish
from lemur.certificates.schemas import certificate_notification_output_schema
@mock_sns()
def test_format(certificate, endpoint):
data = [certificate_notification_output_schema.dump(certificate).data]
for certificate in data:
expected_message = {
"notification_type": "expiration",
"certificate_name": certificate["name"],
"expires": arrow.get(certificate["validityEnd"]).format("dddd, MMMM D, YYYY"),
"endpoints_detected": 0,
"details": "https://lemur.example.com/#/certificates/{name}".format(name=certificate["name"])
}
assert expected_message == json.loads(format_message(certificate, "expiration"))
@mock_sns()
@mock_sqs()
def test_publish(certificate, endpoint):
data = [certificate_notification_output_schema.dump(certificate).data]
sns_client = boto3.client("sns", region_name="us-east-1")
topic_arn = sns_client.create_topic(Name='lemursnstest')["TopicArn"]
sqs_client = boto3.client("sqs", region_name="us-east-1")
queue = sqs_client.create_queue(QueueName="lemursnstestqueue")
queue_url = queue["QueueUrl"]
queue_arn = sqs_client.get_queue_attributes(QueueUrl=queue_url)["Attributes"]["QueueArn"]
sns_client.subscribe(TopicArn=topic_arn, Protocol="sqs", Endpoint=queue_arn)
message_ids = publish(topic_arn, data, "expiration", region_name="us-east-1")
assert len(message_ids) == len(data)
received_messages = sqs_client.receive_message(QueueUrl=queue_url)["Messages"]
print("ALPACA: Received messages = {}".format(received_messages))
for certificate in data:
expected_message_id = message_ids[certificate["name"]]
actual_message = next((m for m in received_messages if json.loads(m["Body"])["MessageId"] == expected_message_id), None)
assert json.loads(actual_message["Body"])["Message"] == format_message(certificate, "expiration")

View File

@ -17,6 +17,7 @@ from lemur.plugins.bases import ExpirationNotificationPlugin
from lemur.plugins import lemur_email as email
from lemur.plugins.lemur_email.templates.config import env
from lemur.plugins.utils import get_plugin_option
def render_html(template_name, message):
@ -105,8 +106,23 @@ class EmailNotificationPlugin(ExpirationNotificationPlugin):
s_type = current_app.config.get("LEMUR_EMAIL_SENDER", "ses").lower()
if s_type == "ses":
send_via_ses(subject, body, targets)
current_app.logger.info("ALPACA: Would send an email to {0} with subject \"{1}\" here".format(targets, subject))
elif s_type == "smtp":
send_via_smtp(subject, body, targets)
# if s_type == "ses":
# send_via_ses(subject, body, targets)
#
# elif s_type == "smtp":
# send_via_smtp(subject, body, targets)
@staticmethod
def filter_recipients(options, excluded_recipients):
print("ALPACA: Getting recipients for notification {0}".format(options))
notification_recipients = get_plugin_option("recipients", options)
print(
"ALPACA: Sending certificate notifications to recipients {0}".format(notification_recipients.split(",")))
if notification_recipients:
notification_recipients = notification_recipients.split(",")
# removing owner and security_email from notification_recipient
notification_recipients = [i for i in notification_recipients if i not in excluded_recipients]
return notification_recipients

View File

@ -34,3 +34,15 @@ def test_render(certificate, endpoint):
hostname="lemur.test.example.com",
)
)
def test_filter_recipients(certificate, endpoint):
from lemur.plugins.lemur_email.plugin import EmailNotificationPlugin
options = [{"name": "recipients", "value": "security@netflix.com,bob@netflix.com,joe@netflix.com"}]
assert EmailNotificationPlugin.filter_recipients(options, []) == ["security@netflix.com", "bob@netflix.com",
"joe@netflix.com"]
assert EmailNotificationPlugin.filter_recipients(options, ["security@netflix.com"]) == ["bob@netflix.com",
"joe@netflix.com"]
assert EmailNotificationPlugin.filter_recipients(options, ["security@netflix.com", "bob@netflix.com",
"joe@netflix.com"]) == []

View File

@ -119,6 +119,9 @@ class SlackNotificationPlugin(ExpirationNotificationPlugin):
"""
A typical check can be performed using the notify command:
`lemur notify`
While we receive a `targets` parameter here, it is unused, as Slack webhooks do not allow
dynamic re-targeting of messages. The webhook itself specifies a channel.
"""
attachments = None
if notification_type == "expiration":
@ -142,6 +145,6 @@ class SlackNotificationPlugin(ExpirationNotificationPlugin):
if r.status_code not in [200]:
raise Exception("Failed to send message")
current_app.logger.error(
current_app.logger.info(
"Slack response: {0} Message Body: {1}".format(r.status_code, body)
)