Skip to content

Commit 543fe5c

Browse files
authored
slack/ecr-scanner-notifier: Add ECR scanner Slack notifier (#196)
#### Summary Send slack message when a vulnerability is found in the ECR image scanning #### Motivation closes #171
1 parent 3f26115 commit 543fe5c

File tree

11 files changed

+261
-3
lines changed

11 files changed

+261
-3
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ jobs:
5353
- script/database-roles
5454
- secrets
5555
- slack/chatbot
56+
- slack/ecr-scanner-notifier
5657
- slack/ecs-deployment-failure
5758
- slack/sentry
5859
- slack/sns

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
77
This project does not follow SemVer, since modules are independent of each other; thus, SemVer does not make sense. Changes are grouped per module.
88

99
## Unreleased
10+
### slack/ecr-scanner-notifier
11+
* Slack notifier for AWS Elastic Container Registry (ECR) image scans. It automatically sends notifications to a designated Slack channel whenever vulnerabilities are detected in ECR scans. [#196](https://github.com/dbl-works/terraform/pull/196)
12+
1013
### stack/global
1114
* New module for deploying resources required only once per project. [#273](https://github.com/dbl-works/terraform/pull/273)
15+
* Add ecr-scanner-notifier module [#196](https://github.com/dbl-works/terraform/pull/196)
1216

1317
### rds
1418
* update default value for backup_retention_period, max_allocated_storage, and allocated_storage. [#274](https://github.com/dbl-works/terraform/pull/274)

lambda/README.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ Used for managing lambda functions.
99
module "lambda" {
1010
source = "github.com/dbl-works/terraform//lambda?ref=main"
1111
12-
project = "dbl"
13-
environment = "production"
14-
source_dir = "Path to the directory containing the lambda function code."
12+
function_name = "lambda"
13+
project = "dbl"
14+
environment = "production"
15+
source_dir = "Path to the directory containing the lambda function code."
1516
1617
# optional
1718
handler = "index.handler"
1819
timeout = 10
1920
memory_size = 1024
21+
runtime = "nodejs16.x"
2022
2123
# Subnets the lambdas are allowed to use to access resources in the VPC.
2224
subnet_ids = [
@@ -40,5 +42,30 @@ module "lambda" {
4042
secrets_and_kms_arns = [
4143
"arn:aws:secrets:*:abc:123",
4244
]
45+
46+
lambda_policy_json = data.aws_iam_policy_document.s3.json
47+
environment_variables = {
48+
foo = "bar"
49+
}
50+
lambda_role_name = "aws-lambda-role"
4351
}
52+
53+
data "aws_iam_policy_document" "s3" {
54+
statement {
55+
effect = "Allow"
56+
actions = [
57+
"s3:PutObject"
58+
]
59+
resources = ["*"]
60+
}
61+
62+
statement {
63+
effect = "Allow"
64+
actions = [
65+
"kms:GenerateDataKey"
66+
]
67+
resources = ["*"]
68+
}
69+
}
70+
4471
```
File renamed without changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Terraform Module: ECR Scanner Slack Notifier
2+
3+
Slack notifier for AWS Elastic Container Registry (ECR) image scans. It automatically sends notifications to a designated Slack channel whenever vulnerabilities are detected in ECR scans.
4+
5+
## Usage
6+
7+
```terraform
8+
module "ecr-scanner-notifier" {
9+
source = "github.com/dbl-works/terraform//slack/ecr-scanner-notifier?ref=v2023.03.06"
10+
11+
project = local.project
12+
slack_webhook_url = "https://hooks.slack.com/services/XXXXXXXXX/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
13+
slack_channel = "ecr-scanner"
14+
}
15+
```
16+
17+
## Pre-requisites
18+
19+
1. Setting up a [Slack App](https://api.slack.com/start/overview#creating).
20+
21+
2. [Configure](https://api.slack.com/messaging/sending) the Slack app for message sending.

slack/ecr-scanner-notifier/main.tf

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
locals {
2+
function_name = "${var.project}-ecr-scanner-notifier"
3+
}
4+
module "lambda" {
5+
source = "../../lambda"
6+
7+
function_name = local.function_name
8+
project = var.project
9+
environment = "production"
10+
source_dir = "${path.module}/script"
11+
12+
environment_variables = {
13+
WEBHOOK_URL = var.slack_webhook_url
14+
CHANNEL = var.slack_channel
15+
}
16+
17+
# optional
18+
handler = "main.lambda_handler"
19+
aws_lambda_layer_arns = []
20+
runtime = "ruby3.2"
21+
}
22+
23+
# EventBridge was formerly known as CloudWatch Events. The functionality is identical.
24+
resource "aws_cloudwatch_event_rule" "ecr_scanner" {
25+
name = "${var.project}-ecr-scanner"
26+
description = "Send notification to slack after each ecr scan"
27+
28+
event_pattern = <<EOF
29+
{
30+
"source": ["aws.ecr"],
31+
"detail-type": ["ECR Image Scan"],
32+
"detail": {
33+
"finding-severity-counts": {
34+
"$or": [{
35+
"CRITICAL": [{
36+
"exists": true
37+
}]
38+
}, {
39+
"HIGH": [{
40+
"exists": true
41+
}]
42+
}, {
43+
"MEDIUM": [{
44+
"exists": true
45+
}]
46+
}, {
47+
"LOW": [{
48+
"exists": true
49+
}]
50+
}, {
51+
"UNDEFINED": [{
52+
"exists": true
53+
}]
54+
}]
55+
}
56+
}
57+
}
58+
EOF
59+
60+
tags = {
61+
Project = var.project
62+
}
63+
}
64+
65+
resource "aws_cloudwatch_event_target" "ecr_scanner" {
66+
rule = aws_cloudwatch_event_rule.ecr_scanner.name
67+
target_id = aws_cloudwatch_event_rule.ecr_scanner.name
68+
arn = module.lambda.arn
69+
70+
depends_on = [
71+
aws_cloudwatch_event_rule.ecr_scanner
72+
]
73+
}
74+
75+
resource "aws_lambda_permission" "allow_cloudwatch_event_to_invoke_lambda" {
76+
action = "lambda:InvokeFunction"
77+
function_name = local.function_name
78+
principal = "events.amazonaws.com"
79+
source_arn = aws_cloudwatch_event_rule.ecr_scanner.arn
80+
81+
depends_on = [
82+
module.lambda
83+
]
84+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
output "lambda_log_group_name" {
2+
value = "/aws/lambda/${local.function_name}"
3+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
require 'json'
2+
require 'net/http'
3+
require 'uri'
4+
5+
def text_block_from(finding_counts)
6+
if finding_counts.fetch('CRITICAL', 0) != 0
7+
{ 'color' => 'danger', 'icon' => ':red_circle:' }
8+
elsif finding_counts.fetch('HIGH', 0) != 0
9+
{ 'color' => 'warning', 'icon' => ':large_orange_diamond:' }
10+
else
11+
{ 'color' => 'good', 'icon' => ':green_heart:' }
12+
end
13+
end
14+
15+
def build_slack_message(event)
16+
channel = ENV.fetch('CHANNEL')
17+
18+
account_id = event.fetch('account')
19+
detail = event.fetch('detail')
20+
region = event.fetch('region')
21+
repository_name = detail.fetch('repository-name')
22+
severity_counts = detail.fetch('finding-severity-counts')
23+
image_digest = detail.fetch('image-digest')
24+
25+
message = "*ECR Image Scan findings | #{region} | Account ID:#{account_id}*"
26+
text_properties = text_block_from(severity_counts)
27+
28+
{
29+
'username' => 'Amazon ECR',
30+
'channels' => channel,
31+
'icon_emoji' => ':ecr:',
32+
'text' => message,
33+
'attachments' => [
34+
{
35+
'fallback' => 'AmazonECR Image Scan Findings Description.',
36+
'color' => text_properties.fetch('color'),
37+
'title' => "#{text_properties.fetch('icon')} #{repository_name}:#{detail.fetch('image-tags', [])[0]}",
38+
'title_link' => "https://#{region}.console.aws.amazon.com/ecr/repositories/#{repository_name}/_/image/#{image_digest}/scan-results?region=#{region}",
39+
'text' => "Image Scan Completed at #{event.fetch('time')}",
40+
'fields' => severity_counts.map do |severity_level, count|
41+
{ 'title' => severity_level.capitalize, 'value' => count, 'short' => true }
42+
end
43+
}
44+
]
45+
}
46+
end
47+
48+
def lambda_handler(event:, context:)
49+
# Sample responses
50+
# {
51+
# "version": "0",
52+
# "id": "85fc3613-e913-7fc4-a80c-a3753e4aa9ae",
53+
# "detail-type": "ECR Image Scan",
54+
# "source": "aws.ecr",
55+
# "account": "123456789012",
56+
# "time": "2019-10-29T02:36:48Z",
57+
# "region": "us-east-1",
58+
# "resources": [
59+
# "arn:aws:ecr:us-east-1:123456789012:repository/my-repo"
60+
# ],
61+
# "detail": {
62+
# "scan-status": "COMPLETE",
63+
# "repository-name": "my-repo",
64+
# "finding-severity-counts": {
65+
# "CRITICAL": 10,
66+
# "MEDIUM": 9
67+
# },
68+
# "image-digest": "sha256:7f5b2640fe6fb4f46592dfd3410c4a79dac4f89e4782432e0378abcd1234",
69+
# "image-tags": ["commit-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]
70+
# }
71+
# }
72+
#
73+
slack_message = build_slack_message(event)
74+
uri = URI(ENV.fetch('WEBHOOK_URL'))
75+
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
76+
req.body = slack_message.to_json
77+
78+
begin
79+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
80+
http.request(req)
81+
end
82+
puts('Message posted.')
83+
rescue StandardError => e
84+
puts("Request failed: #{e.message}")
85+
end
86+
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
variable "project" {
2+
type = string
3+
}
4+
5+
variable "slack_webhook_url" {
6+
type = string
7+
validation {
8+
condition = can(regex("^https://", var.slack_webhook_url))
9+
error_message = "The URL must start with https"
10+
}
11+
}
12+
13+
variable "slack_channel" {
14+
type = string
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module "ecr-scanner-notifier" {
2+
count = var.ecr_scanner_notifier_config == null ? 0 : 1
3+
4+
source = "../../slack/ecr-scanner-notifier"
5+
6+
project = var.project
7+
slack_webhook_url = var.ecr_scanner_notifier_config.slack_webhook_url
8+
slack_channel = var.ecr_scanner_notifier_config.slack_channel
9+
}

0 commit comments

Comments
 (0)