diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7cbabd0..0064109 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.61.0 + rev: v1.62.0 hooks: - id: terraform_fmt - id: terraform_validate diff --git a/README.md b/README.md index 572adac..6d5f29a 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,11 @@ module "notify_slack" { sns_topic_name = "slack-topic" slack_webhook_url = "https://hooks.slack.com/services/AAA/BBB/CCC" - slack_channel = "aws-notification" slack_username = "reporter" } ``` -## Using with Terraform Cloud Agents +### Using with Terraform Cloud Agents [Terraform Cloud Agents](https://www.terraform.io/docs/cloud/workspaces/agent.html) are a paid feature, available as part of the Terraform Cloud for Business upgrade package. @@ -45,19 +44,15 @@ RUN apt-get -y update && apt-get -y install python3.9 python3-pip ENTRYPOINT ["/bin/tfc-agent"] ``` -## Use existing SNS topic or create new +### Local Development and Testing -If you want to subscribe the AWS Lambda Function created by this module to an existing SNS topic you should specify `create_sns_topic = false` as an argument and specify the name of existing SNS topic name in `sns_topic_name`. +See the [functions](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/tree/master/functions) for further details. ## Examples - [notify-slack-simple](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/tree/master/examples/notify-slack-simple) - Creates SNS topic which sends messages to Slack channel. - [cloudwatch-alerts-to-slack](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/tree/master/examples/cloudwatch-alerts-to-slack) - End to end example which shows how to send AWS Cloudwatch alerts to Slack channel and use KMS to encrypt webhook URL. -## Local Development and Testing - -See the [functions](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/tree/master/functions) for further details. - ## Requirements @@ -96,10 +91,11 @@ See the [functions](https://github.com/terraform-aws-modules/terraform-aws-notif | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [cloudwatch\_log\_group\_kms\_key\_id](#input\_cloudwatch\_log\_group\_kms\_key\_id) | The ARN of the KMS Key to use when encrypting log data for Lambda | `string` | `null` | no | -| [cloudwatch\_log\_group\_retention\_in\_days](#input\_cloudwatch\_log\_group\_retention\_in\_days) | Specifies the number of days you want to retain log events in log group for Lambda | `number` | `0` | no | +| [cloudwatch\_log\_group\_retention\_in\_days](#input\_cloudwatch\_log\_group\_retention\_in\_days) | Specifies the number of days you want to retain log events in log group for Lambda | `number` | `7` | no | | [cloudwatch\_log\_group\_tags](#input\_cloudwatch\_log\_group\_tags) | Additional tags for the Cloudwatch log group | `map(string)` | `{}` | no | | [create](#input\_create) | Whether to create all resources | `bool` | `true` | no | | [create\_sns\_topic](#input\_create\_sns\_topic) | Whether to create new SNS topic | `bool` | `true` | no | +| [environment\_variables](#input\_environment\_variables) | A map that defines environment variables for the Lambda Function | `map(string)` | `{}` | no | | [iam\_policy\_path](#input\_iam\_policy\_path) | Path of policies to that should be added to IAM role for Lambda Function | `string` | `null` | no | | [iam\_role\_boundary\_policy\_arn](#input\_iam\_role\_boundary\_policy\_arn) | The ARN of the policy that is used to set the permissions boundary for the role | `string` | `null` | no | | [iam\_role\_name\_prefix](#input\_iam\_role\_name\_prefix) | A unique role name beginning with the specified prefix | `string` | `"lambda"` | no | @@ -117,13 +113,10 @@ See the [functions](https://github.com/terraform-aws-modules/terraform-aws-notif | [lambda\_role](#input\_lambda\_role) | IAM role attached to the Lambda Function. If this is set then a role will not be created for you | `string` | `""` | no | | [lambda\_runtime](#input\_lambda\_runtime) | Lambda Function runtime | `string` | `"python3.9"` | no | | [lambda\_timeout](#input\_lambda\_timeout) | The amount of time your Lambda Function has to run in seconds | `number` | `30` | no | -| [log\_events](#input\_log\_events) | Boolean flag to enabled/disable logging of incoming events | `bool` | `false` | no | | [recreate\_missing\_package](#input\_recreate\_missing\_package) | Whether to recreate missing Lambda package if it is missing locally or not | `bool` | `true` | no | | [reserved\_concurrent\_executions](#input\_reserved\_concurrent\_executions) | The amount of reserved concurrent executions for this lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations | `number` | `-1` | no | -| [slack\_channel](#input\_slack\_channel) | The name of the channel in Slack for notifications | `string` | n/a | yes | -| [slack\_emoji](#input\_slack\_emoji) | A custom emoji that will appear on Slack messages | `string` | `":aws:"` | no | -| [slack\_username](#input\_slack\_username) | The username that will appear on Slack messages | `string` | n/a | yes | -| [slack\_webhook\_url](#input\_slack\_webhook\_url) | The URL of Slack webhook | `string` | n/a | yes | +| [slack\_webhook\_url\_secret\_name](#input\_slack\_webhook\_url\_secret\_name) | Name of Secrets Manager secret that contains the Slack webhook URL. If not provided, `slack_webhook_url_ssm_parameter_name` must be provided | `string` | `""` | no | +| [slack\_webhook\_url\_ssm\_parameter\_name](#input\_slack\_webhook\_url\_ssm\_parameter\_name) | Name of SSM parameter that contains the Slack webhook URL (must be type `SecureString`). If not provided, `slack_webhook_url_secret_name` must be provided | `string` | `""` | no | | [sns\_topic\_kms\_key\_id](#input\_sns\_topic\_kms\_key\_id) | ARN of the KMS key used for enabling SSE on the topic | `string` | `""` | no | | [sns\_topic\_name](#input\_sns\_topic\_name) | The name of the SNS topic to create | `string` | n/a | yes | | [sns\_topic\_tags](#input\_sns\_topic\_tags) | Additional tags for the SNS topic | `map(string)` | `{}` | no | diff --git a/examples/cloudwatch-alerts-to-slack/main.tf b/examples/cloudwatch-alerts-to-slack/main.tf index c05f6c7..0b26829 100644 --- a/examples/cloudwatch-alerts-to-slack/main.tf +++ b/examples/cloudwatch-alerts-to-slack/main.tf @@ -16,12 +16,16 @@ module "notify_slack" { lambda_function_name = "notify_slack_${each.value}" - slack_webhook_url = "https://hooks.slack.com/services/AAA/BBB/CCC" - slack_channel = "aws-notification" - slack_username = "reporter" + # Note: this needs to exist in your account already in SSM + # and should be set as a SecureString + slack_webhook_url_ssm_parameter_name = "/example/notify_slack/webhook_url" + environment_variables = { + SLACK_USERNAME = "reporter" + SLACK_EMOJI = ":wave:" + LOG_EVENTS = "True" + } lambda_description = "Lambda function which sends notifications to Slack" - log_events = true # VPC # lambda_function_vpc_subnet_ids = module.vpc.intra_subnets diff --git a/examples/notify-slack-simple/main.tf b/examples/notify-slack-simple/main.tf index b695fd9..c78bbb4 100644 --- a/examples/notify-slack-simple/main.tf +++ b/examples/notify-slack-simple/main.tf @@ -30,10 +30,14 @@ module "notify_slack" { sns_topic_name = aws_sns_topic.example.name create_sns_topic = false - slack_webhook_url = "https://hooks.slack.com/services/AAA/BBB/CCC" - slack_channel = "aws-notification" - slack_username = "reporter" - log_events = true + # Note: this needs to exist in your account already in SSM + # and should be set as a SecureString + slack_webhook_url_ssm_parameter_name = "/example/notify_slack/webhook_url" + environment_variables = { + SLACK_USERNAME = "reporter" + SLACK_EMOJI = ":wave:" + LOG_EVENTS = "True" + } tags = local.tags } diff --git a/functions/README.md b/functions/README.md index 996f202..a6b6c03 100644 --- a/functions/README.md +++ b/functions/README.md @@ -83,7 +83,6 @@ To run the unit tests: ```hcl slack_webhook_url = "https://hooks.slack.com/services/AAA/BBB/CCC" - slack_channel = "aws-notification" slack_username = "reporter" ``` diff --git a/functions/notify_slack.py b/functions/notify_slack.py index 4ed1750..279baf3 100644 --- a/functions/notify_slack.py +++ b/functions/notify_slack.py @@ -16,6 +16,7 @@ from urllib.error import HTTPError from aws_lambda_powertools import Logger # type: ignore +from aws_lambda_powertools.utilities import parameters from aws_lambda_powertools.utilities.data_classes import SNSEvent, event_source from cloudwatch import get_slack_attachment as get_cloudwatch_slack_attachment @@ -66,12 +67,10 @@ def get_slack_message_payload(message: Union[str, Dict], region: str, subject: O :returns: Slack message payload """ - slack_channel = os.environ["SLACK_CHANNEL"] slack_username = os.environ["SLACK_USERNAME"] slack_emoji = os.environ["SLACK_EMOJI"] payload = { - "channel": slack_channel, "username": slack_username, "icon_emoji": slack_emoji, } @@ -111,9 +110,21 @@ def send_slack_notification(payload: Dict[str, Any]) -> str: :returns: response details from sending notification """ - slack_url = os.environ["SLACK_WEBHOOK_URL"] + # Pull from SSM parameter + webhook_url_ssm_param_name = os.environ.get("SLACK_WEBHOOK_URL_SSM_PARAM_NAME") + if webhook_url_ssm_param_name: + slack_webhook_url = parameters.get_parameter(webhook_url_ssm_param_name, decrypt=True, max_age=300) + + # Pull from Secrets Manager + webhook_url_secret_name = os.environ.get("SLACK_WEBHOOK_URL_SECRET_NAME") + if webhook_url_secret_name: + slack_webhook_url = parameters.get_secret(webhook_url_secret_name, max_age=300) + + if not slack_webhook_url: + raise KeyError("One of `SLACK_WEBHOOK_URL_SSM_PARAM_NAME` or `SLACK_WEBHOOK_URL_SECRET_NAME` must be provided") + data = urllib.parse.urlencode({"payload": json.dumps(payload)}).encode("utf-8") - req = urllib.request.Request(slack_url) + req = urllib.request.Request(cast(str, slack_webhook_url)) try: result = urllib.request.urlopen(req, data) diff --git a/functions/tests/integration_test.py b/functions/tests/integration_test.py index 92636f2..75595b0 100644 --- a/functions/tests/integration_test.py +++ b/functions/tests/integration_test.py @@ -70,7 +70,6 @@ def publish_event_to_sns_topic(): response = sns_client.publish( TopicArn=SNS_TOPIC_ARN, Message=msg, - Subject=event, ) pprint(response) diff --git a/functions/tests/notify_slack_test.py b/functions/tests/notify_slack_test.py index 18aeca8..cb07c75 100644 --- a/functions/tests/notify_slack_test.py +++ b/functions/tests/notify_slack_test.py @@ -21,10 +21,8 @@ @pytest.fixture(autouse=True) def mock_settings_env_vars(monkeypatch): - monkeypatch.setenv("SLACK_CHANNEL", "slack_testing_sandbox") monkeypatch.setenv("SLACK_USERNAME", "notify_slack_test") monkeypatch.setenv("SLACK_EMOJI", ":aws:") - monkeypatch.setenv("SLACK_WEBHOOK_URL", "https://hooks.slack.com/services/YOUR/WEBOOK/URL") def test_sns_get_slack_message_payload_snapshots(snapshot, monkeypatch): @@ -99,10 +97,8 @@ def test_environment_variables_missing(monkeypatch): Should pass since environment variables are NOT provided and will raise a `KeyError` """ - monkeypatch.delenv("SLACK_CHANNEL") monkeypatch.delenv("SLACK_USERNAME") monkeypatch.delenv("SLACK_EMOJI") - monkeypatch.delenv("SLACK_WEBHOOK_URL") with pytest.raises(KeyError): # will raise before parsing/validation diff --git a/functions/tests/snapshots/snap_notify_slack_test.py b/functions/tests/snapshots/snap_notify_slack_test.py index 11d9316..a7e0af8 100644 --- a/functions/tests/snapshots/snap_notify_slack_test.py +++ b/functions/tests/snapshots/snap_notify_slack_test.py @@ -6,378 +6,439 @@ snapshots = Snapshot() -snapshots[ - "test_event_get_slack_message_payload_snapshots event_cloudwatch_alarm.json" -] = [ +snapshots['test_event_get_slack_message_payload_snapshots event_cloudwatch_alarm.json'] = [ { - "attachments": [ + 'attachments': [ { - "color": "danger", - "fallback": "Alarm Example triggered", - "fields": [ - {"short": True, "title": "Alarm Name", "value": "`Example`"}, + 'color': 'danger', + 'fallback': 'Alarm Example triggered', + 'fields': [ { - "short": False, - "title": "Alarm Description", - "value": "`Example alarm description.`", + 'short': True, + 'title': 'Alarm Name', + 'value': '`Example`' }, { - "short": False, - "title": "Alarm reason", - "value": "`Threshold Crossed`", + 'short': False, + 'title': 'Alarm Description', + 'value': '`Example alarm description.`' }, - {"short": True, "title": "Old State", "value": "`OK`"}, - {"short": True, "title": "Current State", "value": "`ALARM`"}, { - "short": False, - "title": "Link to Alarm", - "value": "https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#alarm:alarmFilter=ANY;name=Example", + 'short': False, + 'title': 'Alarm reason', + 'value': '`Threshold Crossed`' }, + { + 'short': True, + 'title': 'Old State', + 'value': '`OK`' + }, + { + 'short': True, + 'title': 'Current State', + 'value': '`ALARM`' + }, + { + 'short': False, + 'title': 'Link to Alarm', + 'value': 'https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#alarm:alarmFilter=ANY;name=Example' + } ], - "text": "AWS CloudWatch notification - Example", + 'text': 'AWS CloudWatch notification - Example' } ], - "channel": "slack_testing_sandbox", - "icon_emoji": ":aws:", - "username": "notify_slack_test", + 'icon_emoji': ':aws:', + 'username': 'notify_slack_test' } ] -snapshots[ - "test_event_get_slack_message_payload_snapshots event_guardduty_finding_high.json" -] = [ +snapshots['test_event_get_slack_message_payload_snapshots event_guardduty_finding_high.json'] = [ { - "attachments": [ + 'attachments': [ { - "color": "danger", - "fallback": "GuardDuty Finding: SAMPLE Unprotected port on EC2 instance i-123123123 is being probed", - "fields": [ + 'color': 'danger', + 'fallback': 'GuardDuty Finding: SAMPLE Unprotected port on EC2 instance i-123123123 is being probed', + 'fields': [ { - "short": False, - "title": "Description", - "value": "`EC2 instance has an unprotected port which is being probed by a known malicious host.`", + 'short': False, + 'title': 'Description', + 'value': '`EC2 instance has an unprotected port which is being probed by a known malicious host.`' }, { - "short": False, - "title": "Finding Type", - "value": "`Recon:EC2 PortProbeUnprotectedPort`", + 'short': False, + 'title': 'Finding Type', + 'value': '`Recon:EC2 PortProbeUnprotectedPort`' }, { - "short": True, - "title": "First Seen", - "value": "`2020-01-02T01:02:03Z`", + 'short': True, + 'title': 'First Seen', + 'value': '`2020-01-02T01:02:03Z`' }, { - "short": True, - "title": "Last Seen", - "value": "`2020-01-03T01:02:03Z`", + 'short': True, + 'title': 'Last Seen', + 'value': '`2020-01-03T01:02:03Z`' }, - {"short": True, "title": "Severity", "value": "`High`"}, - {"short": True, "title": "Count", "value": "`1234`"}, { - "short": False, - "title": "Link to Finding", - "value": "https://console.aws.amazon.com/guardduty/home?region=us-east-1#/findings?search=id%3Dsample-id-2", + 'short': True, + 'title': 'Severity', + 'value': '`High`' }, + { + 'short': True, + 'title': 'Count', + 'value': '`1234`' + }, + { + 'short': False, + 'title': 'Link to Finding', + 'value': 'https://console.aws.amazon.com/guardduty/home?region=us-east-1#/findings?search=id%3Dsample-id-2' + } ], - "text": "AWS GuardDuty Finding - SAMPLE Unprotected port on EC2 instance i-123123123 is being probed", + 'text': 'AWS GuardDuty Finding - SAMPLE Unprotected port on EC2 instance i-123123123 is being probed' } ], - "channel": "slack_testing_sandbox", - "icon_emoji": ":aws:", - "username": "notify_slack_test", + 'icon_emoji': ':aws:', + 'username': 'notify_slack_test' } ] -snapshots[ - "test_event_get_slack_message_payload_snapshots event_guardduty_finding_low.json" -] = [ +snapshots['test_event_get_slack_message_payload_snapshots event_guardduty_finding_low.json'] = [ { - "attachments": [ + 'attachments': [ { - "color": "#777777", - "fallback": "GuardDuty Finding: SAMPLE Unprotected port on EC2 instance i-123123123 is being probed", - "fields": [ + 'color': '#777777', + 'fallback': 'GuardDuty Finding: SAMPLE Unprotected port on EC2 instance i-123123123 is being probed', + 'fields': [ + { + 'short': False, + 'title': 'Description', + 'value': '`EC2 instance has an unprotected port which is being probed by a known malicious host.`' + }, { - "short": False, - "title": "Description", - "value": "`EC2 instance has an unprotected port which is being probed by a known malicious host.`", + 'short': False, + 'title': 'Finding Type', + 'value': '`Recon:EC2 PortProbeUnprotectedPort`' }, { - "short": False, - "title": "Finding Type", - "value": "`Recon:EC2 PortProbeUnprotectedPort`", + 'short': True, + 'title': 'First Seen', + 'value': '`2020-01-02T01:02:03Z`' }, { - "short": True, - "title": "First Seen", - "value": "`2020-01-02T01:02:03Z`", + 'short': True, + 'title': 'Last Seen', + 'value': '`2020-01-03T01:02:03Z`' }, { - "short": True, - "title": "Last Seen", - "value": "`2020-01-03T01:02:03Z`", + 'short': True, + 'title': 'Severity', + 'value': '`Low`' }, - {"short": True, "title": "Severity", "value": "`Low`"}, - {"short": True, "title": "Count", "value": "`1234`"}, { - "short": False, - "title": "Link to Finding", - "value": "https://console.aws.amazon.com/guardduty/home?region=us-east-1#/findings?search=id%3Dsample-id-2", + 'short': True, + 'title': 'Count', + 'value': '`1234`' }, + { + 'short': False, + 'title': 'Link to Finding', + 'value': 'https://console.aws.amazon.com/guardduty/home?region=us-east-1#/findings?search=id%3Dsample-id-2' + } ], - "text": "AWS GuardDuty Finding - SAMPLE Unprotected port on EC2 instance i-123123123 is being probed", + 'text': 'AWS GuardDuty Finding - SAMPLE Unprotected port on EC2 instance i-123123123 is being probed' } ], - "channel": "slack_testing_sandbox", - "icon_emoji": ":aws:", - "username": "notify_slack_test", + 'icon_emoji': ':aws:', + 'username': 'notify_slack_test' } ] -snapshots[ - "test_event_get_slack_message_payload_snapshots event_guardduty_finding_medium.json" -] = [ +snapshots['test_event_get_slack_message_payload_snapshots event_guardduty_finding_medium.json'] = [ { - "attachments": [ + 'attachments': [ { - "color": "warning", - "fallback": "GuardDuty Finding: SAMPLE Unprotected port on EC2 instance i-123123123 is being probed", - "fields": [ + 'color': 'warning', + 'fallback': 'GuardDuty Finding: SAMPLE Unprotected port on EC2 instance i-123123123 is being probed', + 'fields': [ { - "short": False, - "title": "Description", - "value": "`EC2 instance has an unprotected port which is being probed by a known malicious host.`", + 'short': False, + 'title': 'Description', + 'value': '`EC2 instance has an unprotected port which is being probed by a known malicious host.`' }, { - "short": False, - "title": "Finding Type", - "value": "`Recon:EC2 PortProbeUnprotectedPort`", + 'short': False, + 'title': 'Finding Type', + 'value': '`Recon:EC2 PortProbeUnprotectedPort`' }, { - "short": True, - "title": "First Seen", - "value": "`2020-01-02T01:02:03Z`", + 'short': True, + 'title': 'First Seen', + 'value': '`2020-01-02T01:02:03Z`' }, { - "short": True, - "title": "Last Seen", - "value": "`2020-01-03T01:02:03Z`", + 'short': True, + 'title': 'Last Seen', + 'value': '`2020-01-03T01:02:03Z`' }, - {"short": True, "title": "Severity", "value": "`Medium`"}, - {"short": True, "title": "Count", "value": "`1234`"}, { - "short": False, - "title": "Link to Finding", - "value": "https://console.aws.amazon.com/guardduty/home?region=us-east-1#/findings?search=id%3Dsample-id-2", + 'short': True, + 'title': 'Severity', + 'value': '`Medium`' }, + { + 'short': True, + 'title': 'Count', + 'value': '`1234`' + }, + { + 'short': False, + 'title': 'Link to Finding', + 'value': 'https://console.aws.amazon.com/guardduty/home?region=us-east-1#/findings?search=id%3Dsample-id-2' + } ], - "text": "AWS GuardDuty Finding - SAMPLE Unprotected port on EC2 instance i-123123123 is being probed", + 'text': 'AWS GuardDuty Finding - SAMPLE Unprotected port on EC2 instance i-123123123 is being probed' } ], - "channel": "slack_testing_sandbox", - "icon_emoji": ":aws:", - "username": "notify_slack_test", + 'icon_emoji': ':aws:', + 'username': 'notify_slack_test' } ] -snapshots[ - "test_sns_get_slack_message_payload_snapshots message_cloudwatch_alarm.json" -] = [ +snapshots['test_sns_get_slack_message_payload_snapshots message_cloudwatch_alarm.json'] = [ { - "attachments": [ + 'attachments': [ { - "color": "good", - "fallback": "Alarm DBMigrationRequired triggered", - "fields": [ + 'color': 'good', + 'fallback': 'Alarm DBMigrationRequired triggered', + 'fields': [ + { + 'short': True, + 'title': 'Alarm Name', + 'value': '`DBMigrationRequired`' + }, { - "short": True, - "title": "Alarm Name", - "value": "`DBMigrationRequired`", + 'short': False, + 'title': 'Alarm Description', + 'value': '`App is reporting "A JPA error occurred(Unable to build EntityManagerFactory)"`' }, { - "short": False, - "title": "Alarm Description", - "value": '`App is reporting "A JPA error occurred(Unable to build EntityManagerFactory)"`', + 'short': False, + 'title': 'Alarm reason', + 'value': '`Threshold Crossed: 1 datapoint [1.0 (12/02/19 15:44:00)] was not less than the threshold (1.0).`' }, { - "short": False, - "title": "Alarm reason", - "value": "`Threshold Crossed: 1 datapoint [1.0 (12/02/19 15:44:00)] was not less than the threshold (1.0).`", + 'short': True, + 'title': 'Old State', + 'value': '`ALARM`' }, - {"short": True, "title": "Old State", "value": "`ALARM`"}, - {"short": True, "title": "Current State", "value": "`OK`"}, { - "short": False, - "title": "Link to Alarm", - "value": "https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#alarm:alarmFilter=ANY;name=DBMigrationRequired", + 'short': True, + 'title': 'Current State', + 'value': '`OK`' }, + { + 'short': False, + 'title': 'Link to Alarm', + 'value': 'https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#alarm:alarmFilter=ANY;name=DBMigrationRequired' + } ], - "text": "AWS CloudWatch notification - DBMigrationRequired", + 'text': 'AWS CloudWatch notification - DBMigrationRequired' } ], - "channel": "slack_testing_sandbox", - "icon_emoji": ":aws:", - "username": "notify_slack_test", + 'icon_emoji': ':aws:', + 'username': 'notify_slack_test' } ] -snapshots[ - "test_sns_get_slack_message_payload_snapshots message_dms_notification.json" -] = [ +snapshots['test_sns_get_slack_message_payload_snapshots message_dms_notification.json'] = [ { - "attachments": [ + 'attachments': [ { - "fallback": "A new message", - "fields": [ + 'fallback': 'A new message', + 'fields': [ { - "short": True, - "title": "Event Source", - "value": "`replication-task`", + 'short': True, + 'title': 'Event Source', + 'value': '`replication-task`' }, { - "short": True, - "title": "Event Time", - "value": "`2019-02-12 15:45:24.091`", + 'short': True, + 'title': 'Event Time', + 'value': '`2019-02-12 15:45:24.091`' }, { - "short": False, - "title": "Identifier Link", - "value": "`https://console.aws.amazon.com/dms/home?region=us-east-1#tasks:ids=hello-world`", + 'short': False, + 'title': 'Identifier Link', + 'value': '`https://console.aws.amazon.com/dms/home?region=us-east-1#tasks:ids=hello-world`' }, - {"short": True, "title": "SourceId", "value": "`hello-world`"}, { - "short": False, - "title": "Event ID", - "value": "`http://docs.aws.amazon.com/dms/latest/userguide/CHAP_Events.html#DMS-EVENT-0079 `", + 'short': True, + 'title': 'SourceId', + 'value': '`hello-world`' }, { - "short": False, - "title": "Event Message", - "value": "`Replication task has stopped.`", + 'short': False, + 'title': 'Event ID', + 'value': '`http://docs.aws.amazon.com/dms/latest/userguide/CHAP_Events.html#DMS-EVENT-0079 `' }, + { + 'short': False, + 'title': 'Event Message', + 'value': '`Replication task has stopped.`' + } + ], + 'mrkdwn_in': [ + 'value' ], - "mrkdwn_in": ["value"], - "text": "AWS notification", - "title": "DMS Notification Message", + 'text': 'AWS notification', + 'title': 'DMS Notification Message' } ], - "channel": "slack_testing_sandbox", - "icon_emoji": ":aws:", - "username": "notify_slack_test", + 'icon_emoji': ':aws:', + 'username': 'notify_slack_test' } ] -snapshots[ - "test_sns_get_slack_message_payload_snapshots message_glue_notification.json" -] = [ +snapshots['test_sns_get_slack_message_payload_snapshots message_glue_notification.json'] = [ { - "attachments": [ + 'attachments': [ { - "fallback": "A new message", - "fields": [ - {"short": True, "title": "version", "value": "`0`"}, + 'fallback': 'A new message', + 'fields': [ + { + 'short': True, + 'title': 'version', + 'value': '`0`' + }, + { + 'short': False, + 'title': 'id', + 'value': '`ad3c3da1-148c-d5da-9a6a-79f1bc9a8a2e`' + }, + { + 'short': True, + 'title': 'detail-type', + 'value': '`Glue Job State Change`' + }, + { + 'short': True, + 'title': 'source', + 'value': '`aws.glue`' + }, { - "short": False, - "title": "id", - "value": "`ad3c3da1-148c-d5da-9a6a-79f1bc9a8a2e`", + 'short': True, + 'title': 'account', + 'value': '`000000000000`' }, { - "short": True, - "title": "detail-type", - "value": "`Glue Job State Change`", + 'short': True, + 'title': 'time', + 'value': '`2021-06-18T12:34:06Z`' }, - {"short": True, "title": "source", "value": "`aws.glue`"}, - {"short": True, "title": "account", "value": "`000000000000`"}, - {"short": True, "title": "time", "value": "`2021-06-18T12:34:06Z`"}, - {"short": True, "title": "region", "value": "`us-east-2`"}, - {"short": True, "title": "resources", "value": "`[]`"}, { - "short": False, - "title": "detail", - "value": '`{"jobName": "test_job", "severity": "ERROR", "state": "FAILED", "jobRunId": "jr_ca2144d747b45ad412d3c66a1b6934b6b27aa252be9a21a95c54dfaa224a1925", "message": "SystemExit: 1"}`', + 'short': True, + 'title': 'region', + 'value': '`us-east-2`' }, + { + 'short': True, + 'title': 'resources', + 'value': '`[]`' + }, + { + 'short': False, + 'title': 'detail', + 'value': '`{"jobName": "test_job", "severity": "ERROR", "state": "FAILED", "jobRunId": "jr_ca2144d747b45ad412d3c66a1b6934b6b27aa252be9a21a95c54dfaa224a1925", "message": "SystemExit: 1"}`' + } + ], + 'mrkdwn_in': [ + 'value' ], - "mrkdwn_in": ["value"], - "text": "AWS notification", - "title": "Message", + 'text': 'AWS notification', + 'title': 'Message' } ], - "channel": "slack_testing_sandbox", - "icon_emoji": ":aws:", - "username": "notify_slack_test", + 'icon_emoji': ':aws:', + 'username': 'notify_slack_test' } ] -snapshots[ - "test_sns_get_slack_message_payload_snapshots message_guardduty_finding.json" -] = [ +snapshots['test_sns_get_slack_message_payload_snapshots message_guardduty_finding.json'] = [ { - "attachments": [ + 'attachments': [ { - "color": "danger", - "fallback": "GuardDuty Finding: SAMPLE Unprotected port on EC2 instance i-123123123 is being probed", - "fields": [ + 'color': 'danger', + 'fallback': 'GuardDuty Finding: SAMPLE Unprotected port on EC2 instance i-123123123 is being probed', + 'fields': [ { - "short": False, - "title": "Description", - "value": "`EC2 instance has an unprotected port which is being probed by a known malicious host.`", + 'short': False, + 'title': 'Description', + 'value': '`EC2 instance has an unprotected port which is being probed by a known malicious host.`' }, { - "short": False, - "title": "Finding Type", - "value": "`Recon:EC2 PortProbeUnprotectedPort`", + 'short': False, + 'title': 'Finding Type', + 'value': '`Recon:EC2 PortProbeUnprotectedPort`' }, { - "short": True, - "title": "First Seen", - "value": "`2020-01-02T01:02:03Z`", + 'short': True, + 'title': 'First Seen', + 'value': '`2020-01-02T01:02:03Z`' }, { - "short": True, - "title": "Last Seen", - "value": "`2020-01-03T01:02:03Z`", + 'short': True, + 'title': 'Last Seen', + 'value': '`2020-01-03T01:02:03Z`' }, - {"short": True, "title": "Severity", "value": "`High`"}, - {"short": True, "title": "Count", "value": "`1234`"}, { - "short": False, - "title": "Link to Finding", - "value": "https://console.amazonaws-us-gov.com/guardduty/home?region=us-gov-east-1#/findings?search=id%3Dsample-id-2", + 'short': True, + 'title': 'Severity', + 'value': '`High`' }, + { + 'short': True, + 'title': 'Count', + 'value': '`1234`' + }, + { + 'short': False, + 'title': 'Link to Finding', + 'value': 'https://console.amazonaws-us-gov.com/guardduty/home?region=us-gov-east-1#/findings?search=id%3Dsample-id-2' + } ], - "text": "AWS GuardDuty Finding - SAMPLE Unprotected port on EC2 instance i-123123123 is being probed", + 'text': 'AWS GuardDuty Finding - SAMPLE Unprotected port on EC2 instance i-123123123 is being probed' } ], - "channel": "slack_testing_sandbox", - "icon_emoji": ":aws:", - "username": "notify_slack_test", + 'icon_emoji': ':aws:', + 'username': 'notify_slack_test' } ] -snapshots["test_sns_get_slack_message_payload_snapshots message_text_message.json"] = [ +snapshots['test_sns_get_slack_message_payload_snapshots message_text_message.json'] = [ { - "attachments": [ + 'attachments': [ { - "fallback": "A new message", - "fields": [ + 'fallback': 'A new message', + 'fields': [ { - "short": False, - "value": """This + 'short': False, + 'value': '''This is a typical multi-line message from SNS! -Have a ~good~ amazing day! :)""", +Have a ~good~ amazing day! :)''' } ], - "mrkdwn_in": ["value"], - "text": "AWS notification", - "title": "All Fine", + 'mrkdwn_in': [ + 'value' + ], + 'text': 'AWS notification', + 'title': 'All Fine' } ], - "channel": "slack_testing_sandbox", - "icon_emoji": ":aws:", - "username": "notify_slack_test", + 'icon_emoji': ':aws:', + 'username': 'notify_slack_test' } ] diff --git a/main.tf b/main.tf index c9c259e..6b3394d 100644 --- a/main.tf +++ b/main.tf @@ -3,13 +3,16 @@ data "aws_partition" "current" {} data "aws_region" "current" {} locals { + account_id = data.aws_caller_identity.current.account_id + partition_id = data.aws_partition.current.id + region = data.aws_region.current.name sns_topic_arn = try( aws_sns_topic.this[0].arn, - "arn:${data.aws_partition.current.id}:sns:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${var.sns_topic_name}", + "arn:${local.partition_id}:sns:${local.region}:${local.account_id}:${var.sns_topic_name}", "" ) - aws_lambda_powertools_layer = substr(data.aws_region.current.name, 0, 6) != "us-gov-" ? "arn:aws:lambda:${data.aws_region.current.name}:017000801446:layer:AWSLambdaPowertoolsPython:4" : "" + aws_lambda_powertools_layer = contains(["aws-us-gov", "aws-cn"], local.partition_id) ? "" : "arn:${local.partition_id}:lambda:${local.region}:017000801446:layer:AWSLambdaPowertoolsPython:4" lambda_layers = compact(distinct(concat(var.lambda_layers, [local.aws_lambda_powertools_layer]))) } @@ -69,6 +72,10 @@ module "lambda" { timeout = var.lambda_timeout layers = local.lambda_layers + environment_variables = merge(var.environment_variables, { + SLACK_WEBHOOK_URL_SSM_PARAM_NAME = var.slack_webhook_url_ssm_parameter_name + SLACK_WEBHOOK_URL_SECRET_NAME = var.slack_webhook_url_secret_name + }) kms_key_arn = var.kms_key_arn reserved_concurrent_executions = var.reserved_concurrent_executions @@ -76,14 +83,6 @@ module "lambda" { # InvalidParameterValueException: We currently do not support adding policies for $LATEST." publish = true - environment_variables = { - SLACK_WEBHOOK_URL = var.slack_webhook_url - SLACK_CHANNEL = var.slack_channel - SLACK_USERNAME = var.slack_username - SLACK_EMOJI = var.slack_emoji - LOG_EVENTS = var.log_events ? "True" : "False" - } - create_role = var.lambda_role == "" lambda_role = var.lambda_role role_name = "${var.iam_role_name_prefix}-${var.lambda_function_name}" @@ -93,6 +92,30 @@ module "lambda" { policy_path = var.iam_policy_path attach_network_policy = var.lambda_function_vpc_subnet_ids != null + attach_policy_json = true + policy_json = <<-EOT + { + "Version": "2012-10-17", + "Statement": [ + %{if length(var.slack_webhook_url_ssm_parameter_name) > 1~} + { + "Sid": "GetSsmWebhookUrl", + "Effect": "Allow", + "Action": ["ssm:GetParameter"], + "Resource": ["arn:${local.partition_id}:ssm:${local.region}:${local.account_id}:parameter${var.slack_webhook_url_ssm_parameter_name}"] + } + %{endif~} + %{if length(var.slack_webhook_url_secret_name) > 1~} + { + "Sid": "GetSecretWebhookUrl", + "Effect": "Allow", + "Action": ["secretsmanager:GetSecretValue"], + "Resource": ["arn:${local.partition_id}:secretsmanager:${local.region}:${local.account_id}:secret:${var.slack_webhook_url_secret_name}-*"] + } + %{endif~} + ] + } + EOT allowed_triggers = { AllowExecutionFromSNS = { diff --git a/variables.tf b/variables.tf index 40afa9b..5fcbfd8 100644 --- a/variables.tf +++ b/variables.tf @@ -101,25 +101,22 @@ variable "lambda_function_s3_bucket" { default = null } -variable "slack_webhook_url" { - description = "The URL of Slack webhook" - type = string -} - -variable "slack_channel" { - description = "The name of the channel in Slack for notifications" +variable "slack_webhook_url_ssm_parameter_name" { + description = "Name of SSM parameter that contains the Slack webhook URL (must be type `SecureString`). If not provided, `slack_webhook_url_secret_name` must be provided" type = string + default = "" } -variable "slack_username" { - description = "The username that will appear on Slack messages" +variable "slack_webhook_url_secret_name" { + description = "Name of Secrets Manager secret that contains the Slack webhook URL. If not provided, `slack_webhook_url_ssm_parameter_name` must be provided" type = string + default = "" } -variable "slack_emoji" { - description = "A custom emoji that will appear on Slack messages" - type = string - default = ":aws:" +variable "environment_variables" { + description = "A map that defines environment variables for the Lambda Function" + type = map(string) + default = {} } variable "kms_key_arn" { @@ -134,12 +131,6 @@ variable "recreate_missing_package" { default = true } -variable "log_events" { - description = "Boolean flag to enabled/disable logging of incoming events" - type = bool - default = false -} - variable "reserved_concurrent_executions" { description = "The amount of reserved concurrent executions for this lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations" type = number @@ -195,7 +186,7 @@ variable "iam_policy_path" { variable "cloudwatch_log_group_retention_in_days" { description = "Specifies the number of days you want to retain log events in log group for Lambda" type = number - default = 0 + default = 7 } variable "cloudwatch_log_group_kms_key_id" {