├── functions
├── snapshots
│ ├── __init__.py
│ └── snap_notify_slack_test.py
├── .flake8
├── events
│ ├── cloudwatch_alarm.json
│ ├── guardduty_finding_low.json
│ ├── guardduty_finding_high.json
│ ├── guardduty_finding_medium.json
│ └── aws_health_event.json
├── .gitignore
├── messages
│ ├── text_message.json
│ ├── dms_notification.json
│ ├── glue_notification.json
│ ├── guardduty_finding.json
│ ├── cloudwatch_alarm.json
│ ├── guardduty_malware_protection_object_scan_result.json
│ └── backup.json
├── mylambda.py
├── Pipfile
├── .pyproject.toml
├── integration_test.py
├── notify_slack_test.py
├── README.md
└── notify_slack.py
├── examples
├── notify-slack-simple
│ ├── variables.tf
│ ├── versions.tf
│ ├── custom-lambda.tf
│ ├── main.tf
│ ├── outputs.tf
│ └── README.md
├── cloudwatch-alerts-to-slack
│ ├── variables.tf
│ ├── versions.tf
│ ├── outputs.tf
│ ├── main.tf
│ └── README.md
└── README.md
├── versions.tf
├── .editorconfig
├── .gitignore
├── .github
└── workflows
│ ├── unit-test.yml
│ ├── lock.yml
│ ├── release.yml
│ ├── stale-actions.yaml
│ ├── pr-title.yml
│ └── pre-commit.yml
├── .releaserc.json
├── iam.tf
├── .pre-commit-config.yaml
├── outputs.tf
├── main.tf
├── variables.tf
├── LICENSE
├── README.md
└── CHANGELOG.md
/functions/snapshots/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/notify-slack-simple/variables.tf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/cloudwatch-alerts-to-slack/variables.tf:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_version = ">= 1.5.7"
3 |
4 | required_providers {
5 | aws = {
6 | source = "hashicorp/aws"
7 | version = ">= 6.0"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/functions/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-complexity = 10
3 | max-line-length = 120
4 | exclude =
5 | .pytest_cache
6 | __pycache__/
7 | *tests/
8 | events/
9 | messages/
10 | snapshots/
11 |
--------------------------------------------------------------------------------
/examples/notify-slack-simple/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_version = ">= 1.5.7"
3 |
4 | required_providers {
5 | aws = {
6 | source = "hashicorp/aws"
7 | version = ">= 6.0"
8 | }
9 | local = {
10 | source = "hashicorp/local"
11 | version = ">= 2.0"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/cloudwatch-alerts-to-slack/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_version = ">= 1.5.7"
3 |
4 | required_providers {
5 | aws = {
6 | source = "hashicorp/aws"
7 | version = ">= 6.0"
8 | }
9 | random = {
10 | source = "hashicorp/random"
11 | version = ">= 2.0"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/functions/events/cloudwatch_alarm.json:
--------------------------------------------------------------------------------
1 | {
2 | "AlarmName": "Example",
3 | "AlarmDescription": "Example alarm description.",
4 | "AWSAccountId": "000000000000",
5 | "NewStateValue": "ALARM",
6 | "NewStateReason": "Threshold Crossed",
7 | "StateChangeTime": "2017-01-12T16:30:42.236+0000",
8 | "Region": "EU - Ireland",
9 | "OldStateValue": "OK"
10 | }
11 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | Please note - the examples provided serve two primary means:
4 |
5 | 1. Show users working examples of the various ways in which the module can be configured and features supported
6 | 2. A means of testing/validating module changes
7 |
8 | Please do not mistake the examples provided as "best practices". It is up to users to consult the AWS service documentation for best practices, usage recommendations, etc.
9 |
--------------------------------------------------------------------------------
/functions/events/guardduty_finding_low.json:
--------------------------------------------------------------------------------
1 | {
2 | "detail-type": "GuardDuty Finding",
3 | "region": "us-east-1",
4 | "detail": {
5 | "id": "sample-id-2",
6 | "title": "SAMPLE Unprotected port on EC2 instance i-123123123 is being probed",
7 | "severity": 2,
8 | "accountId": "123456789",
9 | "description": "EC2 instance has an unprotected port which is being probed by a known malicious host.",
10 | "type": "Recon:EC2 PortProbeUnprotectedPort",
11 | "service": {
12 | "eventFirstSeen": "2020-01-02T01:02:03Z",
13 | "eventLastSeen": "2020-01-03T01:02:03Z",
14 | "count": 1234
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/functions/events/guardduty_finding_high.json:
--------------------------------------------------------------------------------
1 | {
2 | "detail-type": "GuardDuty Finding",
3 | "region": "us-east-1",
4 | "detail": {
5 | "id": "sample-id-2",
6 | "title": "SAMPLE Unprotected port on EC2 instance i-123123123 is being probed",
7 | "severity": 9,
8 | "accountId": "123456789",
9 | "description": "EC2 instance has an unprotected port which is being probed by a known malicious host.",
10 | "type": "Recon:EC2 PortProbeUnprotectedPort",
11 | "service": {
12 | "eventFirstSeen": "2020-01-02T01:02:03Z",
13 | "eventLastSeen": "2020-01-03T01:02:03Z",
14 | "count": 1234
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/functions/events/guardduty_finding_medium.json:
--------------------------------------------------------------------------------
1 | {
2 | "detail-type": "GuardDuty Finding",
3 | "region": "us-east-1",
4 | "detail": {
5 | "id": "sample-id-2",
6 | "title": "SAMPLE Unprotected port on EC2 instance i-123123123 is being probed",
7 | "severity": 5,
8 | "accountId": "123456789",
9 | "description": "EC2 instance has an unprotected port which is being probed by a known malicious host.",
10 | "type": "Recon:EC2 PortProbeUnprotectedPort",
11 | "service": {
12 | "eventFirstSeen": "2020-01-02T01:02:03Z",
13 | "eventLastSeen": "2020-01-03T01:02:03Z",
14 | "count": 1234
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 | # Uses editorconfig to maintain consistent coding styles
3 |
4 | # top-most EditorConfig file
5 | root = true
6 |
7 | # Unix-style newlines with a newline ending every file
8 | [*]
9 | charset = utf-8
10 | end_of_line = lf
11 | indent_size = 2
12 | indent_style = space
13 | insert_final_newline = true
14 | max_line_length = 80
15 | trim_trailing_whitespace = true
16 |
17 | [*.{tf,tfvars}]
18 | indent_size = 2
19 | indent_style = space
20 |
21 | [*.md]
22 | max_line_length = 0
23 | trim_trailing_whitespace = false
24 |
25 | [Makefile]
26 | tab_width = 2
27 | indent_style = tab
28 |
29 | [COMMIT_EDITMSG]
30 | max_line_length = 0
31 |
--------------------------------------------------------------------------------
/functions/.gitignore:
--------------------------------------------------------------------------------
1 | # Locals
2 | .swp
3 | .idea
4 | .idea*
5 | .vscode/*
6 | *.DS_Store
7 | *.zip
8 | .env
9 | .envrc
10 |
11 | # Byte-compiled / optimized / DLL files
12 | __pycache__/
13 | *.py[cod]
14 | *$py.class
15 |
16 | # Distribution / packaging
17 | .Python
18 | env/
19 | build/
20 | develop-eggs/
21 | dist/
22 | downloads/
23 | eggs/
24 | .eggs/
25 | lib/
26 | lib64/
27 | parts/
28 | sdist/
29 | var/
30 | *.egg-info/
31 | .installed.cfg
32 | *.egg
33 |
34 | # Unit test / coverage reports
35 | .pytest*
36 | htmlcov/
37 | .tox/
38 | .coverage
39 | .coverage.*
40 | .cache
41 | nosetests.xml
42 | coverage.xml
43 | *.cover
44 | *.coverage
45 | .hypothesis/
46 | .mypy_cache/
47 |
48 | # Lockfile
49 | Pipfile.lock
50 |
51 | # Integration testing file
52 | .int.env
53 | pytest.ini
54 |
--------------------------------------------------------------------------------
/functions/messages/text_message.json:
--------------------------------------------------------------------------------
1 | {
2 | "Records": [{
3 | "EventSource": "aws:sns",
4 | "EventVersion": "1.0",
5 | "EventSubscriptionArn": "arn:aws:sns:us-gov-west-1::ExampleTopic",
6 | "Sns": {
7 | "Type": "Notification",
8 | "MessageId": "f86e3c5b-cd17-1ab8-80e9-c0776d4f1e7a",
9 | "TopicArn": "arn:aws:sns:us-gov-west-1:123456789012:ExampleTopic",
10 | "Subject": "All Fine",
11 | "Message": "This\nis\na typical multi-line\nmessage from SNS!\n\nHave a ~good~ amazing day! :)",
12 | "Timestamp": "2019-02-12T15:45:24.091Z",
13 | "SignatureVersion": "1",
14 | "Signature": "EXAMPLE",
15 | "SigningCertUrl": "EXAMPLE",
16 | "UnsubscribeUrl": "EXAMPLE",
17 | "MessageAttributes": {}
18 | }
19 | }]
20 | }
21 |
--------------------------------------------------------------------------------
/functions/mylambda.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3.6
2 | # CUSTOM LAMBDA FUNCTION
3 |
4 | import json
5 | import os
6 |
7 | import urllib3
8 |
9 | http = urllib3.PoolManager()
10 |
11 |
12 | def lambda_handler(event, context):
13 | url = os.environ["SLACK_WEBHOOK_URL"]
14 | msg = {
15 | "channel": "#channel-name",
16 | "username": "Prometheus",
17 | "text": event["Records"][0]["Sns"]["Message"],
18 | "icon_emoji": "",
19 | }
20 |
21 | encoded_msg = json.dumps(msg).encode("utf-8")
22 | resp = http.request("POST", url, body=encoded_msg)
23 | print(
24 | {
25 | "message": event["Records"][0]["Sns"]["Message"],
26 | "status_code": resp.status,
27 | "response": resp.data,
28 | }
29 | )
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Local .terraform directories
2 | **/.terraform/*
3 |
4 | # Terraform lockfile
5 | .terraform.lock.hcl
6 |
7 | # .tfstate files
8 | *.tfstate
9 | *.tfstate.*
10 |
11 | # Crash log files
12 | crash.log
13 |
14 | # Exclude all .tfvars files, which are likely to contain sentitive data, such as
15 | # password, private keys, and other secrets. These should not be part of version
16 | # control as they are data points which are potentially sensitive and subject
17 | # to change depending on the environment.
18 | *.tfvars
19 |
20 | # Ignore override files as they are usually used to override resources locally and so
21 | # are not checked in
22 | override.tf
23 | override.tf.json
24 | *_override.tf
25 | *_override.tf.json
26 |
27 | # Ignore CLI configuration files
28 | .terraformrc
29 | terraform.rc
30 |
31 | # Lambda build artifacts
32 | builds/
33 | __pycache__/
34 | *.zip
35 | .tox
36 |
37 | # Local editors/macos files
38 | .DS_Store
39 | .idea
40 |
--------------------------------------------------------------------------------
/functions/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 |
8 | [dev-packages]
9 | boto3 = "~=1.34"
10 | botocore = "~=1.34"
11 | black = "*"
12 | flake8 = "*"
13 | isort = "*"
14 | mypy = "*"
15 | pytest = "*"
16 | pytest-cov = "*"
17 | radon = "*"
18 | snapshottest = "~=0.6"
19 |
20 | [requires]
21 | python_version = "3.11"
22 |
23 | [scripts]
24 | test = "python3 -m pytest --cov --cov-report=term"
25 | 'test:updatesnapshots' = "python3 -m pytest --snapshot-update"
26 | cover = "python3 -m coverage html"
27 | complexity = "python3 -m radon cc notify_slack.py -a"
28 | halstead = "python3 -m radon hal notify_slack.py"
29 | typecheck = "python3 -m mypy . --ignore-missing-imports"
30 | lint = "python3 -m flake8 . --count --statistics --benchmark --exit-zero --config=.flake8"
31 | 'lint:ci' = "python3 -m flake8 . --config=.flake8"
32 | imports = "python3 -m isort . --profile black"
33 | format = "python3 -m black ."
34 |
35 | [pipenv]
36 | allow_prereleases = true
37 |
--------------------------------------------------------------------------------
/.github/workflows/unit-test.yml:
--------------------------------------------------------------------------------
1 | name: Unit Test
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | - master
8 | paths:
9 | - 'functions/**'
10 | - '.github/workflows/unit-test.yml'
11 |
12 | defaults:
13 | run:
14 | working-directory: functions
15 |
16 | jobs:
17 | test:
18 | name: Execute unit tests
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - name: Checkout
23 | uses: actions/checkout@v4
24 |
25 | - name: Set up Python 3.11
26 | uses: actions/setup-python@v5
27 | with:
28 | python-version: 3.11
29 |
30 | - name: Install pipenv
31 | run: |
32 | python -m pip install --upgrade pip
33 | python -m pip install pipenv
34 |
35 | - name: Install local deps
36 | run: pipenv install --dev
37 |
38 | - name: Lint check
39 | run: pipenv run lint:ci
40 |
41 | - name: Type check
42 | run: pipenv run typecheck
43 |
44 | - name: Unit tests
45 | run: pipenv run test
46 |
--------------------------------------------------------------------------------
/functions/events/aws_health_event.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0",
3 | "id": "121345678-1234-1234-1234-123456789012",
4 | "detail-type": "AWS Health Event",
5 | "source": "aws.health",
6 | "account": "123456789012",
7 | "time": "2016-06-05T06:27:57Z",
8 | "region": "us-west-2",
9 | "resources": [
10 | "i-abcd1111"
11 | ],
12 | "detail": {
13 | "eventArn": "arn:aws:health:us-west-2::event/AWS_EC2_INSTANCE_STORE_DRIVE_PERFORMANCE_DEGRADED_90353408594353980",
14 | "service": "EC2",
15 | "eventTypeCode": "AWS_EC2_INSTANCE_STORE_DRIVE_PERFORMANCE_DEGRADED",
16 | "eventTypeCategory": "issue",
17 | "startTime": "Sat, 05 Jun 2016 15:10:09 GMT",
18 | "eventDescription": [
19 | {
20 | "language": "en_US",
21 | "latestDescription": "A description of the event will be provided here"
22 | }
23 | ],
24 | "affectedEntities": [
25 | {
26 | "entityValue": "i-abcd1111",
27 | "tags": {
28 | "stage": "prod",
29 | "app": "my-app"
30 | }
31 | }
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/functions/messages/dms_notification.json:
--------------------------------------------------------------------------------
1 | {
2 | "Records": [{
3 | "EventSource": "aws:sns",
4 | "EventVersion": "1.0",
5 | "EventSubscriptionArn": "arn:aws:sns:eu-west-1::ExampleTopic",
6 | "Sns": {
7 | "Type": "Notification",
8 | "MessageId": "f86e3c5b-cd17-1ab8-80e9-c0776d4f1e7a",
9 | "TopicArn": "arn:aws:sns:eu-west-1:123456789012:ExampleTopic",
10 | "Subject": "DMS Notification Message",
11 | "Message": "{\"Event Source\": \"replication-task\",\"Event Time\": \"2019-02-12 15:45:24.091\",\"Identifier Link\": \"https:\/\/console.aws.amazon.com\/dms\/home?region=us-east-1#tasks:ids=hello-world\",\"SourceId\": \"hello-world\",\"Event ID\": \"http:\/\/docs.aws.amazon.com\/dms\/latest\/userguide\/CHAP_Events.html#DMS-EVENT-0079 \",\"Event Message\": \"Replication task has stopped.\"}",
12 | "Timestamp": "2019-02-12T15:45:24.091Z",
13 | "SignatureVersion": "1",
14 | "Signature": "EXAMPLE",
15 | "SigningCertUrl": "EXAMPLE",
16 | "UnsubscribeUrl": "EXAMPLE",
17 | "MessageAttributes": {}
18 | }
19 | }]
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/lock.yml:
--------------------------------------------------------------------------------
1 | name: 'Lock Threads'
2 |
3 | on:
4 | schedule:
5 | - cron: '50 1 * * *'
6 |
7 | jobs:
8 | lock:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: dessant/lock-threads@v5
12 | with:
13 | github-token: ${{ secrets.GITHUB_TOKEN }}
14 | issue-comment: >
15 | I'm going to lock this issue because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues.
16 | If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.
17 | issue-inactive-days: '30'
18 | pr-comment: >
19 | I'm going to lock this pull request because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues.
20 | If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.
21 | pr-inactive-days: '30'
22 |
--------------------------------------------------------------------------------
/functions/messages/glue_notification.json:
--------------------------------------------------------------------------------
1 | {
2 | "Records": [{
3 | "EventSource": "aws:sns",
4 | "EventVersion": "1.0",
5 | "EventSubscriptionArn": "arn:aws:sns:us-east-2::ExampleTopic",
6 | "Sns": {
7 | "Type": "Notification",
8 | "MessageId": "00337b3f-0982-5cb1-9138-22799c885da9",
9 | "TopicArn": "arn:aws:sns:us-east-2:123456789012:ExampleTopic",
10 | "Subject": "",
11 | "Message": "{\"version\": \"0\",\"id\": \"ad3c3da1-148c-d5da-9a6a-79f1bc9a8a2e\",\"detail-type\": \"Glue Job State Change\",\"source\": \"aws.glue\",\"account\": \"000000000000\",\"time\": \"2021-06-18T12:34:06Z\",\"region\": \"us-east-2\",\"resources\": [],\"detail\": {\"jobName\": \"test_job\",\"severity\": \"ERROR\",\"state\": \"FAILED\",\"jobRunId\": \"jr_ca2144d747b45ad412d3c66a1b6934b6b27aa252be9a21a95c54dfaa224a1925\",\"message\": \"SystemExit: 1\"}}",
12 | "Timestamp": "2021-06-18T12:34:09.509Z",
13 | "SignatureVersion": "1",
14 | "Signature": "EXAMPLE",
15 | "SigningCertUrl": "EXAMPLE",
16 | "UnsubscribeUrl": "EXAMPLE",
17 | "MessageAttributes": {}
18 | }
19 | }]
20 | }
21 |
--------------------------------------------------------------------------------
/examples/notify-slack-simple/custom-lambda.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | custom = {
3 | name = "ex-${replace(basename(path.cwd), "_", "-")}-custom"
4 | tags = merge({ "Type" = "custom" }, local.tags)
5 | }
6 | }
7 |
8 | ################################################################################
9 | # Supporting Resources
10 | ################################################################################
11 |
12 | resource "aws_sns_topic" "custom_lambda" {
13 | name = local.custom.name
14 | tags = local.custom.tags
15 | }
16 |
17 | ################################################################################
18 | # Slack Notify Module
19 | ################################################################################
20 |
21 | module "custom_lambda" {
22 | source = "../../"
23 |
24 | lambda_function_name = "custom_lambda"
25 | lambda_source_path = "../../functions/mylambda.py"
26 |
27 | iam_role_name_prefix = "custom"
28 |
29 | sns_topic_name = aws_sns_topic.custom_lambda.name
30 |
31 | slack_webhook_url = "https://hooks.slack.com/services/AAA/BBB/CCC"
32 | slack_channel = "aws-notification"
33 | slack_username = "reporter"
34 |
35 | tags = local.custom.tags
36 | }
37 |
--------------------------------------------------------------------------------
/.releaserc.json:
--------------------------------------------------------------------------------
1 | {
2 | "branches": [
3 | "main",
4 | "master"
5 | ],
6 | "ci": false,
7 | "plugins": [
8 | [
9 | "@semantic-release/commit-analyzer",
10 | {
11 | "preset": "conventionalcommits"
12 | }
13 | ],
14 | [
15 | "@semantic-release/release-notes-generator",
16 | {
17 | "preset": "conventionalcommits"
18 | }
19 | ],
20 | [
21 | "@semantic-release/github",
22 | {
23 | "successComment": "This ${issue.pull_request ? 'PR is included' : 'issue has been resolved'} in version ${nextRelease.version} :tada:",
24 | "labels": false,
25 | "releasedLabels": false
26 | }
27 | ],
28 | [
29 | "@semantic-release/changelog",
30 | {
31 | "changelogFile": "CHANGELOG.md",
32 | "changelogTitle": "# Changelog\n\nAll notable changes to this project will be documented in this file."
33 | }
34 | ],
35 | [
36 | "@semantic-release/git",
37 | {
38 | "assets": [
39 | "CHANGELOG.md"
40 | ],
41 | "message": "chore(release): version ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
42 | }
43 | ]
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/functions/messages/guardduty_finding.json:
--------------------------------------------------------------------------------
1 | {
2 | "Records": [{
3 | "EventSource": "aws:sns",
4 | "EventVersion": "1.0",
5 | "EventSubscriptionArn": "arn:aws:sns:us-gov-east-1::ExampleTopic",
6 | "Sns": {
7 | "Type": "Notification",
8 | "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
9 | "TopicArn": "arn:aws:sns:us-gov-east-1:123456789012:ExampleTopic",
10 | "Subject": "GuardDuty Finding",
11 | "Message": "{\"detail-type\": \"GuardDuty Finding\",\"region\": \"us-gov-east-1\",\"detail\": {\"id\": \"sample-id-2\",\"title\": \"SAMPLE Unprotected port on EC2 instance i-123123123 is being probed\",\"severity\": 9,\"accountId\":\"123456789\",\"description\": \"EC2 instance has an unprotected port which is being probed by a known malicious host.\",\"type\": \"Recon:EC2 PortProbeUnprotectedPort\",\"service\": {\"eventFirstSeen\": \"2020-01-02T01:02:03Z\",\"eventLastSeen\": \"2020-01-03T01:02:03Z\",\"count\": 1234}}}",
12 | "Timestamp": "1970-01-01T00:00:00.000Z",
13 | "SignatureVersion": "1",
14 | "Signature": "EXAMPLE",
15 | "SigningCertUrl": "EXAMPLE",
16 | "UnsubscribeUrl": "EXAMPLE",
17 | "MessageAttributes": {}
18 | }
19 | }]
20 | }
21 |
--------------------------------------------------------------------------------
/iam.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | create_sns_feedback_role = local.create && var.create_sns_topic && var.enable_sns_topic_delivery_status_logs && var.sns_topic_lambda_feedback_role_arn == ""
3 | }
4 |
5 | data "aws_iam_policy_document" "sns_feedback" {
6 | count = local.create_sns_feedback_role ? 1 : 0
7 |
8 | statement {
9 | sid = "SnsAssume"
10 | effect = "Allow"
11 |
12 | actions = [
13 | "sts:AssumeRole",
14 | "sts:TagSession",
15 | ]
16 |
17 | principals {
18 | type = "Service"
19 | identifiers = ["sns.amazonaws.com"]
20 | }
21 | }
22 | }
23 |
24 | resource "aws_iam_role" "sns_feedback_role" {
25 | count = local.create_sns_feedback_role ? 1 : 0
26 |
27 | name = var.sns_topic_feedback_role_name
28 | description = var.sns_topic_feedback_role_description
29 | path = var.sns_topic_feedback_role_path
30 | force_detach_policies = var.sns_topic_feedback_role_force_detach_policies
31 | permissions_boundary = var.sns_topic_feedback_role_permissions_boundary
32 | assume_role_policy = data.aws_iam_policy_document.sns_feedback[0].json
33 |
34 | tags = merge(
35 | var.tags,
36 | var.sns_topic_feedback_role_tags,
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - main
8 | - master
9 | paths:
10 | - '**/*.tpl'
11 | - '**/*.py'
12 | - '**/*.tf'
13 | - '.github/workflows/release.yml'
14 |
15 | jobs:
16 | release:
17 | name: Release
18 | runs-on: ubuntu-latest
19 | # Skip running release workflow on forks
20 | if: github.repository_owner == 'terraform-aws-modules'
21 | steps:
22 | - name: Checkout
23 | uses: actions/checkout@v5
24 | with:
25 | persist-credentials: false
26 | fetch-depth: 0
27 |
28 | - name: Set correct Node.js version
29 | uses: actions/setup-node@v6
30 | with:
31 | node-version: 24
32 |
33 | - name: Install dependencies
34 | run: |
35 | npm install \
36 | @semantic-release/changelog@6.0.3 \
37 | @semantic-release/git@10.0.1 \
38 | conventional-changelog-conventionalcommits@9.1.0
39 |
40 | - name: Release
41 | uses: cycjimmy/semantic-release-action@v5
42 | with:
43 | semantic_version: 25.0.0
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }}
46 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/antonbabenko/pre-commit-terraform
3 | rev: v1.103.0
4 | hooks:
5 | - id: terraform_fmt
6 | - id: terraform_docs
7 | args:
8 | - '--args=--lockfile=false'
9 | - id: terraform_tflint
10 | args:
11 | - '--args=--only=terraform_deprecated_interpolation'
12 | - '--args=--only=terraform_deprecated_index'
13 | - '--args=--only=terraform_unused_declarations'
14 | - '--args=--only=terraform_comment_syntax'
15 | - '--args=--only=terraform_documented_outputs'
16 | - '--args=--only=terraform_documented_variables'
17 | - '--args=--only=terraform_typed_variables'
18 | - '--args=--only=terraform_module_pinned_source'
19 | - '--args=--only=terraform_naming_convention'
20 | - '--args=--only=terraform_required_version'
21 | - '--args=--only=terraform_required_providers'
22 | - '--args=--only=terraform_standard_module_structure'
23 | - '--args=--only=terraform_workspace_remote'
24 | - id: terraform_validate
25 | - repo: https://github.com/pre-commit/pre-commit-hooks
26 | rev: v6.0.0
27 | hooks:
28 | - id: check-merge-conflict
29 | - id: end-of-file-fixer
30 | - id: trailing-whitespace
31 |
--------------------------------------------------------------------------------
/functions/.pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 120
3 | target-version = ['py311']
4 | include = '\.pyi?$'
5 | verbose = true
6 | exclude = '''
7 | /(
8 | | \.git
9 | | \.mypy_cache
10 | | dist
11 | | \.pants\.d
12 | | virtualenvs
13 | | \.venv
14 | | _build
15 | | build
16 | | dist
17 | | snapshots
18 | )/
19 | '''
20 |
21 | [tool.isort]
22 | line_length = 120
23 | skip = '.terraform'
24 | sections = 'FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER'
25 | known_third_party = 'aws_lambda_powertools,boto3,botocore,pytest,snapshottest'
26 | known_first_party = 'events,notify_slack'
27 | indent = ' '
28 |
29 | [tool.mypy]
30 | namespace_packages = true
31 | explicit_package_bases = true
32 |
33 | no_implicit_optional = true
34 | implicit_reexport = false
35 | strict_equality = true
36 |
37 | warn_unused_configs = true
38 | warn_unused_ignores = true
39 | warn_return_any = true
40 | warn_redundant_casts = true
41 | warn_unreachable = true
42 |
43 | pretty = true
44 | show_column_numbers = true
45 | show_error_context = true
46 | show_error_codes = true
47 | show_traceback = true
48 |
49 | [tool.coverage.run]
50 | branch = true
51 | omit = ["*_test.py", "tests/*", "events/*", "messages/*", "snapshots/*", "venv/*", ".mypy_cache/*", ".pytest_cache/*"]
52 |
53 | [tool.coverage.report]
54 | show_missing = true
55 | skip_covered = true
56 | skip_empty = true
57 | sort = "-Miss"
58 | fail_under = 75
59 |
--------------------------------------------------------------------------------
/.github/workflows/stale-actions.yaml:
--------------------------------------------------------------------------------
1 | name: 'Mark or close stale issues and PRs'
2 | on:
3 | schedule:
4 | - cron: '0 0 * * *'
5 |
6 | jobs:
7 | stale:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/stale@v10
11 | with:
12 | repo-token: ${{ secrets.GITHUB_TOKEN }}
13 | # Staling issues and PR's
14 | days-before-stale: 30
15 | stale-issue-label: stale
16 | stale-pr-label: stale
17 | stale-issue-message: |
18 | This issue has been automatically marked as stale because it has been open 30 days
19 | with no activity. Remove stale label or comment or this issue will be closed in 10 days
20 | stale-pr-message: |
21 | This PR has been automatically marked as stale because it has been open 30 days
22 | with no activity. Remove stale label or comment or this PR will be closed in 10 days
23 | # Not stale if have this labels or part of milestone
24 | exempt-issue-labels: bug,wip,on-hold
25 | exempt-pr-labels: bug,wip,on-hold
26 | exempt-all-milestones: true
27 | # Close issue operations
28 | # Label will be automatically removed if the issues are no longer closed nor locked.
29 | days-before-close: 10
30 | delete-branch: true
31 | close-issue-message: This issue was automatically closed because of stale in 10 days
32 | close-pr-message: This PR was automatically closed because of stale in 10 days
33 |
--------------------------------------------------------------------------------
/functions/messages/cloudwatch_alarm.json:
--------------------------------------------------------------------------------
1 | {
2 | "Records": [{
3 | "EventSource": "aws:sns",
4 | "EventVersion": "1.0",
5 | "EventSubscriptionArn": "arn:aws:sns:us-east-1::ExampleTopic",
6 | "Sns": {
7 | "Type": "Notification",
8 | "MessageId": "f86e3c5b-cd17-1ab8-80e9-c0776d4f1e7a",
9 | "TopicArn": "arn:aws:sns:us-east-1:123456789012:ExampleTopic",
10 | "Subject": "'OK: \"DBMigrationRequired\" in EU (London)",
11 | "Message": "{\"AlarmName\": \"DBMigrationRequired\",\"AlarmDescription\": \"App is reporting \\\"A JPA error occurred(Unable to build EntityManagerFactory)\\\"\",\"AWSAccountId\": \"735598076380\",\"NewStateValue\": \"OK\",\"NewStateReason\": \"Threshold Crossed: 1 datapoint [1.0 (12\/02\/19 15:44:00)] was not less than the threshold (1.0).\",\"StateChangeTime\": \"2019-02-12T15:45:24.006+0000\",\"Region\": \"US (Virginia)\",\"OldStateValue\": \"ALARM\",\"Trigger\": {\"MetricName\": \"DBMigrationRequired\",\"Namespace\": \"LogMetrics\",\"StatisticType\": \"Statistic\",\"Statistic\": \"SUM\",\"Unit\": null,\"Dimensions\": [],\"Period\": 60,\"EvaluationPeriods\": 1,\"ComparisonOperator\": \"LessThanThreshold\",\"Threshold\": 1.0,\"TreatMissingData\": \"- TreatMissingData:NonBreaching\",\"EvaluateLowSampleCountPercentile\": \"\"}}",
12 | "Timestamp": "2019-02-12T15:45:24.091Z",
13 | "SignatureVersion": "1",
14 | "Signature": "EXAMPLE",
15 | "SigningCertUrl": "EXAMPLE",
16 | "UnsubscribeUrl": "EXAMPLE",
17 | "MessageAttributes": {}
18 | }
19 | }]
20 | }
21 |
--------------------------------------------------------------------------------
/functions/messages/guardduty_malware_protection_object_scan_result.json:
--------------------------------------------------------------------------------
1 | {
2 | "Records": [{
3 | "EventSource": "aws:sns",
4 | "EventVersion": "1.0",
5 | "EventSubscriptionArn": "arn:aws:sns:us-gov-east-1::ExampleTopic",
6 | "Sns": {
7 | "Type": "Notification",
8 | "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
9 | "TopicArn": "arn:aws:sns:us-gov-east-1:123456789012:ExampleTopic",
10 | "Subject": "GuardDuty Malware Protection Object Scan Result",
11 | "Message": "{\"version\":\"0\",\"id\":\"72c7d362-737a-6dce-fc78-9e27a0171419\",\"detail-type\":\"GuardDuty Malware Protection Object Scan Result\",\"source\":\"aws.guardduty\",\"account\":\"111122223333\",\"time\":\"2024-02-28T01:01:01Z\",\"region\":\"us-east-1\",\"resources\":[\"arn:aws:guardduty:us-east-1:111122223333:malware-protection-plan/b4c7f464ab3a4EXAMPLE\"],\"detail\":{\"schemaVersion\":\"1.0\",\"scanStatus\":\"COMPLETED\",\"resourceType\":\"S3_OBJECT\",\"s3ObjectDetails\":{\"bucketName\":\"amzn-s3-demo-bucket\",\"objectKey\":\"APKAEIBAERJR2EXAMPLE\",\"eTag\":\"ASIAI44QH8DHBEXAMPLE\",\"versionId\":\"d41d8cd98f00b204e9800998eEXAMPLE\",\"s3Throttled\":false},\"scanResultDetails\":{\"scanResultStatus\":\"THREATS_FOUND\",\"threats\":[{\"name\":\"EICAR-Test-File (not a virus)\"}]}}}",
12 | "Timestamp": "1970-01-01T00:00:00.000Z",
13 | "SignatureVersion": "1",
14 | "Signature": "EXAMPLE",
15 | "SigningCertUrl": "EXAMPLE",
16 | "UnsubscribeUrl": "EXAMPLE",
17 | "MessageAttributes": {}
18 | }
19 | }]
20 | }
21 |
--------------------------------------------------------------------------------
/examples/cloudwatch-alerts-to-slack/outputs.tf:
--------------------------------------------------------------------------------
1 | output "sns_topic_arn" {
2 | description = "The ARN of the SNS topic from which messages will be sent to Slack"
3 | value = module.notify_slack["develop"].slack_topic_arn
4 | }
5 |
6 | output "lambda_iam_role_arn" {
7 | description = "The ARN of the IAM role used by Lambda function"
8 | value = module.notify_slack["develop"].lambda_iam_role_arn
9 | }
10 |
11 | output "lambda_iam_role_name" {
12 | description = "The name of the IAM role used by Lambda function"
13 | value = module.notify_slack["develop"].lambda_iam_role_name
14 | }
15 |
16 | output "notify_slack_lambda_function_arn" {
17 | description = "The ARN of the Lambda function"
18 | value = module.notify_slack["develop"].notify_slack_lambda_function_arn
19 | }
20 |
21 | output "notify_slack_lambda_function_name" {
22 | description = "The name of the Lambda function"
23 | value = module.notify_slack["develop"].notify_slack_lambda_function_name
24 | }
25 |
26 | output "notify_slack_lambda_function_invoke_arn" {
27 | description = "The ARN to be used for invoking Lambda function from API Gateway"
28 | value = module.notify_slack["develop"].notify_slack_lambda_function_invoke_arn
29 | }
30 |
31 | output "notify_slack_lambda_function_last_modified" {
32 | description = "The date Lambda function was last modified"
33 | value = module.notify_slack["develop"].notify_slack_lambda_function_last_modified
34 | }
35 |
36 | output "notify_slack_lambda_function_version" {
37 | description = "Latest published version of your Lambda function"
38 | value = module.notify_slack["develop"].notify_slack_lambda_function_version
39 | }
40 |
--------------------------------------------------------------------------------
/examples/notify-slack-simple/main.tf:
--------------------------------------------------------------------------------
1 | provider "aws" {
2 | region = local.region
3 | }
4 |
5 | locals {
6 | name = "ex-${replace(basename(path.cwd), "_", "-")}"
7 | region = "eu-west-1"
8 | tags = {
9 | Owner = "user"
10 | Environment = "dev"
11 | }
12 | }
13 |
14 | ################################################################################
15 | # Supporting Resources
16 | ################################################################################
17 |
18 | resource "aws_sns_topic" "example" {
19 | name = local.name
20 | tags = local.tags
21 | }
22 |
23 | ################################################################################
24 | # Slack Notify Module
25 | ################################################################################
26 |
27 | module "notify_slack" {
28 | source = "../../"
29 |
30 | sns_topic_name = aws_sns_topic.example.name
31 | create_sns_topic = false
32 |
33 | slack_webhook_url = "https://hooks.slack.com/services/AAA/BBB/CCC"
34 | slack_channel = "aws-notification"
35 | slack_username = "reporter"
36 |
37 | tags = local.tags
38 | }
39 |
40 | ################################################################################
41 | # Integration Testing Support
42 | # This populates a file that is gitignored to aid in executing the integration tests locally
43 | ################################################################################
44 |
45 | resource "local_file" "integration_testing" {
46 | filename = "${path.module}/../../functions/.int.env"
47 | content = <<-EOT
48 | REGION=${local.region}
49 | LAMBDA_FUNCTION_NAME=${module.notify_slack.notify_slack_lambda_function_name}
50 | SNS_TOPIC_ARN=${aws_sns_topic.example.arn}
51 | EOT
52 | }
53 |
--------------------------------------------------------------------------------
/examples/notify-slack-simple/outputs.tf:
--------------------------------------------------------------------------------
1 | output "sns_topic_arn" {
2 | description = "The ARN of the SNS topic from which messages will be sent to Slack"
3 | value = module.notify_slack.slack_topic_arn
4 | }
5 |
6 | output "lambda_iam_role_arn" {
7 | description = "The ARN of the IAM role used by Lambda function"
8 | value = module.notify_slack.lambda_iam_role_arn
9 | }
10 |
11 | output "lambda_iam_role_name" {
12 | description = "The name of the IAM role used by Lambda function"
13 | value = module.notify_slack.lambda_iam_role_name
14 | }
15 |
16 | output "notify_slack_lambda_function_arn" {
17 | description = "The ARN of the Lambda function"
18 | value = module.notify_slack.notify_slack_lambda_function_arn
19 | }
20 |
21 | output "notify_slack_lambda_function_name" {
22 | description = "The name of the Lambda function"
23 | value = module.notify_slack.notify_slack_lambda_function_name
24 | }
25 |
26 | output "notify_slack_lambda_function_invoke_arn" {
27 | description = "The ARN to be used for invoking Lambda function from API Gateway"
28 | value = module.notify_slack.notify_slack_lambda_function_invoke_arn
29 | }
30 |
31 | output "notify_slack_lambda_function_last_modified" {
32 | description = "The date Lambda function was last modified"
33 | value = module.notify_slack.notify_slack_lambda_function_last_modified
34 | }
35 |
36 | output "notify_slack_lambda_function_version" {
37 | description = "Latest published version of your Lambda function"
38 | value = module.notify_slack.notify_slack_lambda_function_version
39 | }
40 |
41 | output "lambda_cloudwatch_log_group_arn" {
42 | description = "The Amazon Resource Name (ARN) specifying the log group"
43 | value = module.notify_slack.lambda_cloudwatch_log_group_arn
44 | }
45 |
--------------------------------------------------------------------------------
/outputs.tf:
--------------------------------------------------------------------------------
1 | output "slack_topic_arn" {
2 | description = "The ARN of the SNS topic from which messages will be sent to Slack"
3 | value = local.sns_topic_arn
4 | }
5 |
6 | # todo: Remove `this_slack_topic_arn` output during next major release 5.x
7 | output "this_slack_topic_arn" {
8 | description = "The ARN of the SNS topic from which messages will be sent to Slack (backward compatibility for version 4.x)"
9 | value = local.sns_topic_arn
10 | }
11 |
12 | output "lambda_iam_role_arn" {
13 | description = "The ARN of the IAM role used by Lambda function"
14 | value = module.lambda.lambda_role_arn
15 | }
16 |
17 | output "lambda_iam_role_name" {
18 | description = "The name of the IAM role used by Lambda function"
19 | value = module.lambda.lambda_role_name
20 | }
21 |
22 | output "notify_slack_lambda_function_arn" {
23 | description = "The ARN of the Lambda function"
24 | value = module.lambda.lambda_function_arn
25 | }
26 |
27 | output "notify_slack_lambda_function_name" {
28 | description = "The name of the Lambda function"
29 | value = module.lambda.lambda_function_name
30 | }
31 |
32 | output "notify_slack_lambda_function_invoke_arn" {
33 | description = "The ARN to be used for invoking Lambda function from API Gateway"
34 | value = module.lambda.lambda_function_invoke_arn
35 | }
36 |
37 | output "notify_slack_lambda_function_last_modified" {
38 | description = "The date Lambda function was last modified"
39 | value = module.lambda.lambda_function_last_modified
40 | }
41 |
42 | output "notify_slack_lambda_function_version" {
43 | description = "Latest published version of your Lambda function"
44 | value = module.lambda.lambda_function_version
45 | }
46 |
47 | output "lambda_cloudwatch_log_group_arn" {
48 | description = "The Amazon Resource Name (ARN) specifying the log group"
49 | value = try(aws_cloudwatch_log_group.lambda[0].arn, "")
50 | }
51 |
52 | output "sns_topic_feedback_role_arn" {
53 | description = "The Amazon Resource Name (ARN) of the IAM role used for SNS delivery status logging"
54 | value = local.sns_feedback_role
55 | }
56 |
--------------------------------------------------------------------------------
/.github/workflows/pr-title.yml:
--------------------------------------------------------------------------------
1 | name: 'Validate PR title'
2 |
3 | on:
4 | pull_request_target:
5 | types:
6 | - opened
7 | - edited
8 | - synchronize
9 |
10 | jobs:
11 | main:
12 | name: Validate PR title
13 | runs-on: ubuntu-latest
14 | steps:
15 | # Please look up the latest version from
16 | # https://github.com/amannn/action-semantic-pull-request/releases
17 | - uses: amannn/action-semantic-pull-request@v6.1.1
18 | env:
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20 | with:
21 | # Configure which types are allowed.
22 | # Default: https://github.com/commitizen/conventional-commit-types
23 | types: |
24 | fix
25 | feat
26 | docs
27 | ci
28 | chore
29 | # Configure that a scope must always be provided.
30 | requireScope: false
31 | # Configure additional validation for the subject based on a regex.
32 | # This example ensures the subject starts with an uppercase character.
33 | subjectPattern: ^[A-Z].+$
34 | # If `subjectPattern` is configured, you can use this property to override
35 | # the default error message that is shown when the pattern doesn't match.
36 | # The variables `subject` and `title` can be used within the message.
37 | subjectPatternError: |
38 | The subject "{subject}" found in the pull request title "{title}"
39 | didn't match the configured pattern. Please ensure that the subject
40 | starts with an uppercase character.
41 | # For work-in-progress PRs you can typically use draft pull requests
42 | # from Github. However, private repositories on the free plan don't have
43 | # this option and therefore this action allows you to opt-in to using the
44 | # special "[WIP]" prefix to indicate this state. This will avoid the
45 | # validation of the PR title and the pull request checks remain pending.
46 | # Note that a second check will be reported if this is enabled.
47 | wip: true
48 | # When using "Squash and merge" on a PR with only one commit, GitHub
49 | # will suggest using that commit message instead of the PR title for the
50 | # merge commit, and it's easy to commit this by mistake. Enable this option
51 | # to also validate the commit message for one commit PRs.
52 | validateSingleCommit: false
53 |
--------------------------------------------------------------------------------
/examples/cloudwatch-alerts-to-slack/main.tf:
--------------------------------------------------------------------------------
1 | provider "aws" {
2 | region = "eu-west-1"
3 | }
4 |
5 | resource "aws_kms_key" "this" {
6 | description = "KMS key for notify-slack test"
7 | }
8 |
9 | # Encrypt the URL, storing encryption here will show it in logs and in tfstate
10 | # https://www.terraform.io/docs/state/sensitive-data.html
11 | resource "aws_kms_ciphertext" "slack_url" {
12 | plaintext = "https://hooks.slack.com/services/AAA/BBB/CCC"
13 | key_id = aws_kms_key.this.arn
14 | }
15 |
16 | module "notify_slack" {
17 | source = "../../"
18 |
19 | for_each = toset([
20 | "develop",
21 | "release",
22 | "test",
23 | ])
24 |
25 | sns_topic_name = "slack-topic"
26 | enable_sns_topic_delivery_status_logs = true
27 |
28 | # Specify the ARN of the pre-defined feedback role or leave blank to have the module create it
29 | #sns_topic_lambda_feedback_role_arn = "arn:aws:iam::111122223333:role/sns-delivery-status"
30 |
31 | lambda_function_name = "notify_slack_${each.value}"
32 |
33 | slack_webhook_url = aws_kms_ciphertext.slack_url.ciphertext_blob
34 | slack_channel = "aws-notification"
35 | slack_username = "reporter"
36 |
37 | kms_key_arn = aws_kms_key.this.arn
38 |
39 | lambda_description = "Lambda function which sends notifications to Slack"
40 | log_events = true
41 |
42 | # VPC
43 | # lambda_function_vpc_subnet_ids = module.vpc.intra_subnets
44 | # lambda_function_vpc_security_group_ids = [module.vpc.default_security_group_id]
45 |
46 | tags = {
47 | Name = "cloudwatch-alerts-to-slack"
48 | }
49 | }
50 |
51 | resource "aws_cloudwatch_metric_alarm" "lambda_duration" {
52 | alarm_name = "NotifySlackDuration"
53 | comparison_operator = "GreaterThanOrEqualToThreshold"
54 | evaluation_periods = "1"
55 | metric_name = "Duration"
56 | namespace = "AWS/Lambda"
57 | period = "60"
58 | statistic = "Average"
59 | threshold = "5000"
60 | alarm_description = "Duration of notifying slack exceeds threshold"
61 |
62 | alarm_actions = [module.notify_slack["develop"].slack_topic_arn]
63 |
64 | dimensions = {
65 | FunctionName = module.notify_slack["develop"].notify_slack_lambda_function_name
66 | }
67 | }
68 |
69 | ######
70 | # VPC
71 | ######
72 | resource "random_pet" "this" {
73 | length = 2
74 | }
75 |
76 | module "vpc" {
77 | source = "terraform-aws-modules/vpc/aws"
78 |
79 | name = random_pet.this.id
80 | cidr = "10.10.0.0/16"
81 |
82 | azs = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
83 | intra_subnets = ["10.10.101.0/24", "10.10.102.0/24", "10.10.103.0/24"]
84 | }
85 |
--------------------------------------------------------------------------------
/functions/integration_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Integration Test
4 | ----------------
5 |
6 | Executes tests against live Slack webhook
7 |
8 | """
9 |
10 | import os
11 | from pprint import pprint
12 | from typing import List
13 |
14 | import boto3
15 | import pytest
16 |
17 |
18 | @pytest.mark.skip(reason="Execute with`pytest run python integration_test.py`")
19 | def _get_files(directory: str) -> List[str]:
20 | """
21 | Helper function to get list of files under `directory`
22 |
23 | :params directory: directory to pull list of files from
24 | :returns: list of files names under directory specified
25 | """
26 | return [
27 | os.path.join(directory, f)
28 | for f in os.listdir(directory)
29 | if os.path.isfile(os.path.join(directory, f))
30 | ]
31 |
32 |
33 | @pytest.mark.skip(reason="Execute with`pytest run python integration_test.py`")
34 | def invoke_lambda_handler():
35 | """
36 | Invoke lambda handler with sample SNS messages
37 |
38 | Messages should arrive at the live webhook specified
39 | """
40 | lambda_client = boto3.client("lambda", region_name=REGION)
41 |
42 | # These are SNS messages that invoke the lambda handler;
43 | # the event payload is in the `message` field
44 | messages = _get_files(directory="./messages")
45 |
46 | for message in messages:
47 | with open(message, "r") as mfile:
48 | msg = mfile.read()
49 | response = lambda_client.invoke(
50 | FunctionName=LAMBDA_FUNCTION_NAME,
51 | InvocationType="Event",
52 | Payload=msg,
53 | )
54 | pprint(response)
55 |
56 |
57 | @pytest.mark.skip(reason="Execute with`pytest run python integration_test.py`")
58 | def publish_event_to_sns_topic():
59 | """
60 | Publish sample events to SNS topic created
61 |
62 | Messages should arrive at the live webhook specified
63 | """
64 | sns_client = boto3.client("sns", region_name=REGION)
65 |
66 | # These are event payloads that will get published
67 | events = _get_files(directory="./events")
68 |
69 | for event in events:
70 | with open(event, "r") as efile:
71 | msg = efile.read()
72 | response = sns_client.publish(
73 | TopicArn=SNS_TOPIC_ARN,
74 | Message=msg,
75 | Subject=event,
76 | )
77 | pprint(response)
78 |
79 |
80 | if __name__ == "__main__":
81 | # Sourcing env vars set by `notify-slack-simple` example
82 | with open(".int.env", "r") as envvarfile:
83 | for line in envvarfile.readlines():
84 | (_var, _val) = line.strip().split("=")
85 | os.environ[_var] = _val
86 |
87 | # Not using .get() so it fails loudly if not set (`KeyError`)
88 | REGION = os.environ["REGION"]
89 | LAMBDA_FUNCTION_NAME = os.environ["LAMBDA_FUNCTION_NAME"]
90 | SNS_TOPIC_ARN = os.environ["SNS_TOPIC_ARN"]
91 |
92 | invoke_lambda_handler()
93 | publish_event_to_sns_topic()
94 |
--------------------------------------------------------------------------------
/examples/notify-slack-simple/README.md:
--------------------------------------------------------------------------------
1 | Basic Slack notification
2 | ========================
3 |
4 | Configuration in this directory creates an SNS topic that sends messages to a Slack channel.
5 |
6 | Note, this example does not use KMS key.
7 |
8 | Usage
9 | =====
10 |
11 | To run this example you need to execute:
12 |
13 | ```bash
14 | $ terraform init
15 | $ terraform plan
16 | $ terraform apply
17 | ```
18 |
19 | Note that this example may create resources which can cost money (AWS Elastic IP, for example). Run `terraform destroy` when you don't need these resources.
20 |
21 |
22 | ## Requirements
23 |
24 | | Name | Version |
25 | |------|---------|
26 | | [terraform](#requirement\_terraform) | >= 1.5.7 |
27 | | [aws](#requirement\_aws) | >= 6.0 |
28 | | [local](#requirement\_local) | >= 2.0 |
29 |
30 | ## Providers
31 |
32 | | Name | Version |
33 | |------|---------|
34 | | [aws](#provider\_aws) | >= 6.0 |
35 | | [local](#provider\_local) | >= 2.0 |
36 |
37 | ## Modules
38 |
39 | | Name | Source | Version |
40 | |------|--------|---------|
41 | | [custom\_lambda](#module\_custom\_lambda) | ../../ | n/a |
42 | | [notify\_slack](#module\_notify\_slack) | ../../ | n/a |
43 |
44 | ## Resources
45 |
46 | | Name | Type |
47 | |------|------|
48 | | [aws_sns_topic.custom_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource |
49 | | [aws_sns_topic.example](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource |
50 | | [local_file.integration_testing](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) | resource |
51 |
52 | ## Inputs
53 |
54 | No inputs.
55 |
56 | ## Outputs
57 |
58 | | Name | Description |
59 | |------|-------------|
60 | | [lambda\_cloudwatch\_log\_group\_arn](#output\_lambda\_cloudwatch\_log\_group\_arn) | The Amazon Resource Name (ARN) specifying the log group |
61 | | [lambda\_iam\_role\_arn](#output\_lambda\_iam\_role\_arn) | The ARN of the IAM role used by Lambda function |
62 | | [lambda\_iam\_role\_name](#output\_lambda\_iam\_role\_name) | The name of the IAM role used by Lambda function |
63 | | [notify\_slack\_lambda\_function\_arn](#output\_notify\_slack\_lambda\_function\_arn) | The ARN of the Lambda function |
64 | | [notify\_slack\_lambda\_function\_invoke\_arn](#output\_notify\_slack\_lambda\_function\_invoke\_arn) | The ARN to be used for invoking Lambda function from API Gateway |
65 | | [notify\_slack\_lambda\_function\_last\_modified](#output\_notify\_slack\_lambda\_function\_last\_modified) | The date Lambda function was last modified |
66 | | [notify\_slack\_lambda\_function\_name](#output\_notify\_slack\_lambda\_function\_name) | The name of the Lambda function |
67 | | [notify\_slack\_lambda\_function\_version](#output\_notify\_slack\_lambda\_function\_version) | Latest published version of your Lambda function |
68 | | [sns\_topic\_arn](#output\_sns\_topic\_arn) | The ARN of the SNS topic from which messages will be sent to Slack |
69 |
70 |
--------------------------------------------------------------------------------
/functions/messages/backup.json:
--------------------------------------------------------------------------------
1 | {
2 | "Records": [
3 | {
4 | "EventSource": "aws:sns",
5 | "EventVersion": "1.0",
6 | "EventSubscriptionArn": "arn:aws:sns:...-a3802aa1ed45",
7 | "Sns": {
8 | "Type": "Notification",
9 | "MessageId": "12345678-abcd-123a-def0-abcd1a234567",
10 | "TopicArn": "arn:aws:sns:us-west-1:123456789012:backup-2sqs-sns-topic",
11 | "Subject": "Notification from AWS Backup",
12 | "Message": "An AWS Backup job was completed successfully. Recovery point ARN: arn:aws:ec2:us-west-1:123456789012:volume/vol-012f345df6789012d. Resource ARN : arn:aws:ec2:us-west-1:123456789012:volume/vol-012f345df6789012e. BackupJob ID : 1b2345b2-f22c-4dab-5eb6-bbc7890ed123",
13 | "Timestamp": "2019-08-02T18:46:02.788Z",
14 | "MessageAttributes": {
15 | "EventType": {
16 | "Type": "String",
17 | "Value": "BACKUP_JOB"
18 | },
19 | "State": {
20 | "Type": "String",
21 | "Value": "COMPLETED"
22 | },
23 | "AccountId": {
24 | "Type": "String",
25 | "Value": "123456789012"
26 | },
27 | "Id": {
28 | "Type": "String",
29 | "Value": "1b2345b2-f22c-4dab-5eb6-bbc7890ed123"
30 | },
31 | "StartTime": {
32 | "Type": "String",
33 | "Value": "2019-09-02T13:48:52.226Z"
34 | }
35 | }
36 | }
37 | },
38 | {
39 | "EventSource": "aws:sns",
40 | "EventVersion": "1.0",
41 | "EventSubscriptionArn": "arn:aws:sns:...-a3802aa1ed45",
42 | "Sns": {
43 | "Type": "Notification",
44 | "MessageId": "12345678-abcd-123a-def0-abcd1a234567",
45 | "TopicArn": "arn:aws:sns:us-west-1:123456789012:backup-2sqs-sns-topic",
46 | "Subject": "Notification from AWS Backup",
47 | "Message": "An AWS Backup job failed. Resource ARN : arn:aws:ec2:us-west-1:123456789012:volume/vol-012f345df6789012e. BackupJob ID : 1b2345b2-f22c-4dab-5eb6-bbc7890ed123",
48 | "Timestamp": "2019-08-02T18:46:02.788Z",
49 | "MessageAttributes": {
50 | "EventType": {
51 | "Type": "String",
52 | "Value": "BACKUP_JOB"
53 | },
54 | "State": {
55 | "Type": "String",
56 | "Value": "FAILED"
57 | },
58 | "AccountId": {
59 | "Type": "String",
60 | "Value": "123456789012"
61 | },
62 | "Id": {
63 | "Type": "String",
64 | "Value": "1b2345b2-f22c-4dab-5eb6-bbc7890ed123"
65 | },
66 | "StartTime": {
67 | "Type": "String",
68 | "Value": "2019-09-02T13:48:52.226Z"
69 | }
70 | }
71 | }
72 | },
73 | {
74 | "EventSource": "aws:sns",
75 | "EventVersion": "1.0",
76 | "EventSubscriptionArn": "arn:aws:sns:...-a3802aa1ed45",
77 | "Sns": {
78 | "Type": "Notification",
79 | "MessageId": "12345678-abcd-123a-def0-abcd1a234567",
80 | "TopicArn": "arn:aws:sns:us-west-1:123456789012:backup-2sqs-sns-topic",
81 | "Subject": "Notification from AWS Backup",
82 | "Message": "An AWS Backup job failed to complete in time. Resource ARN : arn:aws:ec2:us-west-1:123456789012:volume/vol-012f345df6789012e. BackupJob ID : 1b2345b2-f22c-4dab-5eb6-bbc7890ed123",
83 | "Timestamp": "2019-08-02T18:46:02.788Z",
84 | "MessageAttributes": {
85 | "EventType": {
86 | "Type": "String",
87 | "Value": "BACKUP_JOB"
88 | },
89 | "State": {
90 | "Type": "String",
91 | "Value": "EXPIRED"
92 | },
93 | "AccountId": {
94 | "Type": "String",
95 | "Value": "123456789012"
96 | },
97 | "Id": {
98 | "Type": "String",
99 | "Value": "1b2345b2-f22c-4dab-5eb6-bbc7890ed123"
100 | },
101 | "StartTime": {
102 | "Type": "String",
103 | "Value": "2019-09-02T13:48:52.226Z"
104 | }
105 | }
106 | }
107 | }
108 | ]
109 | }
110 |
--------------------------------------------------------------------------------
/examples/cloudwatch-alerts-to-slack/README.md:
--------------------------------------------------------------------------------
1 | # CloudWatch alerts to Slack
2 |
3 | Configuration in this directory creates a VPC, an SNS topic that sends messages to a Slack channel with Slack webhook URL encrypted using KMS and a CloudWatch Alarm that monitors the duration of lambda execution.
4 |
5 | ## KMS keys
6 |
7 | There are 3 ways to define KMS key which should be used by Lambda function:
8 |
9 | 1. Create [aws_kms_key resource](https://www.terraform.io/docs/providers/aws/r/kms_key.html) and put ARN of it as `kms_key_arn` argument to this module
10 | 1. Use [aws_kms_alias data-source](https://www.terraform.io/docs/providers/aws/d/kms_alias.html) to get an existing KMS key alias and put ARN of it as `kms_key_arn` argument to this module
11 | 1. Hard-code the ARN of KMS key
12 |
13 | ### Option 1:
14 |
15 | ```hcl
16 | resource "aws_kms_key" "this" {
17 | description = "KMS key for notify-slack test"
18 | }
19 |
20 | resource "aws_kms_alias" "this" {
21 | name = "alias/kms-test-key"
22 | target_key_id = aws_kms_key.this.id
23 | }
24 |
25 | // kms_key_arn = aws_kms_key.this.arn
26 | ```
27 |
28 | ### Option 2:
29 |
30 | ```
31 | data "aws_kms_alias" "this" {
32 | name = "alias/kms-test-key"
33 | }
34 |
35 | // kms_key_arn = data.aws_kms_alias.this.target_key_arn
36 | ```
37 |
38 | ### Option 3:
39 |
40 | ```
41 | // kms_key_arn = "arn:aws:kms:eu-west-1:835367859851:key/054b4846-95fe-4537-94f2-1dfd255238cf"
42 | ```
43 |
44 | ## Usage
45 |
46 | To run this example you need to execute:
47 |
48 | ```bash
49 | $ terraform init
50 | $ terraform plan
51 | $ terraform apply
52 | ```
53 |
54 | Note that in practice, encryption of the Slack webhook URL should happen differently (outside of this module).
55 |
56 | Note that this example may create resources which can cost money. Run `terraform destroy` when you don't need these resources.
57 |
58 |
59 | ## Requirements
60 |
61 | | Name | Version |
62 | |------|---------|
63 | | [terraform](#requirement\_terraform) | >= 1.5.7 |
64 | | [aws](#requirement\_aws) | >= 6.0 |
65 | | [random](#requirement\_random) | >= 2.0 |
66 |
67 | ## Providers
68 |
69 | | Name | Version |
70 | |------|---------|
71 | | [aws](#provider\_aws) | >= 6.0 |
72 | | [random](#provider\_random) | >= 2.0 |
73 |
74 | ## Modules
75 |
76 | | Name | Source | Version |
77 | |------|--------|---------|
78 | | [notify\_slack](#module\_notify\_slack) | ../../ | n/a |
79 | | [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | n/a |
80 |
81 | ## Resources
82 |
83 | | Name | Type |
84 | |------|------|
85 | | [aws_cloudwatch_metric_alarm.lambda_duration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_metric_alarm) | resource |
86 | | [aws_kms_ciphertext.slack_url](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_ciphertext) | resource |
87 | | [aws_kms_key.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource |
88 | | [random_pet.this](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/pet) | resource |
89 |
90 | ## Inputs
91 |
92 | No inputs.
93 |
94 | ## Outputs
95 |
96 | | Name | Description |
97 | |------|-------------|
98 | | [lambda\_iam\_role\_arn](#output\_lambda\_iam\_role\_arn) | The ARN of the IAM role used by Lambda function |
99 | | [lambda\_iam\_role\_name](#output\_lambda\_iam\_role\_name) | The name of the IAM role used by Lambda function |
100 | | [notify\_slack\_lambda\_function\_arn](#output\_notify\_slack\_lambda\_function\_arn) | The ARN of the Lambda function |
101 | | [notify\_slack\_lambda\_function\_invoke\_arn](#output\_notify\_slack\_lambda\_function\_invoke\_arn) | The ARN to be used for invoking Lambda function from API Gateway |
102 | | [notify\_slack\_lambda\_function\_last\_modified](#output\_notify\_slack\_lambda\_function\_last\_modified) | The date Lambda function was last modified |
103 | | [notify\_slack\_lambda\_function\_name](#output\_notify\_slack\_lambda\_function\_name) | The name of the Lambda function |
104 | | [notify\_slack\_lambda\_function\_version](#output\_notify\_slack\_lambda\_function\_version) | Latest published version of your Lambda function |
105 | | [sns\_topic\_arn](#output\_sns\_topic\_arn) | The ARN of the SNS topic from which messages will be sent to Slack |
106 |
107 |
--------------------------------------------------------------------------------
/functions/notify_slack_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Slack Notification Test
4 | -----------------------
5 |
6 | Unit tests for `notify_slack.py`
7 |
8 | """
9 |
10 | import ast
11 | import os
12 |
13 | import notify_slack
14 | import pytest
15 |
16 |
17 | def test_sns_get_slack_message_payload_snapshots(snapshot, monkeypatch):
18 | """
19 | Compare outputs of get_slack_message_payload() with snapshots stored
20 |
21 | Run `pipenv run test:updatesnapshots` to update snapshot images
22 | """
23 |
24 | monkeypatch.setenv("SLACK_CHANNEL", "slack_testing_sandbox")
25 | monkeypatch.setenv("SLACK_USERNAME", "notify_slack_test")
26 | monkeypatch.setenv("SLACK_EMOJI", ":aws:")
27 |
28 | # These are SNS messages that invoke the lambda handler; the event payload is in the
29 | # `message` field
30 | _dir = "./messages"
31 | messages = [f for f in os.listdir(_dir) if os.path.isfile(os.path.join(_dir, f))]
32 |
33 | for file in messages:
34 | with open(os.path.join(_dir, file), "r") as ofile:
35 | event = ast.literal_eval(ofile.read())
36 |
37 | attachments = []
38 | # These are as delivered wrapped in an SNS message payload so we unpack
39 | for record in event["Records"]:
40 | sns = record["Sns"]
41 | subject = sns["Subject"]
42 | message = sns["Message"]
43 | region = sns["TopicArn"].split(":")[3]
44 |
45 | attachment = notify_slack.get_slack_message_payload(
46 | message=message, region=region, subject=subject
47 | )
48 | attachments.append(attachment)
49 |
50 | filename = os.path.basename(file)
51 | snapshot.assert_match(attachments, f"message_{filename}")
52 |
53 |
54 | def test_event_get_slack_message_payload_snapshots(snapshot, monkeypatch):
55 | """
56 | Compare outputs of get_slack_message_payload() with snapshots stored
57 |
58 | Run `pipenv run test:updatesnapshots` to update snapshot images
59 | """
60 |
61 | monkeypatch.setenv("SLACK_CHANNEL", "slack_testing_sandbox")
62 | monkeypatch.setenv("SLACK_USERNAME", "notify_slack_test")
63 | monkeypatch.setenv("SLACK_EMOJI", ":aws:")
64 |
65 | # These are just the raw events that will be converted to JSON string and
66 | # sent via SNS message
67 | _dir = "./events"
68 | events = [f for f in os.listdir(_dir) if os.path.isfile(os.path.join(_dir, f))]
69 |
70 | for file in events:
71 | with open(os.path.join(_dir, file), "r") as ofile:
72 | event = ast.literal_eval(ofile.read())
73 |
74 | attachment = notify_slack.get_slack_message_payload(
75 | message=event, region="us-east-1", subject="bar"
76 | )
77 | attachments = [attachment]
78 |
79 | filename = os.path.basename(file)
80 | snapshot.assert_match(attachments, f"event_{filename}")
81 |
82 |
83 | def test_environment_variables_set(monkeypatch):
84 | """
85 | Should pass since environment variables are provided
86 | """
87 |
88 | monkeypatch.setenv("SLACK_CHANNEL", "slack_testing_sandbox")
89 | monkeypatch.setenv("SLACK_USERNAME", "notify_slack_test")
90 | monkeypatch.setenv("SLACK_EMOJI", ":aws:")
91 | monkeypatch.setenv(
92 | "SLACK_WEBHOOK_URL", "https://hooks.slack.com/services/YOUR/WEBOOK/URL"
93 | )
94 |
95 | with open(os.path.join("./messages/text_message.json"), "r") as efile:
96 | event = ast.literal_eval(efile.read())
97 |
98 | for record in event["Records"]:
99 | sns = record["Sns"]
100 | subject = sns["Subject"]
101 | message = sns["Message"]
102 | region = sns["TopicArn"].split(":")[3]
103 |
104 | notify_slack.get_slack_message_payload(
105 | message=message, region=region, subject=subject
106 | )
107 |
108 |
109 | def test_environment_variables_missing():
110 | """
111 | Should pass since environment variables are NOT provided and
112 | will raise a `KeyError`
113 | """
114 | with pytest.raises(KeyError):
115 | # will raise before parsing/validation
116 | notify_slack.get_slack_message_payload(message={}, region="foo", subject="bar")
117 |
118 |
119 | @pytest.mark.parametrize(
120 | "region,service,expected",
121 | [
122 | (
123 | "us-east-1",
124 | "cloudwatch",
125 | "https://console.aws.amazon.com/cloudwatch/home?region=us-east-1",
126 | ),
127 | (
128 | "us-gov-east-1",
129 | "cloudwatch",
130 | "https://console.amazonaws-us-gov.com/cloudwatch/home?region=us-gov-east-1",
131 | ),
132 | (
133 | "us-east-1",
134 | "guardduty",
135 | "https://console.aws.amazon.com/guardduty/home?region=us-east-1",
136 | ),
137 | (
138 | "us-gov-east-1",
139 | "guardduty",
140 | "https://console.amazonaws-us-gov.com/guardduty/home?region=us-gov-east-1",
141 | ),
142 | ],
143 | )
144 | def test_get_service_url(region, service, expected):
145 | assert notify_slack.get_service_url(region=region, service=service) == expected
146 |
147 |
148 | def test_get_service_url_exception():
149 | """
150 | Should raise error since service is not defined in enum
151 | """
152 | with pytest.raises(KeyError):
153 | notify_slack.get_service_url(region="us-east-1", service="athena")
154 |
--------------------------------------------------------------------------------
/main.tf:
--------------------------------------------------------------------------------
1 | data "aws_caller_identity" "current" {}
2 | data "aws_partition" "current" {}
3 | data "aws_region" "current" {}
4 |
5 | locals {
6 | create = var.create && var.putin_khuylo
7 |
8 | sns_topic_arn = try(
9 | aws_sns_topic.this[0].arn,
10 | "arn:${data.aws_partition.current.id}:sns:${data.aws_region.current.region}:${data.aws_caller_identity.current.account_id}:${var.sns_topic_name}",
11 | ""
12 | )
13 |
14 | sns_feedback_role = local.create_sns_feedback_role ? aws_iam_role.sns_feedback_role[0].arn : var.sns_topic_lambda_feedback_role_arn
15 | lambda_policy_document = {
16 | sid = "AllowWriteToCloudwatchLogs"
17 | effect = "Allow"
18 | actions = ["logs:CreateLogStream", "logs:PutLogEvents"]
19 | resources = [replace("${try(aws_cloudwatch_log_group.lambda[0].arn, "")}:*", ":*:*", ":*")]
20 | }
21 |
22 | lambda_policy_document_kms = {
23 | sid = "AllowKMSDecrypt"
24 | effect = "Allow"
25 | actions = ["kms:Decrypt"]
26 | resources = [var.kms_key_arn]
27 | }
28 |
29 | lambda_policy_document_securityhub = {
30 | sid = "AllowSecurityHub"
31 | effect = "Allow"
32 | actions = ["securityhub:BatchUpdateFindings"]
33 | resources = ["*"]
34 | }
35 |
36 | lambda_handler = try(split(".", basename(var.lambda_source_path))[0], "notify_slack")
37 | }
38 |
39 | data "aws_iam_policy_document" "lambda" {
40 | count = var.create ? 1 : 0
41 |
42 | dynamic "statement" {
43 | for_each = concat([local.lambda_policy_document,
44 | local.lambda_policy_document_securityhub], var.kms_key_arn != "" ? [local.lambda_policy_document_kms] : [])
45 | content {
46 | sid = statement.value.sid
47 | effect = statement.value.effect
48 | actions = statement.value.actions
49 | resources = statement.value.resources
50 | }
51 | }
52 | }
53 |
54 | resource "aws_cloudwatch_log_group" "lambda" {
55 | count = var.create ? 1 : 0
56 |
57 | name = "/aws/lambda/${var.lambda_function_name}"
58 | retention_in_days = var.cloudwatch_log_group_retention_in_days
59 | kms_key_id = var.cloudwatch_log_group_kms_key_id
60 |
61 | tags = merge(var.tags, var.cloudwatch_log_group_tags)
62 | }
63 |
64 | resource "aws_sns_topic" "this" {
65 | count = var.create_sns_topic && var.create ? 1 : 0
66 |
67 | name = var.sns_topic_name
68 |
69 | kms_master_key_id = var.sns_topic_kms_key_id
70 |
71 | lambda_failure_feedback_role_arn = var.enable_sns_topic_delivery_status_logs ? local.sns_feedback_role : null
72 | lambda_success_feedback_role_arn = var.enable_sns_topic_delivery_status_logs ? local.sns_feedback_role : null
73 | lambda_success_feedback_sample_rate = var.enable_sns_topic_delivery_status_logs ? var.sns_topic_lambda_feedback_sample_rate : null
74 |
75 | tags = merge(var.tags, var.sns_topic_tags)
76 | }
77 |
78 |
79 | resource "aws_sns_topic_subscription" "sns_notify_slack" {
80 | count = var.create ? 1 : 0
81 |
82 | topic_arn = local.sns_topic_arn
83 | protocol = "lambda"
84 | endpoint = module.lambda.lambda_function_arn
85 | filter_policy = var.subscription_filter_policy
86 | filter_policy_scope = var.subscription_filter_policy_scope
87 | }
88 |
89 | module "lambda" {
90 | source = "terraform-aws-modules/lambda/aws"
91 | version = "8.0.1"
92 |
93 | create = var.create
94 |
95 | function_name = var.lambda_function_name
96 | description = var.lambda_description
97 |
98 | hash_extra = var.hash_extra
99 | handler = "${local.lambda_handler}.lambda_handler"
100 | source_path = var.lambda_source_path != null ? "${path.root}/${var.lambda_source_path}" : "${path.module}/functions/notify_slack.py"
101 | recreate_missing_package = var.recreate_missing_package
102 | runtime = var.runtime
103 | architectures = var.architectures
104 | timeout = 30
105 | kms_key_arn = var.kms_key_arn
106 | reserved_concurrent_executions = var.reserved_concurrent_executions
107 | ephemeral_storage_size = var.lambda_function_ephemeral_storage_size
108 | trigger_on_package_timestamp = var.trigger_on_package_timestamp
109 |
110 | # If publish is disabled, there will be "Error adding new Lambda Permission for notify_slack:
111 | # InvalidParameterValueException: We currently do not support adding policies for $LATEST."
112 | publish = true
113 |
114 | environment_variables = {
115 | SLACK_WEBHOOK_URL = var.slack_webhook_url
116 | SLACK_CHANNEL = var.slack_channel
117 | SLACK_USERNAME = var.slack_username
118 | SLACK_EMOJI = var.slack_emoji
119 | LOG_EVENTS = var.log_events ? "True" : "False"
120 | LOG_LEVEL = var.log_level
121 | }
122 |
123 | create_role = var.lambda_role == ""
124 | lambda_role = var.lambda_role
125 | role_name = "${var.iam_role_name_prefix}-${var.lambda_function_name}"
126 | role_permissions_boundary = var.iam_role_boundary_policy_arn
127 | role_tags = var.iam_role_tags
128 | role_path = var.iam_role_path
129 |
130 | # Do not use Lambda's policy for cloudwatch logs, because we have to add a policy
131 | # for KMS conditionally. This way attach_policy_json is always true independenty of
132 | # the value of presense of KMS. Famous "computed values in count" bug...
133 | attach_cloudwatch_logs_policy = false
134 | attach_policy_json = true
135 | policy_json = try(data.aws_iam_policy_document.lambda[0].json, "")
136 |
137 | use_existing_cloudwatch_log_group = true
138 | attach_network_policy = var.lambda_function_vpc_subnet_ids != null
139 |
140 | dead_letter_target_arn = var.lambda_dead_letter_target_arn
141 | attach_dead_letter_policy = var.lambda_attach_dead_letter_policy
142 |
143 | allowed_triggers = {
144 | AllowExecutionFromSNS = {
145 | principal = "sns.amazonaws.com"
146 | source_arn = local.sns_topic_arn
147 | }
148 | }
149 |
150 | store_on_s3 = var.lambda_function_store_on_s3
151 | s3_bucket = var.lambda_function_s3_bucket
152 |
153 | vpc_subnet_ids = var.lambda_function_vpc_subnet_ids
154 | vpc_security_group_ids = var.lambda_function_vpc_security_group_ids
155 |
156 | tags = merge(var.tags, var.lambda_function_tags)
157 |
158 | depends_on = [aws_cloudwatch_log_group.lambda]
159 | }
160 |
--------------------------------------------------------------------------------
/functions/README.md:
--------------------------------------------------------------------------------
1 | # Slack Notify Lambda Functions
2 |
3 | ## Conventions
4 |
5 | The following tools and conventions are used within this project:
6 |
7 | - [pipenv](https://github.com/pypa/pipenv) for managing Python dependencies and development virtualenv
8 | - [flake8](https://github.com/PyCQA/flake8) & [radon](https://github.com/rubik/radon) for linting and static code analysis
9 | - [isort](https://github.com/timothycrosley/isort) for import statement formatting
10 | - [black](https://github.com/ambv/black) for code formatting
11 | - [mypy](https://github.com/python/mypy) for static type checking
12 | - [pytest](https://github.com/pytest-dev/pytest) and [snapshottest](https://github.com/syrusakbary/snapshottest) for unit testing and snapshot testing
13 |
14 | ## Getting Started
15 |
16 | The following instructions will help you get setup for local development and testing purposes.
17 |
18 | ### Prerequisites
19 |
20 | #### [Pipenv](https://github.com/pypa/pipenv)
21 |
22 | Pipenv is used to help manage the python dependencies and local virtualenv for local testing and development. To install `pipenv` please refer to the project [installation documentation](https://github.com/pypa/pipenv#installation).
23 |
24 | Install the projects Python dependencies (with development dependencies) locally by running the following command.
25 |
26 | ```bash
27 | $ pipenv install --dev
28 | ```
29 |
30 | If you add/change/modify any of the Pipfile dependencies, you can update your local virtualenv using:
31 |
32 | ```bash
33 | $ pipenv update
34 | ```
35 |
36 | ### Testing
37 |
38 | #### Sample Payloads
39 |
40 | In the `functions/` directory there are two folders that contain sample message payloads used for testing and validation:
41 |
42 | 1. `functions/events/` contains raw events as provided by AWS. You can see a more in-depth list of example events in the (AWS documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html)
43 | 2. `functions/messages/` contains SNS message payloads in the form that they are delivered to the Slack notify lambda function. The `Message` attribute field is where the payload is stored that will be parsed and sent to Slack; this can be events like those described above in #1, or any string/stringified-JSON
44 |
45 | #### Unit Tests
46 |
47 | There are a number of pipenv scripts that are provided to aid in testing and ensuring the codebase is formatted properly.
48 |
49 | - `pipenv run test`: execute unit tests defined using pytest and show test coverage
50 | - `pipenv run lint`: show linting errors and static analysis of codebase
51 | - `pipenv run format`: auto-format codebase according to configurations provided
52 | - `pipenv run imports`: auto-format import statements according to configurations provided
53 | - `pipenv run typecheck`: show typecheck analysis of codebase
54 |
55 | See the `[scripts]` section of the `Pipfile` for the complete list of script commands.
56 |
57 | #### Snapshot Testing
58 |
59 | Snapshot testing is used to compare a set of given inputs to generated output snapshots to aid in unit testing. The tests are run in conjunction with the standard unit tests and the output will be shown in the cumulative output from `pipenv run test`. In theory, however, the snapshots themselves should not change unless:
60 |
61 | 1. The expected output of the message payload has changed
62 | 2. Event/message payloads have been added to or removed from the project
63 |
64 | When a change is required to update the snapshots, please do the following:
65 |
66 | 1. Update the snapshots by running:
67 |
68 | ```bash
69 | $ pipenv run test:updatesnapshots
70 | $ pipenv run format # this is necessary since the generated code follows its own style
71 | ```
72 |
73 | 2. Provide a clear reasoning within your pull request as to why the snapshots have changed
74 |
75 | #### Integration Tests
76 |
77 | Integration tests require setting up a live Slack webhook
78 |
79 | To run the unit tests:
80 |
81 | 1. Set up a dedicated slack channel as a test sandbox with it's own webhook. See [Slack Incoming Webhooks docs](https://api.slack.com/messaging/webhooks) for details.
82 | 2. From within the `examples/notify-slack-simple/` directory, update the `slack_*` variables to use your values:
83 |
84 | ```hcl
85 | slack_webhook_url = "https://hooks.slack.com/services/AAA/BBB/CCC"
86 | slack_channel = "aws-notification"
87 | slack_username = "reporter"
88 | ```
89 |
90 | 3. Deploy the resources in the `examples/notify-slack-simple/` project using Terraform
91 |
92 | ```bash
93 | $ terraform init && terraform apply -y
94 | ```
95 |
96 | 4. From within the `functions/` directory, execute the integration tests locally:
97 |
98 | ```bash
99 | $ pipenv run python integration_test.py
100 | ```
101 |
102 | Within the Slack channel that is associated to the webhook URL provided, you should see all of the messages arriving. You can compared the messages to the payloads in the `functions/events/` and `functions/messages` directories; there should be one Slack message per event payload/file.
103 |
104 | 5. Do not forget to clean up your provisioned resources by returning to the `example/notify-slack-simple/` directory and destroying using Terraform:
105 |
106 | ```bash
107 | $ terraform destroy -y
108 | ```
109 |
110 | ## Supporting Additional Events
111 |
112 | To add new events with custom message formatting, the general workflow will consist of (ignoring git actions for brevity):
113 |
114 | 1. Add a new example event paylod to the `functions/events/` directory; please name the file, using snake casing, in the form `_.json` such as `guardduty_finding.json` or `cloudwatch_alarm.json`
115 | 2. In the `functions/notify_slack.py` file, add the new formatting function, following a similar naming pattern like in step #1 where the function name is `format__()` such as `format_guardduty_finding()` or `format_cloudwatch_alarm()`
116 | 3. (Optional) Ff there are different "severity" type levels that are to be mapped to Slack message color bars, create an enum that maps the possible serverity values to the appropriate colors. See the `CloudWatchAlarmState` and `GuardDutyFindingSeverity` for examples. The enum name should follow pascal case, Python standard, in the form of ``
117 | 4. Update the snapshots to include your new event payload and expected output. Note - the other snapshots should not be affected by your change, the snapshot diff should only show your new event:
118 |
119 | ```bash
120 | $ pipenv run test:updatesnapshots
121 | $ pipenv run format # this is necessary since the generated code follows its own style
122 | ```
123 |
--------------------------------------------------------------------------------
/.github/workflows/pre-commit.yml:
--------------------------------------------------------------------------------
1 | name: Pre-Commit
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | - master
8 |
9 | env:
10 | TERRAFORM_DOCS_VERSION: v0.20.0
11 | TFLINT_VERSION: v0.59.1
12 |
13 | jobs:
14 | collectInputs:
15 | name: Collect workflow inputs
16 | runs-on: ubuntu-latest
17 | outputs:
18 | directories: ${{ steps.dirs.outputs.directories }}
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v5
22 |
23 | - name: Get root directories
24 | id: dirs
25 | uses: clowdhaus/terraform-composite-actions/directories@v1.14.0
26 |
27 | preCommitMinVersions:
28 | name: Min TF pre-commit
29 | needs: collectInputs
30 | runs-on: ubuntu-latest
31 | strategy:
32 | matrix:
33 | directory: ${{ fromJson(needs.collectInputs.outputs.directories) }}
34 | steps:
35 | - name: Install rmz
36 | uses: jaxxstorm/action-install-gh-release@v2.1.0
37 | with:
38 | repo: SUPERCILEX/fuc
39 | asset-name: x86_64-unknown-linux-gnu-rmz
40 | rename-to: rmz
41 | chmod: 0755
42 | extension-matching: disable
43 |
44 | # https://github.com/orgs/community/discussions/25678#discussioncomment-5242449
45 | - name: Delete unnecessary files
46 | run: |
47 | formatByteCount() { echo $(numfmt --to=iec-i --suffix=B --padding=7 $1'000'); }
48 | getAvailableSpace() { echo $(df -a $1 | awk 'NR > 1 {avail+=$4} END {print avail}'); }
49 |
50 | BEFORE=$(getAvailableSpace)
51 |
52 | ln -s /opt/hostedtoolcache/SUPERCILEX/x86_64-unknown-linux-gnu-rmz/latest/linux-x64/rmz /usr/local/bin/rmz
53 | rmz -f /opt/hostedtoolcache/CodeQL &
54 | rmz -f /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk &
55 | rmz -f /opt/hostedtoolcache/PyPy &
56 | rmz -f /opt/hostedtoolcache/Ruby &
57 | rmz -f /opt/hostedtoolcache/go &
58 |
59 | wait
60 |
61 | AFTER=$(getAvailableSpace)
62 | SAVED=$((AFTER-BEFORE))
63 | echo "=> Saved $(formatByteCount $SAVED)"
64 |
65 | - name: Checkout
66 | uses: actions/checkout@v5
67 |
68 | - name: Terraform min/max versions
69 | id: minMax
70 | uses: clowdhaus/terraform-min-max@v2.1.0
71 | with:
72 | directory: ${{ matrix.directory }}
73 |
74 | - name: Pre-commit Terraform ${{ steps.minMax.outputs.minVersion }}
75 | # Run only validate pre-commit check on min version supported
76 | if: ${{ matrix.directory != '.' }}
77 | uses: clowdhaus/terraform-composite-actions/pre-commit@v1.14.0
78 | with:
79 | terraform-version: ${{ steps.minMax.outputs.minVersion }}
80 | tflint-version: ${{ env.TFLINT_VERSION }}
81 | args: 'terraform_validate --color=always --show-diff-on-failure --files ${{ matrix.directory }}/*'
82 |
83 | - name: Pre-commit Terraform ${{ steps.minMax.outputs.minVersion }}
84 | # Run only validate pre-commit check on min version supported
85 | if: ${{ matrix.directory == '.' }}
86 | uses: clowdhaus/terraform-composite-actions/pre-commit@v1.14.0
87 | with:
88 | terraform-version: ${{ steps.minMax.outputs.minVersion }}
89 | tflint-version: ${{ env.TFLINT_VERSION }}
90 | args: 'terraform_validate --color=always --show-diff-on-failure --files $(ls *.tf)'
91 |
92 | preCommitMaxVersion:
93 | name: Max TF pre-commit
94 | runs-on: ubuntu-latest
95 | needs: collectInputs
96 | steps:
97 | - name: Install rmz
98 | uses: jaxxstorm/action-install-gh-release@v2.1.0
99 | with:
100 | repo: SUPERCILEX/fuc
101 | asset-name: x86_64-unknown-linux-gnu-rmz
102 | rename-to: rmz
103 | chmod: 0755
104 | extension-matching: disable
105 |
106 | # https://github.com/orgs/community/discussions/25678#discussioncomment-5242449
107 | - name: Delete unnecessary files
108 | run: |
109 | formatByteCount() { echo $(numfmt --to=iec-i --suffix=B --padding=7 $1'000'); }
110 | getAvailableSpace() { echo $(df -a $1 | awk 'NR > 1 {avail+=$4} END {print avail}'); }
111 |
112 | BEFORE=$(getAvailableSpace)
113 |
114 | ln -s /opt/hostedtoolcache/SUPERCILEX/x86_64-unknown-linux-gnu-rmz/latest/linux-x64/rmz /usr/local/bin/rmz
115 | rmz -f /opt/hostedtoolcache/CodeQL &
116 | rmz -f /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk &
117 | rmz -f /opt/hostedtoolcache/PyPy &
118 | rmz -f /opt/hostedtoolcache/Ruby &
119 | rmz -f /opt/hostedtoolcache/go &
120 | sudo rmz -f /usr/local/lib/android &
121 |
122 | if [[ ${{ github.repository }} == terraform-aws-modules/terraform-aws-security-group ]]; then
123 | sudo rmz -f /usr/share/dotnet &
124 | sudo rmz -f /usr/local/.ghcup &
125 | sudo apt-get -qq remove -y 'azure-.*'
126 | sudo apt-get -qq remove -y 'cpp-.*'
127 | sudo apt-get -qq remove -y 'dotnet-runtime-.*'
128 | sudo apt-get -qq remove -y 'google-.*'
129 | sudo apt-get -qq remove -y 'libclang-.*'
130 | sudo apt-get -qq remove -y 'libllvm.*'
131 | sudo apt-get -qq remove -y 'llvm-.*'
132 | sudo apt-get -qq remove -y 'mysql-.*'
133 | sudo apt-get -qq remove -y 'postgresql-.*'
134 | sudo apt-get -qq remove -y 'php.*'
135 | sudo apt-get -qq remove -y 'temurin-.*'
136 | sudo apt-get -qq remove -y kubectl firefox mono-devel
137 | sudo apt-get -qq autoremove -y
138 | sudo apt-get -qq clean
139 | fi
140 |
141 | wait
142 |
143 | AFTER=$(getAvailableSpace)
144 | SAVED=$((AFTER-BEFORE))
145 | echo "=> Saved $(formatByteCount $SAVED)"
146 |
147 | - name: Checkout
148 | uses: actions/checkout@v5
149 | with:
150 | ref: ${{ github.event.pull_request.head.ref }}
151 | repository: ${{github.event.pull_request.head.repo.full_name}}
152 |
153 | - name: Terraform min/max versions
154 | id: minMax
155 | uses: clowdhaus/terraform-min-max@v2.1.0
156 |
157 | - name: Hide template dir
158 | # Special to this repo, we don't want to check this dir
159 | if: ${{ github.repository == 'terraform-aws-modules/terraform-aws-security-group' }}
160 | run: rm -rf modules/_templates
161 |
162 | - name: Pre-commit Terraform ${{ steps.minMax.outputs.maxVersion }}
163 | uses: clowdhaus/terraform-composite-actions/pre-commit@v1.14.0
164 | with:
165 | terraform-version: ${{ steps.minMax.outputs.maxVersion }}
166 | tflint-version: ${{ env.TFLINT_VERSION }}
167 | terraform-docs-version: ${{ env.TERRAFORM_DOCS_VERSION }}
168 | install-hcledit: true
169 |
--------------------------------------------------------------------------------
/variables.tf:
--------------------------------------------------------------------------------
1 | variable "putin_khuylo" {
2 | description = "Do you agree that Putin doesn't respect Ukrainian sovereignty and territorial integrity? More info: https://en.wikipedia.org/wiki/Putin_khuylo!"
3 | type = bool
4 | default = true
5 | }
6 |
7 | variable "architectures" {
8 | description = "Instruction set architecture for your Lambda function. Valid values are [\"x86_64\"] and [\"arm64\"]."
9 | type = list(string)
10 | default = null
11 | }
12 |
13 | variable "create" {
14 | description = "Whether to create all resources"
15 | type = bool
16 | default = true
17 | }
18 |
19 | variable "create_sns_topic" {
20 | description = "Whether to create new SNS topic"
21 | type = bool
22 | default = true
23 | }
24 |
25 | variable "hash_extra" {
26 | description = "The string to add into hashing function. Useful when building same source path for different functions."
27 | type = string
28 | default = ""
29 | }
30 |
31 | variable "lambda_role" {
32 | description = "IAM role attached to the Lambda Function. If this is set then a role will not be created for you."
33 | type = string
34 | default = ""
35 | }
36 |
37 | variable "lambda_function_name" {
38 | description = "The name of the Lambda function to create"
39 | type = string
40 | default = "notify_slack"
41 | }
42 |
43 | variable "lambda_description" {
44 | description = "The description of the Lambda function"
45 | type = string
46 | default = null
47 | }
48 |
49 | variable "lambda_source_path" {
50 | description = "The source path of the custom Lambda function"
51 | type = string
52 | default = null
53 | }
54 |
55 | variable "lambda_dead_letter_target_arn" {
56 | description = "The ARN of an SNS topic or SQS queue to notify when an invocation fails."
57 | type = string
58 | default = null
59 | }
60 |
61 | variable "lambda_attach_dead_letter_policy" {
62 | description = "Controls whether SNS/SQS dead letter notification policy should be added to IAM role for Lambda Function"
63 | type = bool
64 | default = false
65 | }
66 |
67 | variable "sns_topic_name" {
68 | description = "The name of the SNS topic to create"
69 | type = string
70 | }
71 |
72 | variable "sns_topic_kms_key_id" {
73 | description = "ARN of the KMS key used for enabling SSE on the topic"
74 | type = string
75 | default = ""
76 | }
77 |
78 | variable "enable_sns_topic_delivery_status_logs" {
79 | description = "Whether to enable SNS topic delivery status logs"
80 | type = bool
81 | default = false
82 | }
83 |
84 | variable "sns_topic_lambda_feedback_role_arn" {
85 | description = "IAM role for SNS topic delivery status logs. If this is set then a role will not be created for you."
86 | type = string
87 | default = ""
88 | }
89 |
90 | variable "sns_topic_feedback_role_name" {
91 | description = "Name of the IAM role to use for SNS topic delivery status logging"
92 | type = string
93 | default = null
94 | }
95 |
96 | variable "sns_topic_feedback_role_description" {
97 | description = "Description of IAM role to use for SNS topic delivery status logging"
98 | type = string
99 | default = null
100 | }
101 |
102 | variable "sns_topic_feedback_role_path" {
103 | description = "Path of IAM role to use for SNS topic delivery status logging"
104 | type = string
105 | default = null
106 | }
107 |
108 | variable "sns_topic_feedback_role_force_detach_policies" {
109 | description = "Specifies to force detaching any policies the IAM role has before destroying it."
110 | type = bool
111 | default = true
112 | }
113 |
114 | variable "sns_topic_feedback_role_permissions_boundary" {
115 | description = "The ARN of the policy that is used to set the permissions boundary for the IAM role used by SNS topic delivery status logging"
116 | type = string
117 | default = null
118 | }
119 |
120 | variable "sns_topic_feedback_role_tags" {
121 | description = "A map of tags to assign to IAM the SNS topic feedback role"
122 | type = map(string)
123 | default = {}
124 | }
125 |
126 | variable "sns_topic_lambda_feedback_sample_rate" {
127 | description = "The percentage of successful deliveries to log"
128 | type = number
129 | default = 100
130 | }
131 |
132 | variable "slack_webhook_url" {
133 | description = "The URL of Slack webhook"
134 | type = string
135 | }
136 |
137 | variable "slack_channel" {
138 | description = "The name of the channel in Slack for notifications"
139 | type = string
140 | }
141 |
142 | variable "slack_username" {
143 | description = "The username that will appear on Slack messages"
144 | type = string
145 | }
146 |
147 | variable "slack_emoji" {
148 | description = "A custom emoji that will appear on Slack messages"
149 | type = string
150 | default = ":aws:"
151 | }
152 |
153 | variable "kms_key_arn" {
154 | description = "ARN of the KMS key used for decrypting slack webhook url"
155 | type = string
156 | default = ""
157 | }
158 |
159 | variable "recreate_missing_package" {
160 | description = "Whether to recreate missing Lambda package if it is missing locally or not"
161 | type = bool
162 | default = true
163 | }
164 |
165 | variable "log_events" {
166 | description = "Boolean flag to enabled/disable logging of incoming events"
167 | type = bool
168 | default = false
169 | }
170 |
171 | variable "log_level" {
172 | description = "Logging level for the Lambda function"
173 | type = string
174 | default = "INFO"
175 | }
176 |
177 | variable "reserved_concurrent_executions" {
178 | 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"
179 | type = number
180 | default = -1
181 | }
182 |
183 | variable "cloudwatch_log_group_retention_in_days" {
184 | description = "Specifies the number of days you want to retain log events in log group for Lambda."
185 | type = number
186 | default = 0
187 | }
188 |
189 | variable "cloudwatch_log_group_kms_key_id" {
190 | description = "The ARN of the KMS Key to use when encrypting log data for Lambda"
191 | type = string
192 | default = null
193 | }
194 |
195 | variable "tags" {
196 | description = "A map of tags to add to all resources"
197 | type = map(string)
198 | default = {}
199 | }
200 |
201 | variable "iam_role_tags" {
202 | description = "Additional tags for the IAM role"
203 | type = map(string)
204 | default = {}
205 | }
206 |
207 | variable "iam_role_boundary_policy_arn" {
208 | description = "The ARN of the policy that is used to set the permissions boundary for the role"
209 | type = string
210 | default = null
211 | }
212 |
213 | variable "iam_role_name_prefix" {
214 | description = "A unique role name beginning with the specified prefix"
215 | type = string
216 | default = "lambda"
217 | }
218 |
219 | variable "iam_role_path" {
220 | description = "Path of IAM role to use for Lambda Function"
221 | type = string
222 | default = null
223 | }
224 |
225 | variable "lambda_function_tags" {
226 | description = "Additional tags for the Lambda function"
227 | type = map(string)
228 | default = {}
229 | }
230 |
231 | variable "lambda_function_vpc_subnet_ids" {
232 | description = "List of subnet ids when Lambda Function should run in the VPC. Usually private or intra subnets."
233 | type = list(string)
234 | default = null
235 | }
236 |
237 | variable "lambda_function_vpc_security_group_ids" {
238 | description = "List of security group ids when Lambda Function should run in the VPC."
239 | type = list(string)
240 | default = null
241 | }
242 |
243 | variable "lambda_function_store_on_s3" {
244 | description = "Whether to store produced artifacts on S3 or locally."
245 | type = bool
246 | default = false
247 | }
248 |
249 | variable "lambda_function_s3_bucket" {
250 | description = "S3 bucket to store artifacts"
251 | type = string
252 | default = null
253 | }
254 |
255 | variable "lambda_function_ephemeral_storage_size" {
256 | description = "Amount of ephemeral storage (/tmp) in MB your Lambda Function can use at runtime. Valid value between 512 MB to 10,240 MB (10 GB)."
257 | type = number
258 | default = 512
259 | }
260 |
261 | variable "sns_topic_tags" {
262 | description = "Additional tags for the SNS topic"
263 | type = map(string)
264 | default = {}
265 | }
266 |
267 | variable "cloudwatch_log_group_tags" {
268 | description = "Additional tags for the Cloudwatch log group"
269 | type = map(string)
270 | default = {}
271 | }
272 |
273 | variable "subscription_filter_policy" {
274 | description = "(Optional) A valid filter policy that will be used in the subscription to filter messages seen by the target resource."
275 | type = string
276 | default = null
277 | }
278 |
279 | variable "subscription_filter_policy_scope" {
280 | description = "(Optional) A valid filter policy scope MessageAttributes|MessageBody"
281 | type = string
282 | default = null
283 | }
284 |
285 | variable "trigger_on_package_timestamp" {
286 | description = "(Optional) Whether or not to ignore the file timestamp when deciding to create the archive"
287 | type = bool
288 | default = false
289 | }
290 |
291 | variable "runtime" {
292 | description = "Lambda Function runtime"
293 | type = string
294 | default = "python3.13"
295 | }
296 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AWS Notify Slack Terraform module
2 |
3 | This module creates an SNS topic (or uses an existing one) and an AWS Lambda function that sends notifications to Slack using the [incoming webhooks API](https://api.slack.com/incoming-webhooks).
4 |
5 | Start by setting up an [incoming webhook integration](https://my.slack.com/services/new/incoming-webhook/) in your Slack workspace.
6 |
7 | Doing serverless with Terraform? Check out [serverless.tf framework](https://serverless.tf), which aims to simplify all operations when working with the serverless in Terraform.
8 |
9 | ## Supported Features
10 |
11 | - AWS Lambda runtime Python 3.13
12 | - Create new SNS topic or use existing one
13 | - Support plaintext and encrypted version of Slack webhook URL
14 | - Most of Slack message options are customizable
15 | - Custom Lambda function
16 | - Various event types are supported, even generic messages:
17 | - AWS CloudWatch Alarms
18 | - AWS CloudWatch LogMetrics Alarms
19 | - AWS GuardDuty Findings
20 | - AWS GuardDuty Malware Protection Object Scan Results
21 |
22 | ## Usage
23 |
24 | ```hcl
25 | module "notify_slack" {
26 | source = "terraform-aws-modules/notify-slack/aws"
27 | version = "~> 7.0"
28 |
29 | sns_topic_name = "slack-topic"
30 |
31 | slack_webhook_url = "https://hooks.slack.com/services/AAA/BBB/CCC"
32 | slack_channel = "aws-notification"
33 | slack_username = "reporter"
34 | }
35 | ```
36 |
37 | ## Using with Terraform Cloud Agents
38 |
39 | [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.
40 |
41 | This module requires Python 3.11. You can customize [tfc-agent](https://hub.docker.com/r/hashicorp/tfc-agent) to include Python using this sample `Dockerfile`:
42 |
43 | ```Dockerfile
44 | FROM hashicorp/tfc-agent:latest
45 | RUN apt-get -y update && apt-get -y install python3.11 python3-pip
46 | ENTRYPOINT ["/bin/tfc-agent"]
47 | ```
48 |
49 | ## Use existing SNS topic or create new
50 |
51 | 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`.
52 |
53 | ## Examples
54 |
55 | - [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.
56 | - [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.
57 |
58 | ## Local Development and Testing
59 |
60 | See the [functions](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/tree/master/functions) for further details.
61 |
62 |
63 | ## Requirements
64 |
65 | | Name | Version |
66 | |------|---------|
67 | | [terraform](#requirement\_terraform) | >= 1.5.7 |
68 | | [aws](#requirement\_aws) | >= 6.0 |
69 |
70 | ## Providers
71 |
72 | | Name | Version |
73 | |------|---------|
74 | | [aws](#provider\_aws) | >= 6.0 |
75 |
76 | ## Modules
77 |
78 | | Name | Source | Version |
79 | |------|--------|---------|
80 | | [lambda](#module\_lambda) | terraform-aws-modules/lambda/aws | 8.0.1 |
81 |
82 | ## Resources
83 |
84 | | Name | Type |
85 | |------|------|
86 | | [aws_cloudwatch_log_group.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource |
87 | | [aws_iam_role.sns_feedback_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
88 | | [aws_sns_topic.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource |
89 | | [aws_sns_topic_subscription.sns_notify_slack](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription) | resource |
90 | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
91 | | [aws_iam_policy_document.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
92 | | [aws_iam_policy_document.sns_feedback](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
93 | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source |
94 | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source |
95 |
96 | ## Inputs
97 |
98 | | Name | Description | Type | Default | Required |
99 | |------|-------------|------|---------|:--------:|
100 | | [architectures](#input\_architectures) | Instruction set architecture for your Lambda function. Valid values are ["x86\_64"] and ["arm64"]. | `list(string)` | `null` | no |
101 | | [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 |
102 | | [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 |
103 | | [cloudwatch\_log\_group\_tags](#input\_cloudwatch\_log\_group\_tags) | Additional tags for the Cloudwatch log group | `map(string)` | `{}` | no |
104 | | [create](#input\_create) | Whether to create all resources | `bool` | `true` | no |
105 | | [create\_sns\_topic](#input\_create\_sns\_topic) | Whether to create new SNS topic | `bool` | `true` | no |
106 | | [enable\_sns\_topic\_delivery\_status\_logs](#input\_enable\_sns\_topic\_delivery\_status\_logs) | Whether to enable SNS topic delivery status logs | `bool` | `false` | no |
107 | | [hash\_extra](#input\_hash\_extra) | The string to add into hashing function. Useful when building same source path for different functions. | `string` | `""` | no |
108 | | [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 |
109 | | [iam\_role\_name\_prefix](#input\_iam\_role\_name\_prefix) | A unique role name beginning with the specified prefix | `string` | `"lambda"` | no |
110 | | [iam\_role\_path](#input\_iam\_role\_path) | Path of IAM role to use for Lambda Function | `string` | `null` | no |
111 | | [iam\_role\_tags](#input\_iam\_role\_tags) | Additional tags for the IAM role | `map(string)` | `{}` | no |
112 | | [kms\_key\_arn](#input\_kms\_key\_arn) | ARN of the KMS key used for decrypting slack webhook url | `string` | `""` | no |
113 | | [lambda\_attach\_dead\_letter\_policy](#input\_lambda\_attach\_dead\_letter\_policy) | Controls whether SNS/SQS dead letter notification policy should be added to IAM role for Lambda Function | `bool` | `false` | no |
114 | | [lambda\_dead\_letter\_target\_arn](#input\_lambda\_dead\_letter\_target\_arn) | The ARN of an SNS topic or SQS queue to notify when an invocation fails. | `string` | `null` | no |
115 | | [lambda\_description](#input\_lambda\_description) | The description of the Lambda function | `string` | `null` | no |
116 | | [lambda\_function\_ephemeral\_storage\_size](#input\_lambda\_function\_ephemeral\_storage\_size) | Amount of ephemeral storage (/tmp) in MB your Lambda Function can use at runtime. Valid value between 512 MB to 10,240 MB (10 GB). | `number` | `512` | no |
117 | | [lambda\_function\_name](#input\_lambda\_function\_name) | The name of the Lambda function to create | `string` | `"notify_slack"` | no |
118 | | [lambda\_function\_s3\_bucket](#input\_lambda\_function\_s3\_bucket) | S3 bucket to store artifacts | `string` | `null` | no |
119 | | [lambda\_function\_store\_on\_s3](#input\_lambda\_function\_store\_on\_s3) | Whether to store produced artifacts on S3 or locally. | `bool` | `false` | no |
120 | | [lambda\_function\_tags](#input\_lambda\_function\_tags) | Additional tags for the Lambda function | `map(string)` | `{}` | no |
121 | | [lambda\_function\_vpc\_security\_group\_ids](#input\_lambda\_function\_vpc\_security\_group\_ids) | List of security group ids when Lambda Function should run in the VPC. | `list(string)` | `null` | no |
122 | | [lambda\_function\_vpc\_subnet\_ids](#input\_lambda\_function\_vpc\_subnet\_ids) | List of subnet ids when Lambda Function should run in the VPC. Usually private or intra subnets. | `list(string)` | `null` | no |
123 | | [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 |
124 | | [lambda\_source\_path](#input\_lambda\_source\_path) | The source path of the custom Lambda function | `string` | `null` | no |
125 | | [log\_events](#input\_log\_events) | Boolean flag to enabled/disable logging of incoming events | `bool` | `false` | no |
126 | | [log\_level](#input\_log\_level) | Logging level for the Lambda function | `string` | `"INFO"` | no |
127 | | [putin\_khuylo](#input\_putin\_khuylo) | Do you agree that Putin doesn't respect Ukrainian sovereignty and territorial integrity? More info: https://en.wikipedia.org/wiki/Putin_khuylo! | `bool` | `true` | no |
128 | | [recreate\_missing\_package](#input\_recreate\_missing\_package) | Whether to recreate missing Lambda package if it is missing locally or not | `bool` | `true` | no |
129 | | [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 |
130 | | [runtime](#input\_runtime) | Lambda Function runtime | `string` | `"python3.13"` | no |
131 | | [slack\_channel](#input\_slack\_channel) | The name of the channel in Slack for notifications | `string` | n/a | yes |
132 | | [slack\_emoji](#input\_slack\_emoji) | A custom emoji that will appear on Slack messages | `string` | `":aws:"` | no |
133 | | [slack\_username](#input\_slack\_username) | The username that will appear on Slack messages | `string` | n/a | yes |
134 | | [slack\_webhook\_url](#input\_slack\_webhook\_url) | The URL of Slack webhook | `string` | n/a | yes |
135 | | [sns\_topic\_feedback\_role\_description](#input\_sns\_topic\_feedback\_role\_description) | Description of IAM role to use for SNS topic delivery status logging | `string` | `null` | no |
136 | | [sns\_topic\_feedback\_role\_force\_detach\_policies](#input\_sns\_topic\_feedback\_role\_force\_detach\_policies) | Specifies to force detaching any policies the IAM role has before destroying it. | `bool` | `true` | no |
137 | | [sns\_topic\_feedback\_role\_name](#input\_sns\_topic\_feedback\_role\_name) | Name of the IAM role to use for SNS topic delivery status logging | `string` | `null` | no |
138 | | [sns\_topic\_feedback\_role\_path](#input\_sns\_topic\_feedback\_role\_path) | Path of IAM role to use for SNS topic delivery status logging | `string` | `null` | no |
139 | | [sns\_topic\_feedback\_role\_permissions\_boundary](#input\_sns\_topic\_feedback\_role\_permissions\_boundary) | The ARN of the policy that is used to set the permissions boundary for the IAM role used by SNS topic delivery status logging | `string` | `null` | no |
140 | | [sns\_topic\_feedback\_role\_tags](#input\_sns\_topic\_feedback\_role\_tags) | A map of tags to assign to IAM the SNS topic feedback role | `map(string)` | `{}` | no |
141 | | [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 |
142 | | [sns\_topic\_lambda\_feedback\_role\_arn](#input\_sns\_topic\_lambda\_feedback\_role\_arn) | IAM role for SNS topic delivery status logs. If this is set then a role will not be created for you. | `string` | `""` | no |
143 | | [sns\_topic\_lambda\_feedback\_sample\_rate](#input\_sns\_topic\_lambda\_feedback\_sample\_rate) | The percentage of successful deliveries to log | `number` | `100` | no |
144 | | [sns\_topic\_name](#input\_sns\_topic\_name) | The name of the SNS topic to create | `string` | n/a | yes |
145 | | [sns\_topic\_tags](#input\_sns\_topic\_tags) | Additional tags for the SNS topic | `map(string)` | `{}` | no |
146 | | [subscription\_filter\_policy](#input\_subscription\_filter\_policy) | (Optional) A valid filter policy that will be used in the subscription to filter messages seen by the target resource. | `string` | `null` | no |
147 | | [subscription\_filter\_policy\_scope](#input\_subscription\_filter\_policy\_scope) | (Optional) A valid filter policy scope MessageAttributes\|MessageBody | `string` | `null` | no |
148 | | [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no |
149 | | [trigger\_on\_package\_timestamp](#input\_trigger\_on\_package\_timestamp) | (Optional) Whether or not to ignore the file timestamp when deciding to create the archive | `bool` | `false` | no |
150 |
151 | ## Outputs
152 |
153 | | Name | Description |
154 | |------|-------------|
155 | | [lambda\_cloudwatch\_log\_group\_arn](#output\_lambda\_cloudwatch\_log\_group\_arn) | The Amazon Resource Name (ARN) specifying the log group |
156 | | [lambda\_iam\_role\_arn](#output\_lambda\_iam\_role\_arn) | The ARN of the IAM role used by Lambda function |
157 | | [lambda\_iam\_role\_name](#output\_lambda\_iam\_role\_name) | The name of the IAM role used by Lambda function |
158 | | [notify\_slack\_lambda\_function\_arn](#output\_notify\_slack\_lambda\_function\_arn) | The ARN of the Lambda function |
159 | | [notify\_slack\_lambda\_function\_invoke\_arn](#output\_notify\_slack\_lambda\_function\_invoke\_arn) | The ARN to be used for invoking Lambda function from API Gateway |
160 | | [notify\_slack\_lambda\_function\_last\_modified](#output\_notify\_slack\_lambda\_function\_last\_modified) | The date Lambda function was last modified |
161 | | [notify\_slack\_lambda\_function\_name](#output\_notify\_slack\_lambda\_function\_name) | The name of the Lambda function |
162 | | [notify\_slack\_lambda\_function\_version](#output\_notify\_slack\_lambda\_function\_version) | Latest published version of your Lambda function |
163 | | [slack\_topic\_arn](#output\_slack\_topic\_arn) | The ARN of the SNS topic from which messages will be sent to Slack |
164 | | [sns\_topic\_feedback\_role\_arn](#output\_sns\_topic\_feedback\_role\_arn) | The Amazon Resource Name (ARN) of the IAM role used for SNS delivery status logging |
165 | | [this\_slack\_topic\_arn](#output\_this\_slack\_topic\_arn) | The ARN of the SNS topic from which messages will be sent to Slack (backward compatibility for version 4.x) |
166 |
167 |
168 | ## Authors
169 |
170 | Module is maintained by [Anton Babenko](https://github.com/antonbabenko) with help from [these awesome contributors](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/graphs/contributors).
171 |
172 | ## License
173 |
174 | Apache 2 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/tree/master/LICENSE) for full details.
175 |
--------------------------------------------------------------------------------
/functions/snapshots/snap_notify_slack_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # snapshottest: v1 - https://goo.gl/zC4yUc
3 | from __future__ import unicode_literals
4 |
5 | from snapshottest import Snapshot
6 |
7 |
8 | snapshots = Snapshot()
9 |
10 | snapshots['test_event_get_slack_message_payload_snapshots event_aws_health_event.json'] = [
11 | {
12 | 'attachments': [
13 | {
14 | 'color': 'danger',
15 | 'fallback': 'New AWS Health Event for EC2',
16 | 'fields': [
17 | {
18 | 'short': True,
19 | 'title': 'Affected Service',
20 | 'value': '`EC2`'
21 | },
22 | {
23 | 'short': True,
24 | 'title': 'Affected Region',
25 | 'value': '`us-west-2`'
26 | },
27 | {
28 | 'short': False,
29 | 'title': 'Code',
30 | 'value': '`AWS_EC2_INSTANCE_STORE_DRIVE_PERFORMANCE_DEGRADED`'
31 | },
32 | {
33 | 'short': False,
34 | 'title': 'Event Description',
35 | 'value': '`A description of the event will be provided here`'
36 | },
37 | {
38 | 'short': False,
39 | 'title': 'Affected Resources',
40 | 'value': '`i-abcd1111`'
41 | },
42 | {
43 | 'short': True,
44 | 'title': 'Start Time',
45 | 'value': '`Sat, 05 Jun 2016 15:10:09 GMT`'
46 | },
47 | {
48 | 'short': True,
49 | 'title': 'End Time',
50 | 'value': '``'
51 | },
52 | {
53 | 'short': False,
54 | 'title': 'Link to Event',
55 | 'value': 'https://phd.aws.amazon.com/phd/home?region=us-west-2#/dashboard/open-issues'
56 | }
57 | ],
58 | 'text': 'New AWS Health Event for EC2'
59 | }
60 | ],
61 | 'channel': 'slack_testing_sandbox',
62 | 'icon_emoji': ':aws:',
63 | 'username': 'notify_slack_test'
64 | }
65 | ]
66 |
67 | snapshots['test_event_get_slack_message_payload_snapshots event_cloudwatch_alarm.json'] = [
68 | {
69 | 'attachments': [
70 | {
71 | 'color': 'danger',
72 | 'fallback': 'Alarm Example triggered',
73 | 'fields': [
74 | {
75 | 'short': True,
76 | 'title': 'Alarm Name',
77 | 'value': '`Example`'
78 | },
79 | {
80 | 'short': False,
81 | 'title': 'Alarm Description',
82 | 'value': '`Example alarm description.`'
83 | },
84 | {
85 | 'short': False,
86 | 'title': 'Alarm reason',
87 | 'value': '`Threshold Crossed`'
88 | },
89 | {
90 | 'short': True,
91 | 'title': 'Old State',
92 | 'value': '`OK`'
93 | },
94 | {
95 | 'short': True,
96 | 'title': 'Current State',
97 | 'value': '`ALARM`'
98 | },
99 | {
100 | 'short': False,
101 | 'title': 'Link to Alarm',
102 | 'value': 'https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#alarm:alarmFilter=ANY;name=Example'
103 | }
104 | ],
105 | 'text': 'AWS CloudWatch notification - Example'
106 | }
107 | ],
108 | 'channel': 'slack_testing_sandbox',
109 | 'icon_emoji': ':aws:',
110 | 'username': 'notify_slack_test'
111 | }
112 | ]
113 |
114 | snapshots['test_event_get_slack_message_payload_snapshots event_guardduty_finding_high.json'] = [
115 | {
116 | 'attachments': [
117 | {
118 | 'color': 'danger',
119 | 'fallback': 'GuardDuty Finding: SAMPLE Unprotected port on EC2 instance i-123123123 is being probed',
120 | 'fields': [
121 | {
122 | 'short': False,
123 | 'title': 'Description',
124 | 'value': '`EC2 instance has an unprotected port which is being probed by a known malicious host.`'
125 | },
126 | {
127 | 'short': False,
128 | 'title': 'Finding Type',
129 | 'value': '`Recon:EC2 PortProbeUnprotectedPort`'
130 | },
131 | {
132 | 'short': True,
133 | 'title': 'First Seen',
134 | 'value': '`2020-01-02T01:02:03Z`'
135 | },
136 | {
137 | 'short': True,
138 | 'title': 'Last Seen',
139 | 'value': '`2020-01-03T01:02:03Z`'
140 | },
141 | {
142 | 'short': True,
143 | 'title': 'Severity',
144 | 'value': '`High`'
145 | },
146 | {
147 | 'short': True,
148 | 'title': 'Account ID',
149 | 'value': '`123456789`'
150 | },
151 | {
152 | 'short': True,
153 | 'title': 'Count',
154 | 'value': '`1234`'
155 | },
156 | {
157 | 'short': False,
158 | 'title': 'Link to Finding',
159 | 'value': 'https://console.aws.amazon.com/guardduty/home?region=us-east-1#/findings?search=id%3Dsample-id-2'
160 | }
161 | ],
162 | 'text': 'AWS GuardDuty Finding - SAMPLE Unprotected port on EC2 instance i-123123123 is being probed'
163 | }
164 | ],
165 | 'channel': 'slack_testing_sandbox',
166 | 'icon_emoji': ':aws:',
167 | 'username': 'notify_slack_test'
168 | }
169 | ]
170 |
171 | snapshots['test_event_get_slack_message_payload_snapshots event_guardduty_finding_low.json'] = [
172 | {
173 | 'attachments': [
174 | {
175 | 'color': '#777777',
176 | 'fallback': 'GuardDuty Finding: SAMPLE Unprotected port on EC2 instance i-123123123 is being probed',
177 | 'fields': [
178 | {
179 | 'short': False,
180 | 'title': 'Description',
181 | 'value': '`EC2 instance has an unprotected port which is being probed by a known malicious host.`'
182 | },
183 | {
184 | 'short': False,
185 | 'title': 'Finding Type',
186 | 'value': '`Recon:EC2 PortProbeUnprotectedPort`'
187 | },
188 | {
189 | 'short': True,
190 | 'title': 'First Seen',
191 | 'value': '`2020-01-02T01:02:03Z`'
192 | },
193 | {
194 | 'short': True,
195 | 'title': 'Last Seen',
196 | 'value': '`2020-01-03T01:02:03Z`'
197 | },
198 | {
199 | 'short': True,
200 | 'title': 'Severity',
201 | 'value': '`Low`'
202 | },
203 | {
204 | 'short': True,
205 | 'title': 'Account ID',
206 | 'value': '`123456789`'
207 | },
208 | {
209 | 'short': True,
210 | 'title': 'Count',
211 | 'value': '`1234`'
212 | },
213 | {
214 | 'short': False,
215 | 'title': 'Link to Finding',
216 | 'value': 'https://console.aws.amazon.com/guardduty/home?region=us-east-1#/findings?search=id%3Dsample-id-2'
217 | }
218 | ],
219 | 'text': 'AWS GuardDuty Finding - SAMPLE Unprotected port on EC2 instance i-123123123 is being probed'
220 | }
221 | ],
222 | 'channel': 'slack_testing_sandbox',
223 | 'icon_emoji': ':aws:',
224 | 'username': 'notify_slack_test'
225 | }
226 | ]
227 |
228 | snapshots['test_event_get_slack_message_payload_snapshots event_guardduty_finding_medium.json'] = [
229 | {
230 | 'attachments': [
231 | {
232 | 'color': 'warning',
233 | 'fallback': 'GuardDuty Finding: SAMPLE Unprotected port on EC2 instance i-123123123 is being probed',
234 | 'fields': [
235 | {
236 | 'short': False,
237 | 'title': 'Description',
238 | 'value': '`EC2 instance has an unprotected port which is being probed by a known malicious host.`'
239 | },
240 | {
241 | 'short': False,
242 | 'title': 'Finding Type',
243 | 'value': '`Recon:EC2 PortProbeUnprotectedPort`'
244 | },
245 | {
246 | 'short': True,
247 | 'title': 'First Seen',
248 | 'value': '`2020-01-02T01:02:03Z`'
249 | },
250 | {
251 | 'short': True,
252 | 'title': 'Last Seen',
253 | 'value': '`2020-01-03T01:02:03Z`'
254 | },
255 | {
256 | 'short': True,
257 | 'title': 'Severity',
258 | 'value': '`Medium`'
259 | },
260 | {
261 | 'short': True,
262 | 'title': 'Account ID',
263 | 'value': '`123456789`'
264 | },
265 | {
266 | 'short': True,
267 | 'title': 'Count',
268 | 'value': '`1234`'
269 | },
270 | {
271 | 'short': False,
272 | 'title': 'Link to Finding',
273 | 'value': 'https://console.aws.amazon.com/guardduty/home?region=us-east-1#/findings?search=id%3Dsample-id-2'
274 | }
275 | ],
276 | 'text': 'AWS GuardDuty Finding - SAMPLE Unprotected port on EC2 instance i-123123123 is being probed'
277 | }
278 | ],
279 | 'channel': 'slack_testing_sandbox',
280 | 'icon_emoji': ':aws:',
281 | 'username': 'notify_slack_test'
282 | }
283 | ]
284 |
285 | snapshots['test_sns_get_slack_message_payload_snapshots message_backup.json'] = [
286 | {
287 | 'attachments': [
288 | {
289 | 'fields': [
290 | {
291 | 'title': '✅ An AWS Backup job was completed successfully'
292 | },
293 | {
294 | 'short': False,
295 | 'value': 'BackupJob ID'
296 | },
297 | {
298 | 'short': False,
299 | 'value': '`1b2345b2-f22c-4dab-5eb6-bbc7890ed123`'
300 | },
301 | {
302 | 'short': False,
303 | 'value': 'Resource ARN'
304 | },
305 | {
306 | 'short': False,
307 | 'value': '`arn:aws:ec2:us-west-1:123456789012:volume/vol-012f345df6789012e`'
308 | },
309 | {
310 | 'short': False,
311 | 'value': 'Recovery point ARN'
312 | },
313 | {
314 | 'short': False,
315 | 'value': '`arn:aws:ec2:us-west-1:123456789012:volume/vol-012f345df6789012d`'
316 | }
317 | ]
318 | }
319 | ],
320 | 'channel': 'slack_testing_sandbox',
321 | 'icon_emoji': ':aws:',
322 | 'username': 'notify_slack_test'
323 | },
324 | {
325 | 'attachments': [
326 | {
327 | 'fields': [
328 | {
329 | 'title': '⚠️ An AWS Backup job failed'
330 | },
331 | {
332 | 'short': False,
333 | 'value': 'BackupJob ID'
334 | },
335 | {
336 | 'short': False,
337 | 'value': '`1b2345b2-f22c-4dab-5eb6-bbc7890ed123`'
338 | },
339 | {
340 | 'short': False,
341 | 'value': 'Resource ARN'
342 | },
343 | {
344 | 'short': False,
345 | 'value': '`arn:aws:ec2:us-west-1:123456789012:volume/vol-012f345df6789012e`'
346 | }
347 | ]
348 | }
349 | ],
350 | 'channel': 'slack_testing_sandbox',
351 | 'icon_emoji': ':aws:',
352 | 'username': 'notify_slack_test'
353 | },
354 | {
355 | 'attachments': [
356 | {
357 | 'fields': [
358 | {
359 | 'title': '⚠️ An AWS Backup job failed to complete in time'
360 | },
361 | {
362 | 'short': False,
363 | 'value': 'BackupJob ID'
364 | },
365 | {
366 | 'short': False,
367 | 'value': '`1b2345b2-f22c-4dab-5eb6-bbc7890ed123`'
368 | },
369 | {
370 | 'short': False,
371 | 'value': 'Resource ARN'
372 | },
373 | {
374 | 'short': False,
375 | 'value': '`arn:aws:ec2:us-west-1:123456789012:volume/vol-012f345df6789012e`'
376 | }
377 | ]
378 | }
379 | ],
380 | 'channel': 'slack_testing_sandbox',
381 | 'icon_emoji': ':aws:',
382 | 'username': 'notify_slack_test'
383 | }
384 | ]
385 |
386 | snapshots['test_sns_get_slack_message_payload_snapshots message_cloudwatch_alarm.json'] = [
387 | {
388 | 'attachments': [
389 | {
390 | 'color': 'good',
391 | 'fallback': 'Alarm DBMigrationRequired triggered',
392 | 'fields': [
393 | {
394 | 'short': True,
395 | 'title': 'Alarm Name',
396 | 'value': '`DBMigrationRequired`'
397 | },
398 | {
399 | 'short': False,
400 | 'title': 'Alarm Description',
401 | 'value': '`App is reporting "A JPA error occurred(Unable to build EntityManagerFactory)"`'
402 | },
403 | {
404 | 'short': False,
405 | 'title': 'Alarm reason',
406 | 'value': '`Threshold Crossed: 1 datapoint [1.0 (12/02/19 15:44:00)] was not less than the threshold (1.0).`'
407 | },
408 | {
409 | 'short': True,
410 | 'title': 'Old State',
411 | 'value': '`ALARM`'
412 | },
413 | {
414 | 'short': True,
415 | 'title': 'Current State',
416 | 'value': '`OK`'
417 | },
418 | {
419 | 'short': False,
420 | 'title': 'Link to Alarm',
421 | 'value': 'https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#alarm:alarmFilter=ANY;name=DBMigrationRequired'
422 | }
423 | ],
424 | 'text': 'AWS CloudWatch notification - DBMigrationRequired'
425 | }
426 | ],
427 | 'channel': 'slack_testing_sandbox',
428 | 'icon_emoji': ':aws:',
429 | 'username': 'notify_slack_test'
430 | }
431 | ]
432 |
433 | snapshots['test_sns_get_slack_message_payload_snapshots message_dms_notification.json'] = [
434 | {
435 | 'attachments': [
436 | {
437 | 'fallback': 'A new message',
438 | 'fields': [
439 | {
440 | 'short': True,
441 | 'title': 'Event Source',
442 | 'value': '`replication-task`'
443 | },
444 | {
445 | 'short': True,
446 | 'title': 'Event Time',
447 | 'value': '`2019-02-12 15:45:24.091`'
448 | },
449 | {
450 | 'short': False,
451 | 'title': 'Identifier Link',
452 | 'value': '`https://console.aws.amazon.com/dms/home?region=us-east-1#tasks:ids=hello-world`'
453 | },
454 | {
455 | 'short': True,
456 | 'title': 'SourceId',
457 | 'value': '`hello-world`'
458 | },
459 | {
460 | 'short': False,
461 | 'title': 'Event ID',
462 | 'value': '`http://docs.aws.amazon.com/dms/latest/userguide/CHAP_Events.html#DMS-EVENT-0079 `'
463 | },
464 | {
465 | 'short': False,
466 | 'title': 'Event Message',
467 | 'value': '`Replication task has stopped.`'
468 | }
469 | ],
470 | 'mrkdwn_in': [
471 | 'value'
472 | ],
473 | 'text': 'AWS notification',
474 | 'title': 'DMS Notification Message'
475 | }
476 | ],
477 | 'channel': 'slack_testing_sandbox',
478 | 'icon_emoji': ':aws:',
479 | 'username': 'notify_slack_test'
480 | }
481 | ]
482 |
483 | snapshots['test_sns_get_slack_message_payload_snapshots message_glue_notification.json'] = [
484 | {
485 | 'attachments': [
486 | {
487 | 'fallback': 'A new message',
488 | 'fields': [
489 | {
490 | 'short': True,
491 | 'title': 'version',
492 | 'value': '`0`'
493 | },
494 | {
495 | 'short': False,
496 | 'title': 'id',
497 | 'value': '`ad3c3da1-148c-d5da-9a6a-79f1bc9a8a2e`'
498 | },
499 | {
500 | 'short': True,
501 | 'title': 'detail-type',
502 | 'value': '`Glue Job State Change`'
503 | },
504 | {
505 | 'short': True,
506 | 'title': 'source',
507 | 'value': '`aws.glue`'
508 | },
509 | {
510 | 'short': True,
511 | 'title': 'account',
512 | 'value': '`000000000000`'
513 | },
514 | {
515 | 'short': True,
516 | 'title': 'time',
517 | 'value': '`2021-06-18T12:34:06Z`'
518 | },
519 | {
520 | 'short': True,
521 | 'title': 'region',
522 | 'value': '`us-east-2`'
523 | },
524 | {
525 | 'short': True,
526 | 'title': 'resources',
527 | 'value': '`[]`'
528 | },
529 | {
530 | 'short': False,
531 | 'title': 'detail',
532 | 'value': '`{"jobName": "test_job", "severity": "ERROR", "state": "FAILED", "jobRunId": "jr_ca2144d747b45ad412d3c66a1b6934b6b27aa252be9a21a95c54dfaa224a1925", "message": "SystemExit: 1"}`'
533 | }
534 | ],
535 | 'mrkdwn_in': [
536 | 'value'
537 | ],
538 | 'text': 'AWS notification',
539 | 'title': 'Message'
540 | }
541 | ],
542 | 'channel': 'slack_testing_sandbox',
543 | 'icon_emoji': ':aws:',
544 | 'username': 'notify_slack_test'
545 | }
546 | ]
547 |
548 | snapshots['test_sns_get_slack_message_payload_snapshots message_guardduty_finding.json'] = [
549 | {
550 | 'attachments': [
551 | {
552 | 'color': 'danger',
553 | 'fallback': 'GuardDuty Finding: SAMPLE Unprotected port on EC2 instance i-123123123 is being probed',
554 | 'fields': [
555 | {
556 | 'short': False,
557 | 'title': 'Description',
558 | 'value': '`EC2 instance has an unprotected port which is being probed by a known malicious host.`'
559 | },
560 | {
561 | 'short': False,
562 | 'title': 'Finding Type',
563 | 'value': '`Recon:EC2 PortProbeUnprotectedPort`'
564 | },
565 | {
566 | 'short': True,
567 | 'title': 'First Seen',
568 | 'value': '`2020-01-02T01:02:03Z`'
569 | },
570 | {
571 | 'short': True,
572 | 'title': 'Last Seen',
573 | 'value': '`2020-01-03T01:02:03Z`'
574 | },
575 | {
576 | 'short': True,
577 | 'title': 'Severity',
578 | 'value': '`High`'
579 | },
580 | {
581 | 'short': True,
582 | 'title': 'Account ID',
583 | 'value': '`123456789`'
584 | },
585 | {
586 | 'short': True,
587 | 'title': 'Count',
588 | 'value': '`1234`'
589 | },
590 | {
591 | 'short': False,
592 | 'title': 'Link to Finding',
593 | 'value': 'https://console.amazonaws-us-gov.com/guardduty/home?region=us-gov-east-1#/findings?search=id%3Dsample-id-2'
594 | }
595 | ],
596 | 'text': 'AWS GuardDuty Finding - SAMPLE Unprotected port on EC2 instance i-123123123 is being probed'
597 | }
598 | ],
599 | 'channel': 'slack_testing_sandbox',
600 | 'icon_emoji': ':aws:',
601 | 'username': 'notify_slack_test'
602 | }
603 | ]
604 |
605 | snapshots['test_sns_get_slack_message_payload_snapshots message_guardduty_malware_protection_object_scan_result.json'] = [
606 | {
607 | 'attachments': [
608 | {
609 | 'color': 'danger',
610 | 'fallback': 'GuardDuty Malware Scan Result: THREATS_FOUND',
611 | 'fields': [
612 | {
613 | 'short': False,
614 | 'title': 'S3 Bucket',
615 | 'value': '`amzn-s3-demo-bucket`'
616 | },
617 | {
618 | 'short': False,
619 | 'title': 'S3 Object',
620 | 'value': '`APKAEIBAERJR2EXAMPLE`'
621 | },
622 | {
623 | 'short': False,
624 | 'title': 'Link to S3 object',
625 | 'value': 'https://console.aws.amazon.com/s3/object/amzn-s3-demo-bucket?region=us-east-1&prefix=APKAEIBAERJR2EXAMPLE'
626 | }
627 | ],
628 | 'text': 'AWS GuardDuty Malware Scan Result - THREATS_FOUND'
629 | }
630 | ],
631 | 'channel': 'slack_testing_sandbox',
632 | 'icon_emoji': ':aws:',
633 | 'username': 'notify_slack_test'
634 | }
635 | ]
636 |
637 | snapshots['test_sns_get_slack_message_payload_snapshots message_text_message.json'] = [
638 | {
639 | 'attachments': [
640 | {
641 | 'fallback': 'A new message',
642 | 'fields': [
643 | {
644 | 'short': False,
645 | 'value': '''This
646 | is
647 | a typical multi-line
648 | message from SNS!
649 |
650 | Have a ~good~ amazing day! :)'''
651 | }
652 | ],
653 | 'mrkdwn_in': [
654 | 'value'
655 | ],
656 | 'text': 'AWS notification',
657 | 'title': 'All Fine'
658 | }
659 | ],
660 | 'channel': 'slack_testing_sandbox',
661 | 'icon_emoji': ':aws:',
662 | 'username': 'notify_slack_test'
663 | }
664 | ]
665 |
--------------------------------------------------------------------------------
/functions/notify_slack.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Notify Slack
4 | ------------
5 |
6 | Receives event payloads that are parsed and sent to Slack
7 |
8 | """
9 |
10 | import base64
11 | import json
12 | import logging
13 | import os
14 | import re
15 | import urllib.parse
16 | import urllib.request
17 | from enum import Enum
18 | from typing import Any, Dict, Optional, Union, cast
19 | from urllib.error import HTTPError
20 |
21 | import boto3
22 |
23 | # Set default region if not provided
24 | REGION = os.environ.get("AWS_REGION", "us-east-1")
25 |
26 | # Initialize logging
27 | logger = logging.getLogger()
28 | logger.setLevel(os.environ.get("LOG_LEVEL", "INFO"))
29 |
30 | # Create client so its cached/frozen between invocations
31 | KMS_CLIENT = boto3.client("kms", region_name=REGION)
32 |
33 | SECURITY_HUB_CLIENT = boto3.client('securityhub', region_name=REGION)
34 |
35 |
36 | class AwsService(Enum):
37 | """AWS service supported by function"""
38 |
39 | cloudwatch = "cloudwatch"
40 | guardduty = "guardduty"
41 | securityhub = "securityhub"
42 |
43 |
44 | def decrypt_url(encrypted_url: str) -> str:
45 | """Decrypt encrypted URL with KMS
46 |
47 | :param encrypted_url: URL to decrypt with KMS
48 | :returns: plaintext URL
49 | """
50 | try:
51 | decrypted_payload = KMS_CLIENT.decrypt(
52 | CiphertextBlob=base64.b64decode(encrypted_url)
53 | )
54 | return decrypted_payload["Plaintext"].decode()
55 | except Exception:
56 | logging.exception("Failed to decrypt URL with KMS")
57 | return ""
58 |
59 |
60 | def get_service_url(region: str, service: str) -> str:
61 | """Get the appropriate service URL for the region
62 |
63 | :param region: name of the AWS region
64 | :param service: name of the AWS service
65 | :returns: AWS console url formatted for the region and service provided
66 | """
67 | try:
68 | service_name = AwsService[service].value
69 | if region.startswith("us-gov-"):
70 | return f"https://console.amazonaws-us-gov.com/{service_name}/home?region={region}"
71 | else:
72 | return f"https://console.aws.amazon.com/{service_name}/home?region={region}"
73 |
74 | except KeyError:
75 | print(f"Service {service} is currently not supported")
76 | raise
77 |
78 |
79 | def get_s3_object_url(region: str, bucket: str, key: str) -> str:
80 | """Get the appropriate S3 object URL for the region
81 |
82 | :param region: name of the AWS region
83 | :param bucket: name of the S3 bucket
84 | :param key: key of the relevant S3 object
85 | :returns: AWS console url formatted for the region and bucket + key provided
86 | """
87 | if region.startswith("us-gov-"):
88 | return f"https://console.amazonaws-us-gov.com/s3/object/{bucket}?region={region}&prefix={key}"
89 | else:
90 | return f"https://console.aws.amazon.com/s3/object/{bucket}?region={region}&prefix={key}"
91 |
92 |
93 | class CloudWatchAlarmState(Enum):
94 | """Maps CloudWatch notification state to Slack message format color"""
95 |
96 | OK = "good"
97 | INSUFFICIENT_DATA = "warning"
98 | ALARM = "danger"
99 |
100 |
101 | def format_cloudwatch_alarm(message: Dict[str, Any], region: str) -> Dict[str, Any]:
102 | """Format CloudWatch alarm event into Slack message format
103 |
104 | :params message: SNS message body containing CloudWatch alarm event
105 | :region: AWS region where the event originated from
106 | :returns: formatted Slack message payload
107 | """
108 |
109 | cloudwatch_url = get_service_url(region=region, service="cloudwatch")
110 | alarm_name = message["AlarmName"]
111 |
112 | return {
113 | "color": CloudWatchAlarmState[message["NewStateValue"]].value,
114 | "fallback": f"Alarm {alarm_name} triggered",
115 | "fields": [
116 | {"title": "Alarm Name", "value": f"`{alarm_name}`", "short": True},
117 | {
118 | "title": "Alarm Description",
119 | "value": f"`{message['AlarmDescription']}`",
120 | "short": False,
121 | },
122 | {
123 | "title": "Alarm reason",
124 | "value": f"`{message['NewStateReason']}`",
125 | "short": False,
126 | },
127 | {
128 | "title": "Old State",
129 | "value": f"`{message['OldStateValue']}`",
130 | "short": True,
131 | },
132 | {
133 | "title": "Current State",
134 | "value": f"`{message['NewStateValue']}`",
135 | "short": True,
136 | },
137 | {
138 | "title": "Link to Alarm",
139 | "value": f"{cloudwatch_url}#alarm:alarmFilter=ANY;name={urllib.parse.quote(alarm_name)}",
140 | "short": False,
141 | },
142 | ],
143 | "text": f"AWS CloudWatch notification - {message['AlarmName']}",
144 | }
145 |
146 |
147 | def format_aws_security_hub(message: Dict[str, Any], region: str) -> Dict[str, Any]:
148 | """
149 | Format AWS Security Hub finding event into Slack message format
150 |
151 | :params message: SNS message body containing SecurityHub finding event
152 | :params region: AWS region where the event originated from
153 | :returns: formatted Slack message payload
154 | """
155 | service_url = get_service_url(region=region, service="securityhub")
156 | finding = message["detail"]["findings"][0]
157 |
158 | # Switch Status From New To Notified To Prevent Repeated Messages
159 | try:
160 | compliance_status = finding["Compliance"].get("Status", "UNKNOWN")
161 | workflow_status = finding["Workflow"].get("Status", "UNKNOWN")
162 | if compliance_status == "FAILED" and workflow_status == "NEW":
163 | notified = SECURITY_HUB_CLIENT.batch_update_findings(
164 | FindingIdentifiers=[{
165 | 'Id': finding.get('Id'),
166 | 'ProductArn': finding.get("ProductArn")
167 | }],
168 | Workflow={"Status": "NOTIFIED"}
169 | )
170 | logging.warning(f"Successfully updated finding status to NOTIFIED: {json.dumps(notified)}")
171 | except Exception as e:
172 | logging.error(f"Failed to update finding status: {str(e)}")
173 | pass
174 |
175 | if finding.get("ProductName") == "Inspector":
176 | severity = finding["Severity"].get("Label", "INFORMATIONAL")
177 | compliance_status = finding["Compliance"].get("Status", "UNKNOWN")
178 |
179 | Id = finding.get("Id", "No ID Provided")
180 | title = finding.get("Title", "No Title Provided")
181 | description = finding.get("Description", "No Description Provided")
182 | control_id = finding['ProductFields'].get('ControlId', 'N/A')
183 | control_url = service_url + f"#/controls/{control_id}"
184 | aws_account_id = finding.get('AwsAccountId', 'Unknown Account')
185 | first_observed = finding.get('FirstObservedAt', 'Unknown Date')
186 | last_updated = finding.get('UpdatedAt', 'Unknown Date')
187 | affected_resource = finding['Resources'][0].get('Id', 'Unknown Resource')
188 | remediation_url = finding.get("Remediation", {}).get("Recommendation", {}).get("Url", "#")
189 |
190 | finding_base_path = "#/findings?search=Id%3D%255Coperator%255C%253AEQUALS%255C%253A"
191 | double_encoded_id = urllib.parse.quote(urllib.parse.quote(Id, safe=''), safe='')
192 | finding_url = f"{service_url}{finding_base_path}{double_encoded_id}"
193 | generator_id = finding.get("GeneratorId", "Unknown Generator")
194 |
195 | color = SecurityHubSeverity.get(severity.upper(), SecurityHubSeverity.INFORMATIONAL).value
196 | if compliance_status == "PASSED":
197 | color = "#4BB543"
198 |
199 | slack_message = {
200 | "color": color,
201 | "fallback": f"Inspector Finding: {title}",
202 | "fields": [
203 | {"title": "Title", "value": f"`{title}`", "short": False},
204 | {"title": "Description", "value": f"`{description}`", "short": False},
205 | {"title": "Compliance Status", "value": f"`{compliance_status}`", "short": True},
206 | {"title": "Severity", "value": f"`{severity}`", "short": True},
207 | {"title": "Control ID", "value": f"`{control_id}`", "short": True},
208 | {"title": "Account ID", "value": f"`{aws_account_id}`", "short": True},
209 | {"title": "First Observed", "value": f"`{first_observed}`", "short": True},
210 | {"title": "Last Updated", "value": f"`{last_updated}`", "short": True},
211 | {"title": "Affected Resource", "value": f"`{affected_resource}`", "short": False},
212 | {"title": "Generator", "value": f"`{generator_id}`", "short": False},
213 | {"title": "Control Url", "value": f"`{control_url}`", "short": False},
214 | {"title": "Finding Url", "value": f"`{finding_url}`", "short": False},
215 | {"title": "Remediation", "value": f"`{remediation_url}`", "short": False},
216 | ],
217 | "text": f"AWS Inspector Finding - {title}",
218 | }
219 |
220 | return slack_message
221 |
222 | if finding.get("ProductName") == "Security Hub":
223 | severity = finding["Severity"].get("Label", "INFORMATIONAL")
224 | compliance_status = finding["Compliance"].get("Status", "UNKNOWN")
225 |
226 | Id = finding.get("Id", "No ID Provided")
227 | title = finding.get("Title", "No Title Provided")
228 | description = finding.get("Description", "No Description Provided")
229 | control_id = finding['ProductFields'].get('ControlId', 'N/A')
230 | control_url = service_url + f"#/controls/{control_id}"
231 | aws_account_id = finding.get('AwsAccountId', 'Unknown Account')
232 | first_observed = finding.get('FirstObservedAt', 'Unknown Date')
233 | last_updated = finding.get('UpdatedAt', 'Unknown Date')
234 | affected_resource = finding['Resources'][0].get('Id', 'Unknown Resource')
235 | remediation_url = finding.get("Remediation", {}).get("Recommendation", {}).get("Url", "#")
236 | generator_id = finding.get("GeneratorId", "Unknown Generator")
237 |
238 | finding_base_path = "#/findings?search=Id%3D%255Coperator%255C%253AEQUALS%255C%253A"
239 | double_encoded_id = urllib.parse.quote(urllib.parse.quote(Id, safe=''), safe='')
240 | finding_url = f"{service_url}{finding_base_path}{double_encoded_id}"
241 |
242 | color = SecurityHubSeverity.get(severity.upper(), SecurityHubSeverity.INFORMATIONAL).value
243 | if compliance_status == "PASSED":
244 | color = "#4BB543"
245 |
246 | slack_message = {
247 | "color": color,
248 | "fallback": f"Security Hub Finding: {title}",
249 | "fields": [
250 | {"title": "Title", "value": f"`{title}`", "short": False},
251 | {"title": "Description", "value": f"`{description}`", "short": False},
252 | {"title": "Compliance Status", "value": f"`{compliance_status}`", "short": True},
253 | {"title": "Severity", "value": f"`{severity}`", "short": True},
254 | {"title": "Control ID", "value": f"`{control_id}`", "short": True},
255 | {"title": "Account ID", "value": f"`{aws_account_id}`", "short": True},
256 | {"title": "First Observed", "value": f"`{first_observed}`", "short": True},
257 | {"title": "Last Updated", "value": f"`{last_updated}`", "short": True},
258 | {"title": "Affected Resource", "value": f"`{affected_resource}`", "short": False},
259 | {"title": "Generator", "value": f"`{generator_id}`", "short": False},
260 | {"title": "Control Url", "value": f"`{control_url}`", "short": False},
261 | {"title": "Finding Url", "value": f"`{finding_url}`", "short": False},
262 | {"title": "Remediation", "value": f"`{remediation_url}`", "short": False},
263 | ],
264 | "text": f"AWS Security Hub Finding - {title}",
265 | }
266 |
267 | return slack_message
268 |
269 | return format_default(message=message)
270 |
271 |
272 | class SecurityHubSeverity(Enum):
273 | """Maps Security Hub finding severity to Slack message format color"""
274 |
275 | CRITICAL = "danger"
276 | HIGH = "danger"
277 | MEDIUM = "warning"
278 | LOW = "#777777"
279 | INFORMATIONAL = "#439FE0"
280 |
281 | @staticmethod
282 | def get(name, default):
283 | try:
284 | return SecurityHubSeverity[name]
285 | except KeyError:
286 | return default
287 |
288 |
289 | class GuardDutyFindingSeverity(Enum):
290 | """Maps GuardDuty finding severity to Slack message format color"""
291 |
292 | Low = "#777777"
293 | Medium = "warning"
294 | High = "danger"
295 |
296 |
297 | def format_guardduty_finding(message: Dict[str, Any], region: str) -> Dict[str, Any]:
298 | """
299 | Format GuardDuty finding event into Slack message format
300 |
301 | :params message: SNS message body containing GuardDuty finding event
302 | :params region: AWS region where the event originated from
303 | :returns: formatted Slack message payload
304 | """
305 |
306 | guardduty_url = get_service_url(region=region, service="guardduty")
307 | detail = message["detail"]
308 | service = detail.get("service", {})
309 | severity_score = detail.get("severity")
310 |
311 | if severity_score < 4.0:
312 | severity = "Low"
313 | elif severity_score < 7.0:
314 | severity = "Medium"
315 | else:
316 | severity = "High"
317 |
318 | return {
319 | "color": GuardDutyFindingSeverity[severity].value,
320 | "fallback": f"GuardDuty Finding: {detail.get('title')}",
321 | "fields": [
322 | {
323 | "title": "Description",
324 | "value": f"`{detail['description']}`",
325 | "short": False,
326 | },
327 | {
328 | "title": "Finding Type",
329 | "value": f"`{detail['type']}`",
330 | "short": False,
331 | },
332 | {
333 | "title": "First Seen",
334 | "value": f"`{service['eventFirstSeen']}`",
335 | "short": True,
336 | },
337 | {
338 | "title": "Last Seen",
339 | "value": f"`{service['eventLastSeen']}`",
340 | "short": True,
341 | },
342 | {"title": "Severity", "value": f"`{severity}`", "short": True},
343 | {"title": "Account ID", "value": f"`{detail['accountId']}`", "short": True},
344 | {
345 | "title": "Count",
346 | "value": f"`{service['count']}`",
347 | "short": True,
348 | },
349 | {
350 | "title": "Link to Finding",
351 | "value": f"{guardduty_url}#/findings?search=id%3D{detail['id']}",
352 | "short": False,
353 | },
354 | ],
355 | "text": f"AWS GuardDuty Finding - {detail.get('title')}",
356 | }
357 |
358 |
359 | def format_guardduty_malware_protection_object_scan_result(message: Dict[str, Any], region: str) -> Dict[str, Any]:
360 | """
361 | Format GuardDuty Malware Protection Object Scan Result into Slack message format
362 |
363 | :params message: SNS message body containing GuardDuty Malware Protection Object Scan Result
364 | :params region: AWS region where the event originated from
365 | :returns: formatted Slack message payload
366 | """
367 |
368 | detail = message["detail"]
369 | scanResultDetails = detail.get("scanResultDetails")
370 | scanResultStatus = scanResultDetails.get("scanResultStatus")
371 |
372 | s3ObjectDetails = detail.get("s3ObjectDetails")
373 | s3_url = get_s3_object_url(region=region, bucket=s3ObjectDetails["bucketName"], key=s3ObjectDetails["objectKey"])
374 |
375 | severity = "High"
376 |
377 | if scanResultStatus == "NO_THREATS_FOUND":
378 | severity = "Low"
379 | elif scanResultStatus == "THREATS_FOUND":
380 | severity = "High"
381 | elif scanResultStatus == "UNSUPPORTED":
382 | severity = "Medium"
383 | elif scanResultStatus == "ACCESS_DENIED":
384 | severity = "Medium"
385 | elif scanResultStatus == "FAILED":
386 | severity = "Medium"
387 |
388 | return {
389 | "color": GuardDutyFindingSeverity[severity].value,
390 | "fallback": f"GuardDuty Malware Scan Result: {scanResultStatus}",
391 | "fields": [
392 | {
393 | "title": "S3 Bucket",
394 | "value": f"`{detail['s3ObjectDetails']['bucketName']}`",
395 | "short": False,
396 | },
397 | {
398 | "title": "S3 Object",
399 | "value": f"`{detail['s3ObjectDetails']['objectKey']}`",
400 | "short": False,
401 | },
402 | {
403 | "title": "Link to S3 object",
404 | "value": f"{s3_url}",
405 | "short": False,
406 | },
407 | ],
408 | "text": f"AWS GuardDuty Malware Scan Result - {scanResultStatus}",
409 | }
410 |
411 |
412 | class AwsHealthCategory(Enum):
413 | """Maps AWS Health eventTypeCategory to Slack message format color
414 |
415 | eventTypeCategory
416 | The category code of the event. The possible values are issue,
417 | accountNotification, and scheduledChange.
418 | """
419 |
420 | accountNotification = "#777777"
421 | scheduledChange = "warning"
422 | issue = "danger"
423 |
424 |
425 | def format_aws_health(message: Dict[str, Any], region: str) -> Dict[str, Any]:
426 | """
427 | Format AWS Health event into Slack message format
428 |
429 | :params message: SNS message body containing AWS Health event
430 | :params region: AWS region where the event originated from
431 | :returns: formatted Slack message payload
432 | """
433 |
434 | aws_health_url = (
435 | f"https://phd.aws.amazon.com/phd/home?region={region}#/dashboard/open-issues"
436 | )
437 | detail = message["detail"]
438 | resources = message.get("resources", "")
439 | service = detail.get("service", "")
440 |
441 | return {
442 | "color": AwsHealthCategory[detail["eventTypeCategory"]].value,
443 | "text": f"New AWS Health Event for {service}",
444 | "fallback": f"New AWS Health Event for {service}",
445 | "fields": [
446 | {"title": "Affected Service", "value": f"`{service}`", "short": True},
447 | {
448 | "title": "Affected Region",
449 | "value": f"`{message.get('region')}`",
450 | "short": True,
451 | },
452 | {
453 | "title": "Code",
454 | "value": f"`{detail.get('eventTypeCode')}`",
455 | "short": False,
456 | },
457 | {
458 | "title": "Event Description",
459 | "value": f"`{detail['eventDescription'][0]['latestDescription']}`",
460 | "short": False,
461 | },
462 | {
463 | "title": "Affected Resources",
464 | "value": f"`{', '.join(resources)}`",
465 | "short": False,
466 | },
467 | {
468 | "title": "Start Time",
469 | "value": f"`{detail.get('startTime', '')}`",
470 | "short": True,
471 | },
472 | {
473 | "title": "End Time",
474 | "value": f"`{detail.get('endTime', '')}`",
475 | "short": True,
476 | },
477 | {
478 | "title": "Link to Event",
479 | "value": f"{aws_health_url}",
480 | "short": False,
481 | },
482 | ],
483 | }
484 |
485 |
486 | def aws_backup_field_parser(message: str) -> Dict[str, str]:
487 | """
488 | Parser for AWS Backup event message. It extracts the fields from the message and returns a dictionary.
489 |
490 | :params message: message containing AWS Backup event
491 | :returns: dictionary containing the fields extracted from the message
492 | """
493 | # Order is somewhat important, working in reverse order of the message payload
494 | # to reduce right most matched values
495 | field_names = {
496 | "BackupJob ID": r"(BackupJob ID : ).*",
497 | "Resource ARN": r"(Resource ARN : ).*[.]",
498 | "Recovery point ARN": r"(Recovery point ARN: ).*[.]",
499 | }
500 | fields = {}
501 |
502 | for fname, freg in field_names.items():
503 | match = re.search(freg, message)
504 | if match:
505 | value = match.group(0).split(" ")[-1]
506 | fields[fname] = value.removesuffix(".")
507 |
508 | # Remove the matched field from the message
509 | message = message.replace(match.group(0), "")
510 |
511 | return fields
512 |
513 |
514 | def format_aws_backup(message: str) -> Dict[str, Any]:
515 | """
516 | Format AWS Backup event into Slack message format
517 |
518 | :params message: SNS message body containing AWS Backup event
519 | :returns: formatted Slack message payload
520 | """
521 |
522 | fields: list[Dict[str, Any]] = []
523 | attachments = {}
524 |
525 | title = message.split(".")[0]
526 |
527 | if "failed" in title:
528 | title = f"⚠️ {title}"
529 |
530 | if "completed" in title:
531 | title = f"✅ {title}"
532 |
533 | fields.append({"title": title})
534 |
535 | backup_fields = aws_backup_field_parser(message)
536 |
537 | for k, v in backup_fields.items():
538 | fields.append({"value": k, "short": False})
539 | fields.append({"value": f"`{v}`", "short": False})
540 |
541 | attachments["fields"] = fields # type: ignore
542 |
543 | return attachments
544 |
545 |
546 | def format_default(
547 | message: Union[str, Dict], subject: Optional[str] = None
548 | ) -> Dict[str, Any]:
549 | """
550 | Default formatter, converting event into Slack message format
551 |
552 | :params message: SNS message body containing message/event
553 | :returns: formatted Slack message payload
554 | """
555 |
556 | attachments = {
557 | "fallback": "A new message",
558 | "text": "AWS notification",
559 | "title": subject if subject else "Message",
560 | "mrkdwn_in": ["value"],
561 | }
562 | fields = []
563 |
564 | if type(message) is dict:
565 | for k, v in message.items():
566 | value = f"{json.dumps(v)}" if isinstance(v, (dict, list)) else str(v)
567 | fields.append({"title": k, "value": f"`{value}`", "short": len(value) < 25})
568 | else:
569 | fields.append({"value": message, "short": False})
570 |
571 | if fields:
572 | attachments["fields"] = fields # type: ignore
573 |
574 | return attachments
575 |
576 |
577 | def parse_notification(message: Dict[str, Any], subject: Optional[str], region: str) -> Optional[Dict]:
578 | """
579 | Parse notification message and format into Slack message payload
580 |
581 | :params message: SNS message body notification payload
582 | :params subject: Optional subject line for Slack notification
583 | :params region: AWS region where the event originated from
584 | :returns: Slack message payload
585 | """
586 | if "AlarmName" in message:
587 | return format_cloudwatch_alarm(message=message, region=region)
588 | if isinstance(message, Dict) and message.get("detail-type") == "GuardDuty Finding":
589 | return format_guardduty_finding(message=message, region=message["region"])
590 | if isinstance(message, Dict) and message.get("detail-type") == "GuardDuty Malware Protection Object Scan Result":
591 | return format_guardduty_malware_protection_object_scan_result(message=message, region=message["region"])
592 | if isinstance(message, Dict) and message.get("detail-type") == "Security Hub Findings - Imported":
593 | return format_aws_security_hub(message=message, region=message["region"])
594 | if isinstance(message, Dict) and message.get("detail-type") == "AWS Health Event":
595 | return format_aws_health(message=message, region=message["region"])
596 | if subject == "Notification from AWS Backup":
597 | return format_aws_backup(message=str(message))
598 | return format_default(message=message, subject=subject)
599 |
600 |
601 | def get_slack_message_payload(
602 | message: Union[str, Dict], region: str, subject: Optional[str] = None
603 | ) -> Dict:
604 | """
605 | Parse notification message and format into Slack message payload
606 |
607 | :params message: SNS message body notification payload
608 | :params region: AWS region where the event originated from
609 | :params subject: Optional subject line for Slack notification
610 | :returns: Slack message payload
611 | """
612 |
613 | slack_channel = os.environ["SLACK_CHANNEL"]
614 | slack_username = os.environ["SLACK_USERNAME"]
615 | slack_emoji = os.environ["SLACK_EMOJI"]
616 |
617 | payload = {
618 | "channel": slack_channel,
619 | "username": slack_username,
620 | "icon_emoji": slack_emoji,
621 | }
622 | attachment = None
623 |
624 | if isinstance(message, str):
625 | try:
626 | message = json.loads(message)
627 | except json.JSONDecodeError:
628 | logging.info("Not a structured payload, just a string message")
629 |
630 | message = cast(Dict[str, Any], message)
631 |
632 | if "attachments" in message or "text" in message:
633 | payload = {**payload, **message}
634 | else:
635 | attachment = parse_notification(message, subject, region)
636 |
637 | if attachment:
638 | payload["attachments"] = [attachment] # type: ignore
639 |
640 | return payload
641 |
642 |
643 | def send_slack_notification(payload: Dict[str, Any]) -> str:
644 | """
645 | Send notification payload to Slack
646 |
647 | :params payload: formatted Slack message payload
648 | :returns: response details from sending notification
649 | """
650 |
651 | slack_url = os.environ["SLACK_WEBHOOK_URL"]
652 | if not slack_url.startswith("http"):
653 | slack_url = decrypt_url(slack_url)
654 |
655 | data = urllib.parse.urlencode({"payload": json.dumps(payload)}).encode("utf-8")
656 | req = urllib.request.Request(slack_url)
657 |
658 | try:
659 | result = urllib.request.urlopen(req, data)
660 | return json.dumps({"code": result.getcode(), "info": result.info().as_string()})
661 |
662 | except HTTPError as e:
663 | logging.error(f"{e}: result")
664 | return json.dumps({"code": e.getcode(), "info": e.info().as_string()})
665 |
666 |
667 | def lambda_handler(event: Dict[str, Any], context: Dict[str, Any]) -> str:
668 | """
669 | Lambda function to parse notification events and forward to Slack
670 |
671 | :param event: lambda expected event object
672 | :param context: lambda expected context object
673 | :returns: none
674 | """
675 |
676 | if os.environ.get("LOG_EVENTS", "False") == "True":
677 | logging.info("Event logging enabled: %s", json.dumps(event))
678 |
679 | for record in event["Records"]:
680 | sns = record["Sns"]
681 | subject = sns["Subject"]
682 | message = sns["Message"]
683 | region = sns["TopicArn"].split(":")[3]
684 |
685 | payload = get_slack_message_payload(
686 | message=message, region=region, subject=subject
687 | )
688 | response = send_slack_notification(payload=payload)
689 |
690 | if json.loads(response)["code"] != 200:
691 | response_info = json.loads(response)["info"]
692 | logging.error(
693 | f"Error: received status `{response_info}` using event `{event}` and context `{context}`"
694 | )
695 |
696 | return response
697 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## [7.1.1](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v7.1.0...v7.1.1) (2025-10-21)
6 |
7 | ### Bug Fixes
8 |
9 | * Update CI workflow versions to latest ([#263](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/263)) ([43d3f5b](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/43d3f5bad5008e43a2d2188b1c98886356f2889d))
10 |
11 | ## [7.1.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v7.0.1...v7.1.0) (2025-09-19)
12 |
13 |
14 | ### Features
15 |
16 | * Add support for GuardDuty Malware Protection Object Scan Result events ([#262](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/262)) ([1013c35](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/1013c35f1446b2d03fd91556edfc68deddebdbbd))
17 |
18 | ## [7.0.1](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v7.0.0...v7.0.1) (2025-08-24)
19 |
20 |
21 | ### Bug Fixes
22 |
23 | * Ensure logger is initialized correctly and propagate `log_level` variable to `LOG_LEVEL` environment variable ([#261](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/261)) ([af4b9eb](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/af4b9eb39a76cacedba84feb4cbff1dbc6d3a2d0)), closes [#253](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/253)
24 |
25 | ## [7.0.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.7.0...v7.0.0) (2025-07-30)
26 |
27 |
28 | ### ⚠ BREAKING CHANGES
29 |
30 | * Upgrade min AWS provider and Terraform versions to 6.0 and 1.5.7 respectively (#256)
31 |
32 | ### Features
33 |
34 | * Upgrade min AWS provider and Terraform versions to 6.0 and 1.5.7 respectively ([#256](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/256)) ([2368d42](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/2368d423d5616f95373e078db6526ebc81c389bf))
35 |
36 | ## [6.7.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.6.0...v6.7.0) (2025-06-23)
37 |
38 |
39 | ### Features
40 |
41 | * Allow changing runtime variable ([#252](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/252)) ([3502e45](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/3502e45c3dc61fda6fbe6e2c69d79ce25780a34e))
42 |
43 | ## [6.6.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.5.2...v6.6.0) (2025-03-12)
44 |
45 |
46 | ### Features
47 |
48 | * Support for Security Hub ([#242](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/242)) ([3aef5ba](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/3aef5badc119568f8986eb4fea91b76aef99c4df))
49 |
50 | ## [6.5.2](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.5.1...v6.5.2) (2025-02-25)
51 |
52 |
53 | ### Bug Fixes
54 |
55 | * Modify logging for security inspector issue ([#249](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/249)) ([b3cd40f](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/b3cd40f6e90fa628e0481b5093d60a302e58f155))
56 |
57 | ## [6.5.1](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.5.0...v6.5.1) (2025-01-06)
58 |
59 |
60 | ### Bug Fixes
61 |
62 | * Reverts endpoint variable change from e95cde8 ([#240](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/240)) ([81e4b81](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/81e4b816f39c34fab8a5d78e8b854c43aed7dbd2))
63 | * Update CI workflow versions to latest ([#239](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/239)) ([50b951a](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/50b951a333ebab734c5afba984f0584fd1b43dd7))
64 |
65 | ## [6.5.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.4.1...v6.5.0) (2024-09-03)
66 |
67 |
68 | ### Features
69 |
70 | * Add variable to allow disabling the package timestamp trigger ([#233](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/233)) ([b3016e2](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/b3016e2f059ffa4ce12745acd4c131c3744faf44))
71 |
72 | ## [6.4.1](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.4.0...v6.4.1) (2024-09-03)
73 |
74 |
75 | ### Bug Fixes
76 |
77 | * Update `aws_sns_topic_subscription` endpoint to use qualified arn ([#231](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/231)) ([e95cde8](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/e95cde8acdaf221e74595daa2238b75f0682ea06)), closes [#230](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/230)
78 |
79 | ## [6.4.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.3.0...v6.4.0) (2024-04-24)
80 |
81 |
82 | ### Features
83 |
84 | * Improved AWS backup notification readability ([#222](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/222)) ([27d1c46](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/27d1c464f80708740d8d155e7cb11367b41bab6c))
85 |
86 | ## [6.3.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.2.0...v6.3.0) (2024-04-22)
87 |
88 |
89 | ### Features
90 |
91 | * Update Python lambda runtime from `3.8` to `3.11` ([#225](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/225)) ([b4ef4e4](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/b4ef4e45e9f3dafb774ccf62d9473b338de68f3f))
92 |
93 | ## [6.2.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.1.2...v6.2.0) (2024-04-22)
94 |
95 |
96 | ### Features
97 |
98 | * Added architecture variable ([#224](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/224)) ([1ae3ab7](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/1ae3ab7e084341e7a1fd3acccb15d2971020fce5))
99 |
100 | ## [6.1.2](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.1.1...v6.1.2) (2024-03-26)
101 |
102 |
103 | ### Bug Fixes
104 |
105 | * Correct assume role permissions for SNS service to assume IAM role ([#220](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/220)) ([dae0c0f](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/dae0c0f49d41cf98c5e31af7912ed406eea81c83))
106 |
107 | ## [6.1.1](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.1.0...v6.1.1) (2024-03-06)
108 |
109 |
110 | ### Bug Fixes
111 |
112 | * Update CI workflow versions to remove deprecated runtime warnings ([#218](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/218)) ([44edd19](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/44edd191bac2812951faea9562c685fbeeeefee8))
113 |
114 | ## [6.1.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v6.0.0...v6.1.0) (2023-12-11)
115 |
116 |
117 | ### Features
118 |
119 | * Expose `hash_extra` variable from Lambda module ([#211](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/211)) ([ee30bb3](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/ee30bb3f5c0da7c118c8c09fbb195a7c0e607f73))
120 |
121 | ## [6.0.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v5.6.0...v6.0.0) (2023-05-18)
122 |
123 |
124 | ### ⚠ BREAKING CHANGES
125 |
126 | * Added variable to filter body of message on SNS level and bumped min Terraform version to 1.0 (#196)
127 |
128 | ### Features
129 |
130 | * Added variable to filter body of message on SNS level and bumped min Terraform version to 1.0 ([#196](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/196)) ([ab660f7](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/ab660f7e86aec7a4f134036460b98eeb92c6c4c8))
131 |
132 | ## [5.6.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v5.5.0...v5.6.0) (2023-01-26)
133 |
134 |
135 | ### Features
136 |
137 | * Add account ID to GuardDuty event notification ([#187](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/187)) ([e3452b4](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/e3452b424a0e5ccdaf69935094e9fb7785fb315b))
138 |
139 | ## [5.5.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v5.4.1...v5.5.0) (2022-12-07)
140 |
141 |
142 | ### Features
143 |
144 | * Add SNS topic delivery status IAM role ([#178](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/178)) ([2863105](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/2863105fd6e07ea0f16500928242968c4b4873cb))
145 |
146 | ### [5.4.1](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v5.4.0...v5.4.1) (2022-11-07)
147 |
148 |
149 | ### Bug Fixes
150 |
151 | * Update CI configuration files to use latest version ([#181](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/181)) ([6ca4605](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/6ca4605be57c4dd17c3daf87867b6e98136b0914))
152 |
153 | ## [5.4.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v5.3.0...v5.4.0) (2022-10-21)
154 |
155 |
156 | ### Features
157 |
158 | * Add lambda dead-letter queue variables ([#180](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/180)) ([010aa89](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/010aa89147f91eeb95e7d842d90eccc3beac6265))
159 |
160 | ## [5.3.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v5.2.0...v5.3.0) (2022-06-17)
161 |
162 |
163 | ### Features
164 |
165 | * Added support for AWS Health events ([#170](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/170)) ([3d38bfa](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/3d38bfa524541a6497ebcc77051ef78253cc4a3e))
166 |
167 | ## [5.2.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v5.1.0...v5.2.0) (2022-06-14)
168 |
169 |
170 | ### Features
171 |
172 | * Added support for custom lambda function ([#172](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/172)) ([4a9d0b0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/4a9d0b02a9421ff52b392145aaa2aea0c7317a51))
173 |
174 | ## [5.1.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v5.0.0...v5.1.0) (2022-05-04)
175 |
176 |
177 | ### Features
178 |
179 | * Added ephemeral_storage_size variable ([#167](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/167)) ([c82299a](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/c82299aaec22f301c62f220d8446675647168ff4))
180 |
181 | ## [5.0.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.24.0...v5.0.0) (2022-03-31)
182 |
183 |
184 | ### ⚠ BREAKING CHANGES
185 |
186 | * - Update lambda module to 3.1.0 to support AWS provider version 4.8+
187 |
188 | ### Features
189 |
190 | * Update lambda module to 3.1.0 to support AWS provider version 4.8+ ([#166](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/166)) ([ea822a3](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/ea822a3dbd4ac24803385cabae43538c9a3b10f3))
191 |
192 | # [4.24.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.23.0...v4.24.0) (2021-12-14)
193 |
194 |
195 | ### Features
196 |
197 | * Revert incorrectly removed output this_slack_topic_arn ([#159](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/159)) ([24ec027](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/24ec027e1b6fe708eb4a6d7788a64d9452ecbfe0))
198 |
199 | # [4.23.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.22.0...v4.23.0) (2021-12-11)
200 |
201 |
202 | ### Features
203 |
204 | * add support for recreating package locally if not missing/not present ([#158](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/158)) ([912e11d](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/912e11dc38416650ac07e0762a5e469a030032bd))
205 |
206 | # [4.22.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.21.0...v4.22.0) (2021-12-10)
207 |
208 |
209 | ### Features
210 |
211 | * add lint and unit test workflow checks for pull requests ([#152](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/152)) ([d2675ec](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/d2675eca91f3ca4bc8b7a18912ae84b36b7922f1))
212 |
213 | # [4.21.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.20.0...v4.21.0) (2021-12-10)
214 |
215 |
216 | ### Features
217 |
218 | * Added policy path variable to lambda module IAM role policy ([#153](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/153)) ([b3179a9](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/b3179a9f025943da60daf39d3ce73e88ed57e9ba))
219 |
220 | # [4.20.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.19.0...v4.20.0) (2021-12-09)
221 |
222 |
223 | ### Features
224 |
225 | * Update lambda module and bump Terraform/AWS provider versions ([#151](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/151)) ([0a1fae8](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/0a1fae86060248353eea2ededad26f43774e500e))
226 |
227 | # [4.19.0](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.18.0...v4.19.0) (2021-12-09)
228 |
229 |
230 | ### Bug Fixes
231 |
232 | * update CI/CD process to enable auto-release workflow ([#149](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/149)) ([f7dd0a3](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/f7dd0a35d1c140a3465564740abe3579c9e12b48))
233 |
234 |
235 | ### Features
236 |
237 | * Added path input variable for lambda module IAM role ([#150](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/150)) ([fc0c120](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/commit/fc0c120bd379be65177745637ad402b46334cda5))
238 |
239 |
240 | ## [v4.18.0] - 2021-10-01
241 |
242 | - feat: Added support for GuardDuty Findings format ([#143](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/143))
243 |
244 |
245 |
246 | ## [v4.17.0] - 2021-06-28
247 |
248 | - feat: Allow custom attachement ([#123](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/123))
249 |
250 |
251 |
252 | ## [v4.16.0] - 2021-06-28
253 |
254 | - feat: add support for nested messages ([#142](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/142))
255 |
256 |
257 |
258 | ## [v4.15.0] - 2021-05-25
259 |
260 | - chore: Remove check boxes that don't render properly in module doc ([#140](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/140))
261 | - chore: update CI/CD to use stable `terraform-docs` release artifact and discoverable Apache2.0 license ([#138](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/138))
262 |
263 |
264 |
265 | ## [v4.14.0] - 2021-04-19
266 |
267 | - feat: Updated code to support Terraform 0.15 ([#136](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/136))
268 | - chore: update documentation and pin `terraform_docs` version to avoid future changes ([#134](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/134))
269 |
270 |
271 |
272 | ## [v4.13.0] - 2021-03-12
273 |
274 | - fix: use the current aws partition ([#133](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/133))
275 | - chore: align ci-cd static checks to use individual minimum Terraform versions ([#131](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/131))
276 | - fix: Remove data resource for sns topic to avoid race condition ([#81](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/81))
277 | - chore: add ci-cd workflow for pre-commit checks ([#128](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/128))
278 | - feat: Improve slack message formatting for generic messages ([#124](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/124))
279 | - chore: update documentation based on latest `terraform-docs` which includes module and resource sections ([#126](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/126))
280 | - feat: add support for GovCloud URLs ([#114](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/114))
281 | - feat: Allow Lambda function to be VPC bound ([#122](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/122))
282 | - feat: Updated version of terraform-aws-lambda module to 1.28.0 ([#119](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/119))
283 | - feat: Updated version of Terraform AWS Lambda module to support multiple copies ([#117](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/117))
284 | - fix: Typo on subscription_filter_policy variable ([#113](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/113))
285 | - docs: Added a note about using with Terraform Cloud Agents ([#108](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/108))
286 | - feat: allow reuse of existing lambda_role ([#85](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/85))
287 | - fix: Fix regression with aws_cloudwatch_log_group resource after upgrade of AWS provider 3.0 ([#106](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/106))
288 | - docs: Updated version of module to use for Terraform 0.12 users
289 | - fix: Updated version requirements to be Terraform 0.13 only ([#101](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/101))
290 | - feat: Updated Lambda module to work with Terraform 0.13 ([#99](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/99))
291 | - fix: Bump version of lambda module that supports Terraform 13 and AWS Provider 3.x ([#96](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/96))
292 |
293 |
294 |
295 | ## [v3.6.0] - 2021-03-01
296 |
297 | - fix: Fix regression with aws_cloudwatch_log_group resource after upgrade of AWS provider 3.0 ([#106](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/106)) ([#130](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/130))
298 | - feat: Updated version of Lambda module to allow AWS provider version 3
299 |
300 |
301 |
302 | ## [v4.12.0] - 2021-03-01
303 |
304 | - fix: Remove data resource for sns topic to avoid race condition ([#81](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/81))
305 | - chore: add ci-cd workflow for pre-commit checks ([#128](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/128))
306 |
307 |
308 |
309 | ## [v4.11.0] - 2021-02-21
310 |
311 | - feat: Improve slack message formatting for generic messages ([#124](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/124))
312 |
313 |
314 |
315 | ## [v4.10.0] - 2021-02-20
316 |
317 | - chore: update documentation based on latest `terraform-docs` which includes module and resource sections ([#126](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/126))
318 |
319 |
320 |
321 | ## [v4.9.0] - 2020-12-18
322 |
323 | - feat: add support for GovCloud URLs ([#114](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/114))
324 |
325 |
326 |
327 | ## [v4.8.0] - 2020-12-18
328 |
329 | - feat: Allow Lambda function to be VPC bound ([#122](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/122))
330 |
331 |
332 |
333 | ## [v4.7.0] - 2020-11-17
334 |
335 | - feat: Updated version of terraform-aws-lambda module to 1.28.0 ([#119](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/119))
336 |
337 |
338 |
339 | ## [v4.6.0] - 2020-11-05
340 |
341 | - feat: Updated version of Terraform AWS Lambda module to support multiple copies ([#117](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/117))
342 |
343 |
344 |
345 | ## [v4.5.0] - 2020-10-15
346 |
347 | - fix: Typo on subscription_filter_policy variable ([#113](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/113))
348 |
349 |
350 |
351 | ## [v4.4.0] - 2020-10-08
352 |
353 | - docs: Added a note about using with Terraform Cloud Agents ([#108](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/108))
354 |
355 |
356 |
357 | ## [v4.3.0] - 2020-09-07
358 |
359 | - feat: allow reuse of existing lambda_role ([#85](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/85))
360 |
361 |
362 |
363 | ## [v4.2.0] - 2020-09-07
364 |
365 | - fix: Fix regression with aws_cloudwatch_log_group resource after upgrade of AWS provider 3.0 ([#106](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/106))
366 | - docs: Updated version of module to use for Terraform 0.12 users
367 | - fix: Updated version requirements to be Terraform 0.13 only ([#101](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/101))
368 | - feat: Updated Lambda module to work with Terraform 0.13 ([#99](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/99))
369 | - fix: Bump version of lambda module that supports Terraform 13 and AWS Provider 3.x ([#96](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/96))
370 |
371 |
372 |
373 | ## [v3.5.0] - 2020-08-14
374 |
375 | - feat: Updated version of Lambda module to allow AWS provider version 3
376 |
377 |
378 |
379 | ## [v4.1.0] - 2020-08-14
380 |
381 | - fix: Updated version requirements to be Terraform 0.13 only ([#101](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/101))
382 |
383 |
384 |
385 | ## [v4.0.0] - 2020-08-13
386 |
387 | - feat: Updated Lambda module to work with Terraform 0.13 ([#99](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/99))
388 | - fix: Bump version of lambda module that supports Terraform 13 and AWS Provider 3.x ([#96](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/96))
389 |
390 |
391 |
392 | ## [v3.4.0] - 2020-08-13
393 |
394 | - feat: update required version of aws provider to allow 3.0 ([#95](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/95))
395 |
396 |
397 |
398 | ## [v3.3.0] - 2020-06-19
399 |
400 | - Updated README
401 | - feat: Add support for SSE on the topic ([#82](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/82))
402 |
403 |
404 |
405 | ## [v3.2.0] - 2020-06-11
406 |
407 | - feat: Updated version of Lambda module
408 |
409 |
410 |
411 | ## [v3.1.0] - 2020-06-10
412 |
413 | - fix: Upgraded version of Lambda module (fix [#84](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/84))
414 |
415 |
416 |
417 | ## [v3.0.0] - 2020-06-07
418 |
419 | - Updated pre-commit hooks
420 | - feat: Rewrote module to handle Lambda resources properly with terraform-aws-lambda module ([#83](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/83))
421 | - chore: Removed stale.yml from .github folder
422 | - fix: Stale bot should process only issues for now ([#79](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/79))
423 |
424 |
425 |
426 | ## [v2.15.0] - 2020-04-13
427 |
428 | - docs: Updated required versions in README
429 | - Add support fro IAM role boundary policy ([#61](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/61))
430 |
431 |
432 |
433 | ## [v2.14.0] - 2020-04-13
434 |
435 | - docs: Updated README after [#62](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/62)
436 | - feat: Add support for custom name prefixes for IAM role and policy ([#62](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/62))
437 | - fix: Move stale.yml to .github ([#78](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/78))
438 | - feat: Add Stale Bot Config ([#77](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/77))
439 |
440 |
441 |
442 | ## [v2.13.0] - 2020-03-19
443 |
444 |
445 |
446 |
447 | ## [v2.12.0] - 2020-03-19
448 |
449 |
450 |
451 |
452 | ## [v2.11.0] - 2020-03-19
453 |
454 | - Add subsription filter policy support ([#74](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/74))
455 |
456 |
457 |
458 | ## [v2.10.0] - 2020-01-21
459 |
460 | - Updated pre-commit-terraform with terraform-docs 0.8.0 support ([#65](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/65))
461 |
462 |
463 |
464 | ## [v2.9.0] - 2020-01-16
465 |
466 | - Fix empty tuple error ([#64](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/64))
467 |
468 |
469 |
470 | ## [v2.8.0] - 2019-12-21
471 |
472 | - Added lambda description and improved Lambda IAM policy for KMS ([#56](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/56))
473 |
474 |
475 |
476 | ## [v2.7.0] - 2019-12-20
477 |
478 | - Added support for multiline messages ([#55](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/55))
479 |
480 |
481 |
482 | ## [v2.6.0] - 2019-12-20
483 |
484 | - Added pytest and logging (based on [#27](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/27)) ([#54](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/54))
485 |
486 |
487 |
488 | ## [v2.5.0] - 2019-12-20
489 |
490 | - Updated formatting
491 | - use 0.12 syntax for depends_on ([#51](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/51))
492 |
493 |
494 |
495 | ## [v2.4.0] - 2019-12-10
496 |
497 | - Use urllib.parse.quote for the alarm name ([#35](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/35))
498 | - Updated simple example a bit
499 | - Create AWS Cloudwatch log group and give explicit access to it ([#40](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/40))
500 | - Added support for reserved_concurrent_executions
501 | - Updated docs, python3.7
502 | - Add support for resource tagging ([#45](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/45))
503 | - Upgraded module to support Terraform 0.12 ([#36](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/36))
504 |
505 |
506 |
507 | ## [v1.14.0] - 2019-11-08
508 |
509 | - Updated pre-commit hooks
510 | - Reduce scope of IAM Policy for CloudWatch Logs ([#44](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/44))
511 |
512 |
513 |
514 | ## [v2.3.0] - 2019-11-08
515 |
516 | - Create AWS Cloudwatch log group and give explicit access to it ([#40](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/40))
517 |
518 |
519 |
520 | ## [v2.2.0] - 2019-11-08
521 |
522 | - Added support for reserved_concurrent_executions
523 |
524 |
525 |
526 | ## [v2.1.0] - 2019-11-08
527 |
528 | - Updated docs, python3.7
529 | - Add support for resource tagging ([#45](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/45))
530 |
531 |
532 |
533 | ## [v2.0.0] - 2019-06-12
534 |
535 | - Upgraded module to support Terraform 0.12 ([#36](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/36))
536 |
537 |
538 |
539 | ## [v1.13.0] - 2019-02-22
540 |
541 | - need to convert from json string to dict when extracting message from event ([#30](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/30))
542 |
543 |
544 |
545 | ## [v1.12.0] - 2019-02-21
546 |
547 | - Pass the subject ot default_notification ([#29](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/29))
548 |
549 |
550 |
551 | ## [v1.11.0] - 2018-12-28
552 |
553 | - No longer parsing the SNS event as incoming JSON ([#23](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/23))
554 |
555 |
556 |
557 | ## [v1.10.0] - 2018-08-20
558 |
559 | - Fixed bug which causes apply failure when create = false ([#19](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/19))
560 |
561 |
562 |
563 | ## [v1.9.0] - 2018-06-21
564 |
565 | - Allow computed KMS key value (fixed [#10](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/10)) ([#18](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/18))
566 |
567 |
568 |
569 | ## [v1.8.0] - 2018-06-20
570 |
571 | - include short alarm name in slack notification text ([#14](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/14))
572 |
573 |
574 |
575 | ## [v1.7.0] - 2018-06-20
576 |
577 | - Renamed enable to create, minor fixes after [#15](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/15)
578 | - Add flag to enable/disable creation of resources ([#15](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/15))
579 |
580 |
581 |
582 | ## [v1.6.0] - 2018-06-19
583 |
584 | - Fixed formatting
585 | - Fix Lambda path in shared state ([#17](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/17))
586 | - Fixed spelling a bit
587 | - Cirumvent TF's path.module limitation for lambda filenames
588 | - Cirumvent TF's path.module limitation for lambda filenames
589 | - Cirumvent TF's path.module limitation for lambda filenames
590 |
591 |
592 |
593 | ## [v1.5.0] - 2018-06-06
594 |
595 | - Fixed formatting (ran 'pre-commit run -a')
596 | - Add in slack emoji support ([#11](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/11))
597 | - Update comments in examples/ about aws_kms_ciphertext ([#12](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/12))
598 |
599 |
600 |
601 | ## [v1.4.0] - 2018-06-05
602 |
603 | - Ignore `last_modified` timestamp deciding whether to do an update ([#9](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/9))
604 | - Updated formatting in examples
605 |
606 |
607 |
608 | ## [v1.3.0] - 2018-05-29
609 |
610 | - Ignore changes in filename (fixed [#6](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/6))
611 |
612 |
613 |
614 | ## [v1.2.0] - 2018-05-16
615 |
616 | - Added pre-commit hook to autogenerate terraform-docs ([#7](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/7))
617 |
618 |
619 |
620 | ## [v1.1.0] - 2018-03-22
621 |
622 | - Feature/lambda function name variable ([#5](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/5))
623 |
624 |
625 |
626 | ## [v1.0.1] - 2018-02-22
627 |
628 | - Fix mismatch in alarm state labels and values ([#4](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/4))
629 |
630 |
631 |
632 | ## [v1.0.0] - 2018-02-15
633 |
634 | - Added better code, examples, docs ([#2](https://github.com/terraform-aws-modules/terraform-aws-notify-slack/issues/2))
635 |
636 |
637 |
638 | ## v0.0.1 - 2018-02-12
639 |
640 | - Add encrypted webhook URL example
641 | - Fix decryption of webhook URL
642 | - Update readme
643 | - Add basic example
644 | - Make KMS optional
645 | - Add README description
646 | - Add preliminary cloudwatch event handling lambda
647 | - Initial commit
648 |
649 |
650 | [Unreleased]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.18.0...HEAD
651 | [v4.18.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.17.0...v4.18.0
652 | [v4.17.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.16.0...v4.17.0
653 | [v4.16.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.15.0...v4.16.0
654 | [v4.15.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.14.0...v4.15.0
655 | [v4.14.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.13.0...v4.14.0
656 | [v4.13.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v3.6.0...v4.13.0
657 | [v3.6.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.12.0...v3.6.0
658 | [v4.12.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.11.0...v4.12.0
659 | [v4.11.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.10.0...v4.11.0
660 | [v4.10.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.9.0...v4.10.0
661 | [v4.9.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.8.0...v4.9.0
662 | [v4.8.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.7.0...v4.8.0
663 | [v4.7.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.6.0...v4.7.0
664 | [v4.6.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.5.0...v4.6.0
665 | [v4.5.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.4.0...v4.5.0
666 | [v4.4.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.3.0...v4.4.0
667 | [v4.3.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.2.0...v4.3.0
668 | [v4.2.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v3.5.0...v4.2.0
669 | [v3.5.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.1.0...v3.5.0
670 | [v4.1.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v4.0.0...v4.1.0
671 | [v4.0.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v3.4.0...v4.0.0
672 | [v3.4.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v3.3.0...v3.4.0
673 | [v3.3.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v3.2.0...v3.3.0
674 | [v3.2.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v3.1.0...v3.2.0
675 | [v3.1.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v3.0.0...v3.1.0
676 | [v3.0.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.15.0...v3.0.0
677 | [v2.15.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.14.0...v2.15.0
678 | [v2.14.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.13.0...v2.14.0
679 | [v2.13.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.12.0...v2.13.0
680 | [v2.12.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.11.0...v2.12.0
681 | [v2.11.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.10.0...v2.11.0
682 | [v2.10.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.9.0...v2.10.0
683 | [v2.9.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.8.0...v2.9.0
684 | [v2.8.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.7.0...v2.8.0
685 | [v2.7.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.6.0...v2.7.0
686 | [v2.6.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.5.0...v2.6.0
687 | [v2.5.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.4.0...v2.5.0
688 | [v2.4.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.14.0...v2.4.0
689 | [v1.14.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.3.0...v1.14.0
690 | [v2.3.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.2.0...v2.3.0
691 | [v2.2.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.1.0...v2.2.0
692 | [v2.1.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v2.0.0...v2.1.0
693 | [v2.0.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.13.0...v2.0.0
694 | [v1.13.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.12.0...v1.13.0
695 | [v1.12.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.11.0...v1.12.0
696 | [v1.11.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.10.0...v1.11.0
697 | [v1.10.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.9.0...v1.10.0
698 | [v1.9.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.8.0...v1.9.0
699 | [v1.8.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.7.0...v1.8.0
700 | [v1.7.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.6.0...v1.7.0
701 | [v1.6.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.5.0...v1.6.0
702 | [v1.5.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.4.0...v1.5.0
703 | [v1.4.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.3.0...v1.4.0
704 | [v1.3.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.2.0...v1.3.0
705 | [v1.2.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.1.0...v1.2.0
706 | [v1.1.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.0.1...v1.1.0
707 | [v1.0.1]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v1.0.0...v1.0.1
708 | [v1.0.0]: https://github.com/terraform-aws-modules/terraform-aws-notify-slack/compare/v0.0.1...v1.0.0
709 |
--------------------------------------------------------------------------------