lono 3.0.1 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a7bf899e0c6e9dba34f0aff2e220325275f419fd
4
- data.tar.gz: 47400300431d3be963f406faff2ce98b369021d9
3
+ metadata.gz: ab4d099f039afce1da99fa539e4f09d5297b8405
4
+ data.tar.gz: 3a566f0d7c3b68dca04bdf5b62e0bdb435ad5d39
5
5
  SHA512:
6
- metadata.gz: 9e4b477af4147d519185b81ad26ad1489739d13ddc849dde311fdf3994862132c56e567f13cb18f0ccfe554042f4a044703c00e46a2d5601aaeb1045f7afa98a
7
- data.tar.gz: 95b037ae178f17e914542ef48667fc4196235c8ce68a9b997fa1d339f6a2584841b0707c06b047318eb699052e2a00e9643726390dc6ff4eeb5072ee69be9151
6
+ metadata.gz: c40c0fe52867c7122464a6a2bd8d33f3a9508103b643d6a28f902c0fc965b60721264413c3faedf0ba8c898cd774b16f8aa1da14a0cd8c1565ab4923ea91f992
7
+ data.tar.gz: 3c2b37adac7f040ad08455c85d0326af2f64b743ca6a624d4c5c36512a33666fe7830008e78f8dd3863ec37c895bac882f983b5fc953e63f0ef11681e508cf88
data/.gitignore CHANGED
@@ -7,3 +7,4 @@ pkg
7
7
  tmp
8
8
  output
9
9
  spec/project
10
+ spec/fixtures/my_project/templates/aws-waf-security-automations.yml
data/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  This project *tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
5
 
6
+ ## [3.1.1]
7
+ - update lono import and new cli help
8
+
9
+ ## [3.1.0]
10
+ - lono import command
11
+
6
12
  ## [3.0.1]
7
13
  - update aws-sdk to version 3
8
14
 
data/lib/lono.rb CHANGED
@@ -24,6 +24,7 @@ module Lono
24
24
  autoload :Param, 'lono/param'
25
25
  autoload :Clean, 'lono/clean'
26
26
  autoload :Settings, 'lono/settings'
27
+ autoload :Importer, 'lono/importer'
27
28
  end
28
29
 
29
30
  Lono::Env.setup!
data/lib/lono/cfn/diff.rb CHANGED
@@ -43,6 +43,6 @@ class Lono::Cfn::Diff < Lono::Cfn::Base
43
43
  end
44
44
 
45
45
  def existing_template_path
46
- "/tmp/existing_cfn_template.json"
46
+ "/tmp/existing_cfn_template.yml"
47
47
  end
48
48
  end
data/lib/lono/cfn/util.rb CHANGED
@@ -14,7 +14,7 @@ module Lono::Cfn::Util
14
14
  end
15
15
 
16
16
  unless sure =~ /^y/
17
- puts "Exiting without #{action}"
17
+ puts "Whew! Exiting without running #{action}."
18
18
  exit 0
19
19
  end
20
20
  end
data/lib/lono/cli.rb CHANGED
@@ -6,7 +6,7 @@ module Lono
6
6
  class CLI < Lono::Command
7
7
 
8
8
  desc "new [NAME]", "Generates lono starter project"
9
- Help.new_long_desc
9
+ long_desc Help.new_long_desc
10
10
  option :force, type: :boolean, aliases: "-f", desc: "override existing starter files"
11
11
  option :quiet, type: :boolean, aliases: "-q", desc: "silence the output"
12
12
  option :format, type: :string, default: "yaml", desc: "starter project template format: json or yaml"
@@ -14,6 +14,14 @@ module Lono
14
14
  Lono::New.new(options.clone.merge(project_root: project_root)).run
15
15
  end
16
16
 
17
+ desc "import [SOURCE]", "Imports raw CloudFormation template and lono-fies it"
18
+ long_desc Help.import
19
+ option :format, type: :string, default: "yaml", desc: "format for the final template"
20
+ option :project_root, default: ".", aliases: "-r", desc: "project root"
21
+ def import(source)
22
+ Importer.new(source, options).run
23
+ end
24
+
17
25
  desc "generate", "Generate both CloudFormation templates and parameters files"
18
26
  Help.generate
19
27
  option :clean, type: :boolean, aliases: "-c", desc: "remove all output files before generating"
data/lib/lono/help.rb CHANGED
@@ -9,6 +9,20 @@ $ lono new lono
9
9
  EOL
10
10
  end
11
11
 
12
+ def import
13
+ <<-EOL
14
+ Examples:
15
+
16
+ $ lono import /path/to/file
17
+
18
+ $ lono import http://url.com/path/to/template.json
19
+
20
+ $ lono import http://url.com/path/to/template.yml
21
+
22
+ Imports a raw CloudFormation template and lono-fies it.
23
+ EOL
24
+ end
25
+
12
26
  def generate
13
27
  <<-EOL
14
28
  Examples:
@@ -19,7 +33,6 @@ $ lono g -c # shortcut
19
33
 
20
34
  Builds both CloudFormation template and parameter files based on lono project and writes them to the output folder on the filesystem.
21
35
  EOL
22
-
23
36
  end
24
37
 
25
38
  def template
@@ -0,0 +1,60 @@
1
+ require "open-uri"
2
+ require "json"
3
+ require "yaml"
4
+
5
+ class Lono::Importer
6
+ attr_reader :options
7
+ def initialize(source, options)
8
+ @source = source
9
+ @options = options
10
+ @format = normalize_format(@options[:format])
11
+ @project_root = options[:project_root] || '.'
12
+ end
13
+
14
+ def run
15
+ download_template
16
+ add_template_definition
17
+ puts "Imported raw CloudFormation template and lono-fied it!"
18
+ end
19
+
20
+ def download_template
21
+ template = open(@source).read
22
+
23
+ result = if @format == 'yml'
24
+ YAML.dump(YAML.load(template))
25
+ else
26
+ JSON.pretty_generate(JSON.load(template))
27
+ end
28
+
29
+ folder = File.dirname(dest_path)
30
+ FileUtils.mkdir_p(folder) unless File.exist?(folder)
31
+ IO.write(dest_path, result)
32
+ puts "Template downloaded to #{dest_path}."
33
+ end
34
+
35
+ # Add template definition to config/templates/base/stacks.rb.
36
+ def add_template_definition
37
+ path = "#{@project_root}/config/templates/base/stacks.rb"
38
+ lines = File.exist?(path) ? IO.readlines(path) : []
39
+ new_template_definition = %Q|template "#{template_name}"|
40
+ unless lines.detect { |l| l.include?(new_template_definition) }
41
+ lines << ["\n", new_template_definition]
42
+ result = lines.join('')
43
+ IO.write(path, result)
44
+ end
45
+ puts "Template definition added to #{path}."
46
+ end
47
+
48
+ def dest_path
49
+ "#{@project_root}/templates/#{template_name}.#{@format}"
50
+ end
51
+
52
+ def template_name
53
+ File.basename(@source, ".*")
54
+ end
55
+
56
+ private
57
+ def normalize_format(format)
58
+ format == 'yaml' ? 'yml' : format
59
+ end
60
+ end
@@ -1,6 +1,7 @@
1
1
  require 'erb'
2
2
  require 'json'
3
3
  require 'base64'
4
+ require 'digest'
4
5
 
5
6
  class Lono::Template::Upload
6
7
  include Lono::Template::AwsServices
@@ -8,10 +9,13 @@ class Lono::Template::Upload
8
9
  def initialize(options={})
9
10
  @options = options
10
11
  @project_root = options[:project_root] || '.'
12
+ @checksums = {}
11
13
  end
12
14
 
13
15
  def run
14
16
  ensure_s3_setup!
17
+ load_checksums!
18
+
15
19
  paths = Dir.glob("#{@project_root}/output/**/*")
16
20
  paths.reject { |p| p =~ %r{output/params} }.
17
21
  select { |p| File.file?(p) }.each do |path|
@@ -20,11 +24,48 @@ class Lono::Template::Upload
20
24
  say "Templates uploaded to s3."
21
25
  end
22
26
 
27
+ # Read existing files on s3 to grab their md5 checksum.
28
+ # We do this so we can see if we should avoid re-uploading the s3 child template
29
+ # entirely. If we upload a new child template that does not change AWS CloudFormation
30
+ # is not smart enough to know that it not has changed. I think all AWS CloudFormation
31
+ # does is check if the file's timestamp.
32
+ #
33
+ # Thought this would result in better AWS Change Set info but AWS still reports child
34
+ # stacks being changed even though they should not be reported. Leaving this s3 checksum
35
+ # in for now.
36
+ def load_checksums!
37
+ return if @options[:noop]
38
+
39
+ prefix = "#{s3_path}/#{LONO_ENV}" # s3://s3-bucket-and-path-from-settings/prod
40
+ resp = s3.list_objects(bucket: s3_bucket, prefix: prefix)
41
+ resp.contents.each do |object|
42
+ # key does not include the bucket name
43
+ # full path = s3://my-bucket/cloudformation-templates/prod/my-template.yml
44
+ # key = cloudformation-templates/prod/my-template.yml
45
+ # etag is the checksum as long as the file is not a multi-part file upload
46
+ # it has extra double quotes wrapped around it.
47
+ # etag = "\"9cb437490cee2cc96101baf326e5ca81\""
48
+ @checksums[object.key] = strip_surrounding_quotes(object.etag)
49
+ end
50
+ @checksums
51
+ end
52
+
53
+ def strip_surrounding_quotes(string)
54
+ string.sub(/^"/,'').sub(/"$/,'')
55
+ end
56
+
23
57
  def upload(path)
24
58
  pretty_path = path.sub(/^\.\//, '')
25
59
  key = "#{s3_path}/#{LONO_ENV}/#{pretty_path.sub(/^output\//,'')}"
26
60
  s3_full_path = "s3://#{s3_bucket}/#{key}"
27
61
 
62
+ local_checksum = Digest::MD5.hexdigest(IO.read(path))
63
+ remote_checksum = remote_checksum(path)
64
+ if local_checksum == remote_checksum
65
+ say("Not modified: #{pretty_path} to #{s3_full_path}".colorize(:yellow)) unless @options[:noop]
66
+ return # do not upload unless the checksum has changed
67
+ end
68
+
28
69
  resp = s3.put_object(
29
70
  body: IO.read(path),
30
71
  bucket: s3_bucket,
@@ -32,11 +73,26 @@ class Lono::Template::Upload
32
73
  storage_class: "REDUCED_REDUNDANCY"
33
74
  ) unless @options[:noop]
34
75
 
35
- message = "Uploaded: #{pretty_path} to #{s3_full_path}"
76
+ # Example output:
77
+ # Uploaded: output/docker.yml to s3://boltops-stag/cloudformation-templates/stag/docker.yml
78
+ # Uploaded: output/ecs/private.yml to s3://boltops-stag/cloudformation-templates/stag/ecs/private.yml
79
+ message = "Uploaded: #{pretty_path} to #{s3_full_path}".colorize(:green)
36
80
  message = "NOOP: #{message}" if @options[:noop]
37
81
  say message
38
82
  end
39
83
 
84
+ # @checksums map has a key format: cloudformation-templates/stag/docker.yml
85
+ #
86
+ # path = ./output/docker.yml
87
+ # s3_path = s3://boltops-stag/cloudformation-templates/stag/docker.yml
88
+ def remote_checksum(path)
89
+ # first convert the local path to the path format that is stored in @checksums keys
90
+ # ./output/docker.yml => cloudformation-templates/stag/docker.yml
91
+ pretty_path = path.sub(/^\.\//, '')
92
+ key = "#{s3_path}/#{LONO_ENV}/#{pretty_path.sub(/^output\//,'')}"
93
+ @checksums[key]
94
+ end
95
+
40
96
  # https://s3.amazonaws.com/mybucket/cloudformation-templates/prod/parent.yml
41
97
  def s3_https_url(template_path)
42
98
  ensure_s3_setup!
data/lib/lono/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Lono
2
- VERSION = "3.0.1"
2
+ VERSION = "3.1.1"
3
3
  end
@@ -0,0 +1,1575 @@
1
+ {
2
+ "AWSTemplateFormatVersion": "2010-09-09",
3
+ "Description": "(SO0006-CloudFront) - AWS WAF Security Automations: This AWS CloudFormation template helps you provision the AWS WAF Security Automations stack without worrying about creating and configuring the underlying AWS infrastructure. **WARNING** This template creates an AWS Lambda function, an AWS WAF Web ACL, an Amazon S3 bucket, and an Amazon CloudWatch custom metric. You will be billed for the AWS resources used if you create a stack from this template. **NOTICE** Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. Licensed under the Amazon Software License (the License). You may not use this file except in compliance with the License. A copy of the License is located at http://aws.amazon.com/asl/ or in the license file accompanying this file. This file is distributed on an AS IS BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and limitations under the License.",
4
+ "Metadata": {
5
+ "AWS::CloudFormation::Interface": {
6
+ "ParameterGroups": [{
7
+ "Label": {
8
+ "default": "Protection List"
9
+ },
10
+ "Parameters": ["SqlInjectionProtectionParam", "CrossSiteScriptingProtectionParam", "ActivateHttpFloodProtectionParam", "ActivateScansProbesProtectionParam", "ActivateReputationListsProtectionParam", "ActivateBadBotProtectionParam"]
11
+ }, {
12
+ "Label": {
13
+ "default": "Settings"
14
+ },
15
+ "Parameters": ["CloudFrontAccessLogBucket"]
16
+ }, {
17
+ "Label": {
18
+ "default": "Advanced Settings"
19
+ },
20
+ "Parameters": ["RequestThreshold", "ErrorThreshold", "WAFBlockPeriod"]
21
+ }, {
22
+ "Label": {
23
+ "default": "Anonymous Metrics Request"
24
+ },
25
+ "Parameters": ["SendAnonymousUsageData"]
26
+ }],
27
+ "ParameterLabels": {
28
+ "SqlInjectionProtectionParam": {
29
+ "default": "Activate SQL Injection Protection"
30
+ },
31
+ "CrossSiteScriptingProtectionParam": {
32
+ "default": "Activate Cross-site Scripting Protection"
33
+ },
34
+ "ActivateHttpFloodProtectionParam": {
35
+ "default": "Activate HTTP Flood Protection"
36
+ },
37
+ "ActivateScansProbesProtectionParam": {
38
+ "default": "Activate Scanner & Probe Protection"
39
+ },
40
+ "ActivateReputationListsProtectionParam": {
41
+ "default": "Activate Reputation List Protection"
42
+ },
43
+ "ActivateBadBotProtectionParam": {
44
+ "default": "Activate Bad Bot Protection"
45
+ },
46
+ "CloudFrontAccessLogBucket": {
47
+ "default": "CloudFront Access Log Bucket Name"
48
+ },
49
+ "SendAnonymousUsageData": {
50
+ "default": "Send Anonymous Usage Data"
51
+ },
52
+ "RequestThreshold": {
53
+ "default": "Request Threshold"
54
+ },
55
+ "ErrorThreshold": {
56
+ "default": "Error Threshold"
57
+ },
58
+ "WAFBlockPeriod": {
59
+ "default": "WAF Block Period"
60
+ }
61
+ }
62
+ }
63
+ },
64
+
65
+
66
+ "Parameters": {
67
+ "SqlInjectionProtectionParam": {
68
+ "Type": "String",
69
+ "Default": "yes",
70
+ "AllowedValues": ["yes", "no"],
71
+ "Description": "Choose yes to enable the component designed to block common SQL injection attacks."
72
+ },
73
+ "CrossSiteScriptingProtectionParam": {
74
+ "Type": "String",
75
+ "Default": "yes",
76
+ "AllowedValues": ["yes", "no"],
77
+ "Description": "Choose yes to enable the component designed to block common XSS attacks."
78
+ },
79
+ "ActivateHttpFloodProtectionParam": {
80
+ "Type": "String",
81
+ "Default": "yes",
82
+ "AllowedValues": ["yes", "no"],
83
+ "Description": "Choose yes to enable the component designed to block HTTP flood attacks."
84
+ },
85
+ "ActivateScansProbesProtectionParam": {
86
+ "Type": "String",
87
+ "Default": "yes",
88
+ "AllowedValues": ["yes", "no"],
89
+ "Description": "Choose yes to enable the component designed to block scanners and probes."
90
+ },
91
+ "ActivateReputationListsProtectionParam": {
92
+ "Type": "String",
93
+ "Default": "yes",
94
+ "AllowedValues": ["yes", "no"],
95
+ "Description": "Choose yes to block requests from IP addresses on third-party reputation lists (supported lists: spamhaus, torproject, and emergingthreats)."
96
+ },
97
+ "ActivateBadBotProtectionParam": {
98
+ "Type": "String",
99
+ "Default": "yes",
100
+ "AllowedValues": ["yes", "no"],
101
+ "Description": "Choose yes to enable the component designed to block bad bots and content scrapers."
102
+ },
103
+ "CloudFrontAccessLogBucket": {
104
+ "Type": "String",
105
+ "Default": "",
106
+ "AllowedPattern": "(^$|^([a-z]|(\\d(?!\\d{0,2}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})))([a-z\\d]|(\\.(?!(\\.|-)))|(-(?!\\.))){1,61}[a-z\\d]$)",
107
+ "Description": "Enter a name for the Amazon S3 bucket where you want to store Amazon CloudFront access logs. This can be the name of either an existing S3 bucket, or a new bucket that the template will create during stack launch (if it does not find a matching bucket name). The solution will modify the bucket’s notification configuration to trigger the Log Parser AWS Lambda function whenever a new log file is saved in this bucket. More about bucket name restriction here: http://amzn.to/1p1YlU5"
108
+ },
109
+ "SendAnonymousUsageData": {
110
+ "Type": "String",
111
+ "Default": "yes",
112
+ "AllowedValues": ["yes", "no"],
113
+ "Description": "Send anonymous data to AWS to help us understand solution usage across our customer base as a whole. To opt out of this feature, select No."
114
+ },
115
+ "RequestThreshold": {
116
+ "Type": "Number",
117
+ "Default": "400",
118
+ "Description": "If you chose yes for the Activate HTTP Flood Protection parameter, enter the maximum acceptable requests per minute per IP address. If you chose to deactivate this protection, ignore this parameter."
119
+ },
120
+ "ErrorThreshold": {
121
+ "Type": "Number",
122
+ "Default": "50",
123
+ "Description": "If you chose yes for the Activate Scanners & Probes Protection parameter, enter the maximum acceptable bad requests per minute per IP. If you chose to deactivate Scanners & Probes protection, ignore this parameter."
124
+ },
125
+ "WAFBlockPeriod": {
126
+ "Type": "Number",
127
+ "Default": "240",
128
+ "Description": "If you chose yes for the Activate HTTP Flood Protection or Activate Scanners & Probes Protection parameters, enter the period (in minutes) to block applicable IP addresses. If you chose to deactivate both types of protection, ignore this parameter."
129
+ }
130
+ },
131
+
132
+
133
+ "Conditions": {
134
+ "SqlInjectionProtectionActivated": {
135
+ "Fn::Equals": [{
136
+ "Ref": "SqlInjectionProtectionParam"
137
+ }, "yes"]
138
+ },
139
+ "CrossSiteScriptingProtectionActivated": {
140
+ "Fn::Equals": [{
141
+ "Ref": "CrossSiteScriptingProtectionParam"
142
+ }, "yes"]
143
+ },
144
+ "HttpFloodProtectionActivated": {
145
+ "Fn::Equals": [{
146
+ "Ref": "ActivateHttpFloodProtectionParam"
147
+ }, "yes"]
148
+ },
149
+ "ScansProbesProtectionActivated": {
150
+ "Fn::Equals": [{
151
+ "Ref": "ActivateScansProbesProtectionParam"
152
+ }, "yes"]
153
+ },
154
+ "ReputationListsProtectionActivated": {
155
+ "Fn::Equals": [{
156
+ "Ref": "ActivateReputationListsProtectionParam"
157
+ }, "yes"]
158
+ },
159
+ "BadBotProtectionActivated": {
160
+ "Fn::Equals": [{
161
+ "Ref": "ActivateBadBotProtectionParam"
162
+ }, "yes"]
163
+ },
164
+ "LogParserActivated": {
165
+ "Fn::Or": [{
166
+ "Condition": "HttpFloodProtectionActivated"
167
+ }, {
168
+ "Condition": "ScansProbesProtectionActivated"
169
+ }]
170
+ },
171
+ "CreateWebACL": {
172
+ "Fn::Or": [{
173
+ "Condition": "SqlInjectionProtectionActivated"
174
+ }, {
175
+ "Condition": "CrossSiteScriptingProtectionActivated"
176
+ }, {
177
+ "Condition": "LogParserActivated"
178
+ }, {
179
+ "Condition": "ReputationListsProtectionActivated"
180
+ }, {
181
+ "Condition": "BadBotProtectionActivated"
182
+ }]
183
+ }
184
+ },
185
+ "Resources": {
186
+ "WAFWhitelistSet": {
187
+ "Type": "AWS::WAF::IPSet",
188
+ "Condition": "CreateWebACL",
189
+ "Properties": {
190
+ "Name": {
191
+ "Fn::Join": [" - ", [{
192
+ "Ref": "AWS::StackName"
193
+ }, "Whitelist Set"]]
194
+ }
195
+ }
196
+ },
197
+ "WAFBlacklistSet": {
198
+ "Type": "AWS::WAF::IPSet",
199
+ "Condition": "LogParserActivated",
200
+ "Properties": {
201
+ "Name": {
202
+ "Fn::Join": [" - ", [{
203
+ "Ref": "AWS::StackName"
204
+ }, "Blacklist Set"]]
205
+ }
206
+ }
207
+ },
208
+ "WAFAutoBlockSet": {
209
+ "Type": "AWS::WAF::IPSet",
210
+ "Condition": "LogParserActivated",
211
+ "Properties": {
212
+ "Name": {
213
+ "Fn::Join": [" - ", [{
214
+ "Ref": "AWS::StackName"
215
+ }, "Auto Block Set"]]
216
+ }
217
+ }
218
+ },
219
+ "WAFReputationListsSet1": {
220
+ "Type": "AWS::WAF::IPSet",
221
+ "Condition": "ReputationListsProtectionActivated",
222
+ "Properties": {
223
+ "Name": {
224
+ "Fn::Join": [" - ", [{
225
+ "Ref": "AWS::StackName"
226
+ }, "IP Reputation Lists Set #1"]]
227
+ }
228
+ }
229
+ },
230
+ "WAFReputationListsSet2": {
231
+ "Type": "AWS::WAF::IPSet",
232
+ "Condition": "ReputationListsProtectionActivated",
233
+ "Properties": {
234
+ "Name": {
235
+ "Fn::Join": [" - ", [{
236
+ "Ref": "AWS::StackName"
237
+ }, "IP Reputation Lists Set #2"]]
238
+ }
239
+ }
240
+ },
241
+ "WAFBadBotSet": {
242
+ "Type": "AWS::WAF::IPSet",
243
+ "Condition": "BadBotProtectionActivated",
244
+ "Properties": {
245
+ "Name": {
246
+ "Fn::Join": [" - ", [{
247
+ "Ref": "AWS::StackName"
248
+ }, "IP Bad Bot Set"]]
249
+ }
250
+ }
251
+ },
252
+ "WAFSqlInjectionDetection": {
253
+ "Type": "AWS::WAF::SqlInjectionMatchSet",
254
+ "Condition": "SqlInjectionProtectionActivated",
255
+ "Properties": {
256
+ "Name": {
257
+ "Fn::Join": [" - ", [{
258
+ "Ref": "AWS::StackName"
259
+ }, "SQL injection Detection"]]
260
+ },
261
+ "SqlInjectionMatchTuples": [{
262
+ "FieldToMatch": {
263
+ "Type": "QUERY_STRING"
264
+ },
265
+ "TextTransformation": "URL_DECODE"
266
+ }, {
267
+ "FieldToMatch": {
268
+ "Type": "QUERY_STRING"
269
+ },
270
+ "TextTransformation": "HTML_ENTITY_DECODE"
271
+ }, {
272
+ "FieldToMatch": {
273
+ "Type": "BODY"
274
+ },
275
+ "TextTransformation": "URL_DECODE"
276
+ }, {
277
+ "FieldToMatch": {
278
+ "Type": "BODY"
279
+ },
280
+ "TextTransformation": "HTML_ENTITY_DECODE"
281
+ }, {
282
+ "FieldToMatch": {
283
+ "Type": "URI"
284
+ },
285
+ "TextTransformation": "URL_DECODE"
286
+ }, {
287
+ "FieldToMatch": {
288
+ "Type": "URI"
289
+ },
290
+ "TextTransformation": "HTML_ENTITY_DECODE"
291
+ }]
292
+ }
293
+ },
294
+ "WAFXssDetection": {
295
+ "Type": "AWS::WAF::XssMatchSet",
296
+ "Condition": "CrossSiteScriptingProtectionActivated",
297
+ "Properties": {
298
+ "Name": {
299
+ "Fn::Join": [" - ", [{
300
+ "Ref": "AWS::StackName"
301
+ }, "XSS Detection Detection"]]
302
+ },
303
+ "XssMatchTuples": [{
304
+ "FieldToMatch": {
305
+ "Type": "QUERY_STRING"
306
+ },
307
+ "TextTransformation": "URL_DECODE"
308
+ }, {
309
+ "FieldToMatch": {
310
+ "Type": "QUERY_STRING"
311
+ },
312
+ "TextTransformation": "HTML_ENTITY_DECODE"
313
+ }, {
314
+ "FieldToMatch": {
315
+ "Type": "BODY"
316
+ },
317
+ "TextTransformation": "URL_DECODE"
318
+ }, {
319
+ "FieldToMatch": {
320
+ "Type": "BODY"
321
+ },
322
+ "TextTransformation": "HTML_ENTITY_DECODE"
323
+ }, {
324
+ "FieldToMatch": {
325
+ "Type": "URI"
326
+ },
327
+ "TextTransformation": "URL_DECODE"
328
+ }, {
329
+ "FieldToMatch": {
330
+ "Type": "URI"
331
+ },
332
+ "TextTransformation": "HTML_ENTITY_DECODE"
333
+ }]
334
+ }
335
+ },
336
+ "WAFWhitelistRule": {
337
+ "Type": "AWS::WAF::Rule",
338
+ "Condition": "CreateWebACL",
339
+ "DependsOn": "WAFWhitelistSet",
340
+ "Properties": {
341
+ "Name": {
342
+ "Fn::Join": [" - ", [{
343
+ "Ref": "AWS::StackName"
344
+ }, "Whitelist Rule"]]
345
+ },
346
+ "MetricName": "SecurityAutomationsWhitelistRule",
347
+ "Predicates": [{
348
+ "DataId": {
349
+ "Ref": "WAFWhitelistSet"
350
+ },
351
+ "Negated": false,
352
+ "Type": "IPMatch"
353
+ }]
354
+ }
355
+ },
356
+ "WAFBlacklistRule": {
357
+ "Type": "AWS::WAF::Rule",
358
+ "Condition": "LogParserActivated",
359
+ "DependsOn": "WAFBlacklistSet",
360
+ "Properties": {
361
+ "Name": {
362
+ "Fn::Join": [" - ", [{
363
+ "Ref": "AWS::StackName"
364
+ }, "Blacklist Rule"]]
365
+ },
366
+ "MetricName": "SecurityAutomationsBlacklistRule",
367
+ "Predicates": [{
368
+ "DataId": {
369
+ "Ref": "WAFBlacklistSet"
370
+ },
371
+ "Negated": false,
372
+ "Type": "IPMatch"
373
+ }]
374
+ }
375
+ },
376
+ "WAFAutoBlockRule": {
377
+ "Type": "AWS::WAF::Rule",
378
+ "Condition": "LogParserActivated",
379
+ "DependsOn": "WAFAutoBlockSet",
380
+ "Properties": {
381
+ "Name": {
382
+ "Fn::Join": [" - ", [{
383
+ "Ref": "AWS::StackName"
384
+ }, "Auto Block Rule"]]
385
+ },
386
+ "MetricName": "SecurityAutomationsAutoBlockRule",
387
+ "Predicates": [{
388
+ "DataId": {
389
+ "Ref": "WAFAutoBlockSet"
390
+ },
391
+ "Negated": false,
392
+ "Type": "IPMatch"
393
+ }]
394
+ }
395
+ },
396
+ "WAFIPReputationListsRule1": {
397
+ "Type": "AWS::WAF::Rule",
398
+ "Condition": "ReputationListsProtectionActivated",
399
+ "DependsOn": "WAFReputationListsSet1",
400
+ "Properties": {
401
+ "Name": {
402
+ "Fn::Join": [" - ", [{
403
+ "Ref": "AWS::StackName"
404
+ }, "WAF IP Reputation Lists Rule #1"]]
405
+ },
406
+ "MetricName": "SecurityAutomationsIPReputationListsRule1",
407
+ "Predicates": [{
408
+ "DataId": {
409
+ "Ref": "WAFReputationListsSet1"
410
+ },
411
+ "Type": "IPMatch",
412
+ "Negated": "false"
413
+ }]
414
+ }
415
+ },
416
+ "WAFIPReputationListsRule2": {
417
+ "Type": "AWS::WAF::Rule",
418
+ "Condition": "ReputationListsProtectionActivated",
419
+ "DependsOn": "WAFReputationListsSet2",
420
+ "Properties": {
421
+ "Name": {
422
+ "Fn::Join": [" - ", [{
423
+ "Ref": "AWS::StackName"
424
+ }, "WAF IP Reputation Lists Rule #2"]]
425
+ },
426
+ "MetricName": "SecurityAutomationsIPReputationListsRule2",
427
+ "Predicates": [{
428
+ "DataId": {
429
+ "Ref": "WAFReputationListsSet2"
430
+ },
431
+ "Type": "IPMatch",
432
+ "Negated": "false"
433
+ }]
434
+ }
435
+ },
436
+ "WAFBadBotRule": {
437
+ "Type": "AWS::WAF::Rule",
438
+ "Condition": "BadBotProtectionActivated",
439
+ "DependsOn": "WAFBadBotSet",
440
+ "Properties": {
441
+ "Name": {
442
+ "Fn::Join": [" - ", [{
443
+ "Ref": "AWS::StackName"
444
+ }, "Bad Bot Rule"]]
445
+ },
446
+ "MetricName": "SecurityAutomationsBadBotRule",
447
+ "Predicates": [{
448
+ "DataId": {
449
+ "Ref": "WAFBadBotSet"
450
+ },
451
+ "Type": "IPMatch",
452
+ "Negated": "false"
453
+ }]
454
+ }
455
+ },
456
+ "WAFSqlInjectionRule": {
457
+ "Type": "AWS::WAF::Rule",
458
+ "Condition": "SqlInjectionProtectionActivated",
459
+ "DependsOn": "WAFSqlInjectionDetection",
460
+ "Properties": {
461
+ "Name": {
462
+ "Fn::Join": [" - ", [{
463
+ "Ref": "AWS::StackName"
464
+ }, "SQL Injection Rule"]]
465
+ },
466
+ "MetricName": "SecurityAutomationsSqlInjectionRule",
467
+ "Predicates": [{
468
+ "DataId": {
469
+ "Ref": "WAFSqlInjectionDetection"
470
+ },
471
+ "Negated": false,
472
+ "Type": "SqlInjectionMatch"
473
+ }]
474
+ }
475
+ },
476
+ "WAFXssRule": {
477
+ "Type": "AWS::WAF::Rule",
478
+ "Condition": "CrossSiteScriptingProtectionActivated",
479
+ "DependsOn": "WAFXssDetection",
480
+ "Properties": {
481
+ "Name": {
482
+ "Fn::Join": [" - ", [{
483
+ "Ref": "AWS::StackName"
484
+ }, "XSS Rule"]]
485
+ },
486
+ "MetricName": "SecurityAutomationsXssRule",
487
+ "Predicates": [{
488
+ "DataId": {
489
+ "Ref": "WAFXssDetection"
490
+ },
491
+ "Negated": false,
492
+ "Type": "XssMatch"
493
+ }]
494
+ }
495
+ },
496
+ "WAFWebACL": {
497
+ "Type": "AWS::WAF::WebACL",
498
+ "Condition": "CreateWebACL",
499
+ "DependsOn": ["WAFWhitelistRule"],
500
+ "Properties": {
501
+ "Name": {
502
+ "Ref": "AWS::StackName"
503
+ },
504
+ "DefaultAction": {
505
+ "Type": "ALLOW"
506
+ },
507
+ "MetricName": "SecurityAutomationsMaliciousRequesters",
508
+ "Rules": [{
509
+ "Action": {
510
+ "Type": "ALLOW"
511
+ },
512
+ "Priority": 10,
513
+ "RuleId": {
514
+ "Ref": "WAFWhitelistRule"
515
+ }
516
+ }]
517
+ }
518
+ },
519
+ "LambdaRoleLogParser": {
520
+ "Type": "AWS::IAM::Role",
521
+ "Condition": "LogParserActivated",
522
+ "Properties": {
523
+ "AssumeRolePolicyDocument": {
524
+ "Version": "2012-10-17",
525
+ "Statement": [{
526
+ "Effect": "Allow",
527
+ "Principal": {
528
+ "Service": ["lambda.amazonaws.com"]
529
+ },
530
+ "Action": ["sts:AssumeRole"]
531
+ }]
532
+ },
533
+ "Path": "/",
534
+ "Policies": [{
535
+ "PolicyName": "S3Access",
536
+ "PolicyDocument": {
537
+ "Version": "2012-10-17",
538
+ "Statement": [{
539
+ "Effect": "Allow",
540
+ "Action": "s3:GetObject",
541
+ "Resource": {
542
+ "Fn::Join": ["", ["arn:aws:s3:::", {
543
+ "Ref": "CloudFrontAccessLogBucket"
544
+ }, "/*"]]
545
+ }
546
+ }]
547
+ }
548
+ }, {
549
+ "PolicyName": "S3AccessPut",
550
+ "PolicyDocument": {
551
+ "Version": "2012-10-17",
552
+ "Statement": [{
553
+ "Effect": "Allow",
554
+ "Action": "s3:PutObject",
555
+ "Resource": {
556
+ "Fn::Join": ["", ["arn:aws:s3:::", {
557
+ "Ref": "CloudFrontAccessLogBucket"
558
+ }, "/aws-waf-security-automations-current-blocked-ips.json"]]
559
+ }
560
+ }]
561
+ }
562
+ }, {
563
+ "PolicyName": "WAFGetChangeToken",
564
+ "PolicyDocument": {
565
+ "Statement": [{
566
+ "Effect": "Allow",
567
+ "Action": "waf:GetChangeToken",
568
+ "Resource": "*"
569
+ }]
570
+ }
571
+ }, {
572
+ "PolicyName": "WAFGetAndUpdateIPSet",
573
+ "PolicyDocument": {
574
+ "Statement": [{
575
+ "Effect": "Allow",
576
+ "Action": [
577
+ "waf:GetIPSet",
578
+ "waf:UpdateIPSet"
579
+ ],
580
+ "Resource": [{
581
+ "Fn::Join": [
582
+ "", [
583
+ "arn:aws:waf::", {
584
+ "Ref": "AWS::AccountId"
585
+ },
586
+ ":ipset/", {
587
+ "Ref": "WAFBlacklistSet"
588
+ }
589
+ ]
590
+ ]
591
+ }, {
592
+ "Fn::Join": [
593
+ "", [
594
+ "arn:aws:waf::", {
595
+ "Ref": "AWS::AccountId"
596
+ },
597
+ ":ipset/", {
598
+ "Ref": "WAFAutoBlockSet"
599
+ }
600
+ ]
601
+ ]
602
+ }]
603
+ }]
604
+ }
605
+ }, {
606
+ "PolicyName": "LogsAccess",
607
+ "PolicyDocument": {
608
+ "Version": "2012-10-17",
609
+ "Statement": [{
610
+ "Effect": "Allow",
611
+ "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
612
+ "Resource": { "Fn::Join" : [":", ["arn:aws:logs",{"Ref" : "AWS::Region"},{ "Ref" : "AWS::AccountId" }, "log-group:/aws/lambda/*" ]]}
613
+ }]
614
+ }
615
+ }, {
616
+ "PolicyName": "CloudWatchAccess",
617
+ "PolicyDocument": {
618
+ "Version": "2012-10-17",
619
+ "Statement": [{
620
+ "Effect": "Allow",
621
+ "Action": "cloudwatch:GetMetricStatistics",
622
+ "Resource": "*"
623
+ }]
624
+ }
625
+ }]
626
+ }
627
+ },
628
+ "LambdaWAFLogParserFunction": {
629
+ "Type": "AWS::Lambda::Function",
630
+ "Condition": "LogParserActivated",
631
+ "DependsOn": ["LambdaRoleLogParser", "WAFBlacklistSet", "WAFAutoBlockSet"],
632
+ "Properties": {
633
+ "Description": {
634
+ "Fn::Join": ["", [
635
+ "This function parses CloudFront access logs to identify suspicious behavior, such as an abnormal amount of requests or errors. It then blocks those IP addresses for a customer-defined period of time. Parameters: ", {
636
+ "Ref": "RequestThreshold"
637
+ },
638
+ ",", {
639
+ "Ref": "ErrorThreshold"
640
+ },
641
+ ",", {
642
+ "Ref": "WAFBlockPeriod"
643
+ },
644
+ "."
645
+ ]]
646
+ },
647
+ "Handler": "log-parser.lambda_handler",
648
+ "Role": {
649
+ "Fn::GetAtt": ["LambdaRoleLogParser", "Arn"]
650
+ },
651
+ "Code": {
652
+ "S3Bucket": {
653
+ "Fn::Join": ["-", [
654
+ "solutions", {
655
+ "Ref": "AWS::Region"
656
+ }
657
+ ]]
658
+ },
659
+ "S3Key": "aws-waf-security-automations/v2/log-parser.zip"
660
+ },
661
+ "Environment": {
662
+ "Variables": {
663
+ "OUTPUT_BUCKET": {
664
+ "Ref": "CloudFrontAccessLogBucket"
665
+ },
666
+ "IP_SET_ID_BLACKLIST": {
667
+ "Ref": "WAFBlacklistSet"
668
+ },
669
+ "IP_SET_ID_AUTO_BLOCK": {
670
+ "Ref": "WAFAutoBlockSet"
671
+ },
672
+ "BLACKLIST_BLOCK_PERIOD": {
673
+ "Ref": "WAFBlockPeriod"
674
+ },
675
+ "REQUEST_PER_MINUTE_LIMIT": {
676
+ "Ref": "RequestThreshold"
677
+ },
678
+ "ERROR_PER_MINUTE_LIMIT": {
679
+ "Ref": "ErrorThreshold"
680
+ },
681
+ "SEND_ANONYMOUS_USAGE_DATA": {
682
+ "Ref": "SendAnonymousUsageData"
683
+ },
684
+ "UUID": {
685
+ "Fn::GetAtt": ["CreateUniqueID", "UUID"]
686
+ },
687
+ "LIMIT_IP_ADDRESS_RANGES_PER_IP_MATCH_CONDITION": "10000",
688
+ "MAX_AGE_TO_UPDATE": "30",
689
+ "REGION": {
690
+ "Ref": "AWS::Region"
691
+ },
692
+ "LOG_TYPE": "cloudfront"
693
+ }
694
+ },
695
+ "Runtime": "python2.7",
696
+ "MemorySize": "512",
697
+ "Timeout": "300"
698
+ }
699
+ },
700
+ "LambdaInvokePermissionLogParser": {
701
+ "Type": "AWS::Lambda::Permission",
702
+ "Condition": "LogParserActivated",
703
+ "DependsOn": "LambdaWAFLogParserFunction",
704
+ "Properties": {
705
+ "FunctionName": {
706
+ "Fn::GetAtt": ["LambdaWAFLogParserFunction", "Arn"]
707
+ },
708
+ "Action": "lambda:*",
709
+ "Principal": "s3.amazonaws.com",
710
+ "SourceAccount": {
711
+ "Ref": "AWS::AccountId"
712
+ }
713
+ }
714
+ },
715
+ "LambdaRoleReputationListsParser": {
716
+ "Type": "AWS::IAM::Role",
717
+ "Condition": "ReputationListsProtectionActivated",
718
+ "Properties": {
719
+ "AssumeRolePolicyDocument": {
720
+ "Statement": [{
721
+ "Effect": "Allow",
722
+ "Principal": {
723
+ "Service": [
724
+ "lambda.amazonaws.com"
725
+ ]
726
+ },
727
+ "Action": "sts:AssumeRole"
728
+ }]
729
+ },
730
+ "Policies": [{
731
+ "PolicyName": "CloudWatchLogs",
732
+ "PolicyDocument": {
733
+ "Statement": [{
734
+ "Effect": "Allow",
735
+ "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
736
+ "Resource": { "Fn::Join" : [":", ["arn:aws:logs",{"Ref" : "AWS::Region"},{ "Ref" : "AWS::AccountId" }, "log-group:/aws/lambda/*" ]]}
737
+ }]
738
+ }
739
+ }, {
740
+ "PolicyName": "WAFGetChangeToken",
741
+ "PolicyDocument": {
742
+ "Statement": [{
743
+ "Effect": "Allow",
744
+ "Action": "waf:GetChangeToken",
745
+ "Resource": "*"
746
+ }]
747
+ }
748
+ }, {
749
+ "PolicyName": "WAFGetAndUpdateIPSet",
750
+ "PolicyDocument": {
751
+ "Statement": [{
752
+ "Effect": "Allow",
753
+ "Action": [
754
+ "waf:GetIPSet",
755
+ "waf:UpdateIPSet"
756
+ ],
757
+ "Resource": [{
758
+ "Fn::Join": [
759
+ "", [
760
+ "arn:aws:waf::", {
761
+ "Ref": "AWS::AccountId"
762
+ },
763
+ ":ipset/", {
764
+ "Ref": "WAFReputationListsSet1"
765
+ }
766
+ ]
767
+ ]
768
+ }, {
769
+ "Fn::Join": [
770
+ "", [
771
+ "arn:aws:waf::", {
772
+ "Ref": "AWS::AccountId"
773
+ },
774
+ ":ipset/", {
775
+ "Ref": "WAFReputationListsSet2"
776
+ }
777
+ ]
778
+ ]
779
+ }]
780
+ }]
781
+ }
782
+ }, {
783
+ "PolicyName": "CloudFormationAccess",
784
+ "PolicyDocument": {
785
+ "Version": "2012-10-17",
786
+ "Statement": [{
787
+ "Effect": "Allow",
788
+ "Action": "cloudformation:DescribeStacks",
789
+ "Resource": {
790
+ "Fn::Join": [
791
+ "", [
792
+ "arn:aws:cloudformation:", {
793
+ "Ref": "AWS::Region"
794
+ },
795
+ ":", {
796
+ "Ref": "AWS::AccountId"
797
+ },
798
+ ":stack/", {
799
+ "Ref": "AWS::StackName"
800
+ },
801
+ "/*"
802
+ ]
803
+ ]
804
+ }
805
+ }]
806
+ }
807
+ }, {
808
+ "PolicyName": "CloudWatchAccess",
809
+ "PolicyDocument": {
810
+ "Version": "2012-10-17",
811
+ "Statement": [{
812
+ "Effect": "Allow",
813
+ "Action": "cloudwatch:GetMetricStatistics",
814
+ "Resource": "*"
815
+ }]
816
+ }
817
+ }]
818
+ }
819
+ },
820
+ "LambdaWAFReputationListsParserFunction": {
821
+ "Type": "AWS::Lambda::Function",
822
+ "Condition": "ReputationListsProtectionActivated",
823
+ "DependsOn": "LambdaRoleReputationListsParser",
824
+ "Properties": {
825
+ "Description": "This lambda function checks third-party IP reputation lists hourly for new IP ranges to block. These lists include the Spamhaus Dont Route Or Peer (DROP) and Extended Drop (EDROP) lists, the Proofpoint Emerging Threats IP list, and the Tor exit node list.",
826
+ "Handler": "reputation-lists-parser.handler",
827
+ "Role": {
828
+ "Fn::GetAtt": [
829
+ "LambdaRoleReputationListsParser",
830
+ "Arn"
831
+ ]
832
+ },
833
+ "Code": {
834
+ "S3Bucket": {
835
+ "Fn::Join": ["-", [
836
+ "solutions", {
837
+ "Ref": "AWS::Region"
838
+ }
839
+ ]]
840
+ },
841
+ "S3Key": "aws-waf-security-automations/v3/reputation-lists-parser.zip"
842
+ },
843
+ "Runtime": "nodejs6.10",
844
+ "MemorySize": "128",
845
+ "Timeout": "300"
846
+ }
847
+ },
848
+ "LambdaWAFReputationListsParserEventsRule": {
849
+ "Type": "AWS::Events::Rule",
850
+ "Condition": "ReputationListsProtectionActivated",
851
+ "DependsOn": ["LambdaWAFReputationListsParserFunction", "WAFReputationListsSet1", "WAFReputationListsSet2"],
852
+ "Properties": {
853
+ "Description": "Security Automations - WAF Reputation Lists",
854
+ "ScheduleExpression": "rate(1 hour)",
855
+ "Targets": [{
856
+ "Arn": {
857
+ "Fn::GetAtt": [
858
+ "LambdaWAFReputationListsParserFunction",
859
+ "Arn"
860
+ ]
861
+ },
862
+ "Id": "LambdaWAFReputationListsParserFunction",
863
+ "Input": {
864
+ "Fn::Join": [
865
+ "", [
866
+ "{\"lists\":",
867
+ "[{\"url\":\"https://www.spamhaus.org/drop/drop.txt\"},{\"url\":\"https://check.torproject.org/exit-addresses\",\"prefix\":\"ExitAddress \"},{\"url\":\"https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt\"}]",
868
+ ",\"logType\":\"cloudfront\"",
869
+ ",\"region\":\"", {
870
+ "Ref": "AWS::Region"
871
+ }, "\",",
872
+ "\"ipSetIds\": [",
873
+ "\"", {
874
+ "Ref": "WAFReputationListsSet1"
875
+ },
876
+ "\",",
877
+ "\"", {
878
+ "Ref": "WAFReputationListsSet2"
879
+ },
880
+ "\"",
881
+ "]}"
882
+ ]
883
+ ]
884
+ }
885
+ }]
886
+ }
887
+ },
888
+ "LambdaInvokePermissionReputationListsParser": {
889
+ "Type": "AWS::Lambda::Permission",
890
+ "Condition": "ReputationListsProtectionActivated",
891
+ "DependsOn": ["LambdaWAFReputationListsParserFunction", "LambdaWAFReputationListsParserEventsRule"],
892
+ "Properties": {
893
+ "FunctionName": {
894
+ "Ref": "LambdaWAFReputationListsParserFunction"
895
+ },
896
+ "Action": "lambda:InvokeFunction",
897
+ "Principal": "events.amazonaws.com",
898
+ "SourceArn": {
899
+ "Fn::GetAtt": [
900
+ "LambdaWAFReputationListsParserEventsRule",
901
+ "Arn"
902
+ ]
903
+ }
904
+ }
905
+ },
906
+ "LambdaRoleBadBot": {
907
+ "Type": "AWS::IAM::Role",
908
+ "Condition": "BadBotProtectionActivated",
909
+ "Properties": {
910
+ "AssumeRolePolicyDocument": {
911
+ "Version": "2012-10-17",
912
+ "Statement": [{
913
+ "Effect": "Allow",
914
+ "Principal": {
915
+ "Service": ["lambda.amazonaws.com"]
916
+ },
917
+ "Action": ["sts:AssumeRole"]
918
+ }]
919
+ },
920
+ "Path": "/",
921
+ "Policies": [{
922
+ "PolicyName": "WAFGetChangeToken",
923
+ "PolicyDocument": {
924
+ "Statement": [{
925
+ "Effect": "Allow",
926
+ "Action": "waf:GetChangeToken",
927
+ "Resource": "*"
928
+ }]
929
+ }
930
+ }, {
931
+ "PolicyName": "WAFGetAndUpdateIPSet",
932
+ "PolicyDocument": {
933
+ "Statement": [{
934
+ "Effect": "Allow",
935
+ "Action": [
936
+ "waf:GetIPSet",
937
+ "waf:UpdateIPSet"
938
+ ],
939
+ "Resource": {
940
+ "Fn::Join": [
941
+ "", [
942
+ "arn:aws:waf::", {
943
+ "Ref": "AWS::AccountId"
944
+ },
945
+ ":ipset/", {
946
+ "Ref": "WAFBadBotSet"
947
+ }
948
+ ]
949
+ ]
950
+ }
951
+ }]
952
+ }
953
+ }, {
954
+ "PolicyName": "LogsAccess",
955
+ "PolicyDocument": {
956
+ "Version": "2012-10-17",
957
+ "Statement": [{
958
+ "Effect": "Allow",
959
+ "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
960
+ "Resource": { "Fn::Join" : [":", ["arn:aws:logs",{"Ref" : "AWS::Region"},{ "Ref" : "AWS::AccountId" }, "log-group:/aws/lambda/*" ]]}
961
+ }]
962
+ }
963
+ }, {
964
+ "PolicyName": "CloudFormationAccess",
965
+ "PolicyDocument": {
966
+ "Version": "2012-10-17",
967
+ "Statement": [{
968
+ "Effect": "Allow",
969
+ "Action": "cloudformation:DescribeStacks",
970
+ "Resource": {
971
+ "Fn::Join": [
972
+ "", [
973
+ "arn:aws:cloudformation:", {
974
+ "Ref": "AWS::Region"
975
+ },
976
+ ":", {
977
+ "Ref": "AWS::AccountId"
978
+ },
979
+ ":stack/", {
980
+ "Ref": "AWS::StackName"
981
+ },
982
+ "/*"
983
+ ]
984
+ ]
985
+ }
986
+ }]
987
+ }
988
+ }, {
989
+ "PolicyName": "CloudWatchAccess",
990
+ "PolicyDocument": {
991
+ "Version": "2012-10-17",
992
+ "Statement": [{
993
+ "Effect": "Allow",
994
+ "Action": "cloudwatch:GetMetricStatistics",
995
+ "Resource": "*"
996
+ }]
997
+ }
998
+ }]
999
+ }
1000
+ },
1001
+ "LambdaWAFBadBotParserFunction": {
1002
+ "Type": "AWS::Lambda::Function",
1003
+ "Condition": "BadBotProtectionActivated",
1004
+ "DependsOn": "LambdaRoleBadBot",
1005
+ "Properties": {
1006
+ "Description": "This lambda function will intercepts and inspects trap endpoint requests to extract its IP address, and then add it to an AWS WAF block list.",
1007
+ "Handler": "access-handler.lambda_handler",
1008
+ "Role": {
1009
+ "Fn::GetAtt": ["LambdaRoleBadBot", "Arn"]
1010
+ },
1011
+ "Code": {
1012
+ "S3Bucket": {
1013
+ "Fn::Join": ["-", [
1014
+ "solutions", {
1015
+ "Ref": "AWS::Region"
1016
+ }
1017
+ ]]
1018
+ },
1019
+ "S3Key": "aws-waf-security-automations/v2/access-handler.zip"
1020
+ },
1021
+ "Environment": {
1022
+ "Variables": {
1023
+ "IP_SET_ID_BAD_BOT": {
1024
+ "Ref": "WAFBadBotSet"
1025
+ },
1026
+ "SEND_ANONYMOUS_USAGE_DATA": {
1027
+ "Ref": "SendAnonymousUsageData"
1028
+ },
1029
+ "UUID": {
1030
+ "Fn::GetAtt": ["CreateUniqueID", "UUID"]
1031
+ },
1032
+ "REGION": {
1033
+ "Ref": "AWS::Region"
1034
+ },
1035
+ "LOG_TYPE": "cloudfront"
1036
+ }
1037
+ },
1038
+ "Runtime": "python2.7",
1039
+ "MemorySize": "128",
1040
+ "Timeout": "300"
1041
+ }
1042
+ },
1043
+ "LambdaInvokePermissionBadBot": {
1044
+ "Type": "AWS::Lambda::Permission",
1045
+ "Condition": "BadBotProtectionActivated",
1046
+ "DependsOn": "LambdaWAFBadBotParserFunction",
1047
+ "Properties": {
1048
+ "FunctionName": {
1049
+ "Fn::GetAtt": ["LambdaWAFBadBotParserFunction", "Arn"]
1050
+ },
1051
+ "Action": "lambda:*",
1052
+ "Principal": "apigateway.amazonaws.com"
1053
+ }
1054
+ },
1055
+ "ApiGatewayBadBot": {
1056
+ "Type": "AWS::ApiGateway::RestApi",
1057
+ "Condition": "BadBotProtectionActivated",
1058
+ "Properties": {
1059
+ "Name": "Security Automations - WAF Bad Bot API",
1060
+ "Description": "API created by AWS WAF Security Automations CloudFormation template. This endpoint will be used to capture bad bots."
1061
+ }
1062
+ },
1063
+ "ApiGatewayBadBotResource": {
1064
+ "Type": "AWS::ApiGateway::Resource",
1065
+ "Properties": {
1066
+ "RestApiId": {
1067
+ "Ref": "ApiGatewayBadBot"
1068
+ },
1069
+ "ParentId": {
1070
+ "Fn::GetAtt": ["ApiGatewayBadBot", "RootResourceId"]
1071
+ },
1072
+ "PathPart": "{proxy+}"
1073
+ }
1074
+ },
1075
+ "ApiGatewayBadBotMethodRoot": {
1076
+ "Type": "AWS::ApiGateway::Method",
1077
+ "Condition": "BadBotProtectionActivated",
1078
+ "DependsOn": ["LambdaWAFBadBotParserFunction", "LambdaInvokePermissionBadBot", "ApiGatewayBadBot"],
1079
+ "Properties": {
1080
+ "RestApiId": {
1081
+ "Ref": "ApiGatewayBadBot"
1082
+ },
1083
+ "ResourceId": {
1084
+ "Fn::GetAtt": ["ApiGatewayBadBot", "RootResourceId"]
1085
+ },
1086
+ "HttpMethod": "ANY",
1087
+ "AuthorizationType": "NONE",
1088
+ "RequestParameters": {
1089
+ "method.request.header.X-Forwarded-For": false
1090
+ },
1091
+ "Integration": {
1092
+ "Type": "AWS_PROXY",
1093
+ "IntegrationHttpMethod": "POST",
1094
+ "Uri": {
1095
+ "Fn::Join": ["", [
1096
+ "arn:aws:apigateway:", {
1097
+ "Ref": "AWS::Region"
1098
+ },
1099
+ ":lambda:path/2015-03-31/functions/", {
1100
+ "Fn::GetAtt": ["LambdaWAFBadBotParserFunction", "Arn"]
1101
+ },
1102
+ "/invocations"
1103
+ ]]
1104
+ }
1105
+ }
1106
+ }
1107
+ },
1108
+ "ApiGatewayBadBotMethod": {
1109
+ "Type": "AWS::ApiGateway::Method",
1110
+ "Condition": "BadBotProtectionActivated",
1111
+ "DependsOn": ["LambdaWAFBadBotParserFunction", "LambdaInvokePermissionBadBot", "ApiGatewayBadBot"],
1112
+ "Properties": {
1113
+ "RestApiId": {
1114
+ "Ref": "ApiGatewayBadBot"
1115
+ },
1116
+ "ResourceId": {
1117
+ "Ref": "ApiGatewayBadBotResource"
1118
+ },
1119
+ "HttpMethod": "ANY",
1120
+ "AuthorizationType": "NONE",
1121
+ "RequestParameters": {
1122
+ "method.request.header.X-Forwarded-For": false
1123
+ },
1124
+ "Integration": {
1125
+ "Type": "AWS_PROXY",
1126
+ "IntegrationHttpMethod": "POST",
1127
+ "Uri": {
1128
+ "Fn::Join": ["", [
1129
+ "arn:aws:apigateway:", {
1130
+ "Ref": "AWS::Region"
1131
+ },
1132
+ ":lambda:path/2015-03-31/functions/", {
1133
+ "Fn::GetAtt": ["LambdaWAFBadBotParserFunction", "Arn"]
1134
+ },
1135
+ "/invocations"
1136
+ ]]
1137
+ }
1138
+ }
1139
+ }
1140
+ },
1141
+ "ApiGatewayBadBotDeployment": {
1142
+ "Type": "AWS::ApiGateway::Deployment",
1143
+ "Condition": "BadBotProtectionActivated",
1144
+ "DependsOn": "ApiGatewayBadBotMethod",
1145
+ "Properties": {
1146
+ "RestApiId": {
1147
+ "Ref": "ApiGatewayBadBot"
1148
+ },
1149
+ "Description": "CloudFormation Deployment Stage",
1150
+ "StageName": "CFDeploymentStage"
1151
+ }
1152
+ },
1153
+ "ApiGatewayBadBotStage": {
1154
+ "Type": "AWS::ApiGateway::Stage",
1155
+ "Condition": "BadBotProtectionActivated",
1156
+ "DependsOn": "ApiGatewayBadBotDeployment",
1157
+ "Properties": {
1158
+ "DeploymentId": {
1159
+ "Ref": "ApiGatewayBadBotDeployment"
1160
+ },
1161
+ "Description": "Production Stage",
1162
+ "RestApiId": {
1163
+ "Ref": "ApiGatewayBadBot"
1164
+ },
1165
+ "StageName": "ProdStage"
1166
+ }
1167
+ },
1168
+ "LambdaRoleCustomResource": {
1169
+ "Type": "AWS::IAM::Role",
1170
+ "Condition": "CreateWebACL",
1171
+ "DependsOn": "WAFWebACL",
1172
+ "Properties": {
1173
+ "AssumeRolePolicyDocument": {
1174
+ "Version": "2012-10-17",
1175
+ "Statement": [{
1176
+ "Effect": "Allow",
1177
+ "Principal": {
1178
+ "Service": ["lambda.amazonaws.com"]
1179
+ },
1180
+ "Action": ["sts:AssumeRole"]
1181
+ }]
1182
+ },
1183
+ "Path": "/",
1184
+ "Policies": [{
1185
+ "PolicyName": "S3Access",
1186
+ "PolicyDocument": {
1187
+ "Version": "2012-10-17",
1188
+ "Statement": [{
1189
+ "Effect": "Allow",
1190
+ "Action": [
1191
+ "s3:CreateBucket",
1192
+ "s3:GetBucketLocation",
1193
+ "s3:GetBucketNotification",
1194
+ "s3:GetObject",
1195
+ "s3:ListBucket",
1196
+ "s3:PutBucketNotification"
1197
+ ],
1198
+ "Resource": {
1199
+ "Fn::Join": ["", ["arn:aws:s3:::", {
1200
+ "Ref": "CloudFrontAccessLogBucket"
1201
+ }]]
1202
+ }
1203
+ }]
1204
+ }
1205
+ }, {
1206
+ "PolicyName": "LambdaAccess",
1207
+ "PolicyDocument": {
1208
+ "Version": "2012-10-17",
1209
+ "Statement": [{
1210
+ "Effect": "Allow",
1211
+ "Action": "lambda:InvokeFunction",
1212
+ "Resource": {
1213
+ "Fn::Join": ["", ["arn:aws:lambda:", {
1214
+ "Ref": "AWS::Region"
1215
+ },
1216
+ ":", {
1217
+ "Ref": "AWS::AccountId"
1218
+ },
1219
+ ":function:", {
1220
+ "Ref": "AWS::StackName"
1221
+ },
1222
+ "-LambdaWAFReputationLists*"
1223
+ ]]
1224
+ }
1225
+ }]
1226
+ }
1227
+ }, {
1228
+ "PolicyName": "WAFAccess",
1229
+ "PolicyDocument": {
1230
+ "Version": "2012-10-17",
1231
+ "Statement": [{
1232
+ "Effect": "Allow",
1233
+ "Action": [
1234
+ "waf:GetWebACL",
1235
+ "waf:UpdateWebACL"
1236
+ ],
1237
+ "Resource": {
1238
+ "Fn::Join": ["", ["arn:aws:waf::", {
1239
+ "Ref": "AWS::AccountId"
1240
+ },
1241
+ ":webacl/", {
1242
+ "Ref": "WAFWebACL"
1243
+ }
1244
+ ]]
1245
+ }
1246
+ }]
1247
+ }
1248
+ }, {
1249
+ "PolicyName": "WAFRuleAccess",
1250
+ "PolicyDocument": {
1251
+ "Version": "2012-10-17",
1252
+ "Statement": [{
1253
+ "Effect": "Allow",
1254
+ "Action": "waf:GetRule",
1255
+ "Resource": {
1256
+ "Fn::Join": ["", ["arn:aws:waf::", {
1257
+ "Ref": "AWS::AccountId"
1258
+ },
1259
+ ":rule/*"
1260
+ ]]
1261
+ }
1262
+ }]
1263
+ }
1264
+ }, {
1265
+ "PolicyName": "CloudFormationAccess",
1266
+ "PolicyDocument": {
1267
+ "Version": "2012-10-17",
1268
+ "Statement": [{
1269
+ "Effect": "Allow",
1270
+ "Action": "cloudformation:DescribeStacks",
1271
+ "Resource": {
1272
+ "Fn::Join": [
1273
+ "", [
1274
+ "arn:aws:cloudformation:", {
1275
+ "Ref": "AWS::Region"
1276
+ },
1277
+ ":", {
1278
+ "Ref": "AWS::AccountId"
1279
+ },
1280
+ ":stack/", {
1281
+ "Ref": "AWS::StackName"
1282
+ },
1283
+ "/*"
1284
+ ]
1285
+ ]
1286
+ }
1287
+ }]
1288
+ }
1289
+ }, {
1290
+ "PolicyName": "WAFGetChangeToken",
1291
+ "PolicyDocument": {
1292
+ "Statement": [{
1293
+ "Effect": "Allow",
1294
+ "Action": "waf:GetChangeToken",
1295
+ "Resource": "*"
1296
+ }]
1297
+ }
1298
+ }, {
1299
+ "PolicyName": "LogsAccess",
1300
+ "PolicyDocument": {
1301
+ "Version": "2012-10-17",
1302
+ "Statement": [{
1303
+ "Effect": "Allow",
1304
+ "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
1305
+ "Resource": { "Fn::Join" : [":", ["arn:aws:logs",{"Ref" : "AWS::Region"},{ "Ref" : "AWS::AccountId" }, "log-group:/aws/lambda/*" ]]}
1306
+ }]
1307
+ }
1308
+ }]
1309
+ }
1310
+ },
1311
+ "LambdaWAFCustomResourceFunction": {
1312
+ "Type": "AWS::Lambda::Function",
1313
+ "Condition": "CreateWebACL",
1314
+ "DependsOn": "LambdaRoleCustomResource",
1315
+ "Properties": {
1316
+ "Description": {
1317
+ "Fn::Join": ["", [
1318
+ "This lambda function configures the Web ACL rules based on the features enabled in the CloudFormation template. Parameters: ", {
1319
+ "Ref": "SendAnonymousUsageData"
1320
+ },
1321
+ "."
1322
+ ]]
1323
+ },
1324
+ "Handler": "custom-resource.lambda_handler",
1325
+ "Role": {
1326
+ "Fn::GetAtt": ["LambdaRoleCustomResource", "Arn"]
1327
+ },
1328
+ "Code": {
1329
+ "S3Bucket": {
1330
+ "Fn::Join": ["-", [
1331
+ "solutions", {
1332
+ "Ref": "AWS::Region"
1333
+ }
1334
+ ]]
1335
+ },
1336
+ "S3Key": "aws-waf-security-automations/v3/custom-resource.zip"
1337
+ },
1338
+ "Runtime": "python2.7",
1339
+ "MemorySize": "128",
1340
+ "Timeout": "300"
1341
+ }
1342
+ },
1343
+ "WafWebAclRuleControler": {
1344
+ "Type": "Custom::WafWebAclRuleControler",
1345
+ "Condition": "CreateWebACL",
1346
+ "DependsOn": ["LambdaWAFCustomResourceFunction", "WAFWebACL"],
1347
+ "Properties": {
1348
+ "ServiceToken": {
1349
+ "Fn::GetAtt": ["LambdaWAFCustomResourceFunction", "Arn"]
1350
+ },
1351
+ "WAFWebACL": {
1352
+ "Ref": "WAFWebACL"
1353
+ },
1354
+ "Region": {
1355
+ "Ref": "AWS::Region"
1356
+ },
1357
+ "LambdaWAFReputationListsParserFunction": {
1358
+ "Fn::If": ["ReputationListsProtectionActivated", {
1359
+ "Fn::GetAtt": ["LambdaWAFReputationListsParserFunction", "Arn"]
1360
+ }, {
1361
+ "Ref": "AWS::NoValue"
1362
+ }]
1363
+ },
1364
+ "WAFReputationListsSet1": {
1365
+ "Fn::If": ["ReputationListsProtectionActivated", {
1366
+ "Ref": "WAFReputationListsSet1"
1367
+ }, {
1368
+ "Ref": "AWS::NoValue"
1369
+ }]
1370
+ },
1371
+ "WAFReputationListsSet2": {
1372
+ "Fn::If": ["ReputationListsProtectionActivated", {
1373
+ "Ref": "WAFReputationListsSet2"
1374
+ }, {
1375
+ "Ref": "AWS::NoValue"
1376
+ }]
1377
+ },
1378
+ "CloudFrontAccessLogBucket": {
1379
+ "Fn::If": ["LogParserActivated", {
1380
+ "Ref": "CloudFrontAccessLogBucket"
1381
+ }, {
1382
+ "Ref": "AWS::NoValue"
1383
+ }]
1384
+ },
1385
+ "LambdaWAFLogParserFunction": {
1386
+ "Fn::If": ["LogParserActivated", {
1387
+ "Fn::GetAtt": ["LambdaWAFLogParserFunction", "Arn"]
1388
+ }, {
1389
+ "Ref": "AWS::NoValue"
1390
+ }]
1391
+ },
1392
+ "WAFWhitelistRule": {
1393
+ "Fn::If": ["CreateWebACL", {
1394
+ "Ref": "WAFWhitelistRule"
1395
+ }, {
1396
+ "Ref": "AWS::NoValue"
1397
+ }]
1398
+ },
1399
+ "WAFBlacklistRule": {
1400
+ "Fn::If": ["LogParserActivated", {
1401
+ "Ref": "WAFBlacklistRule"
1402
+ }, {
1403
+ "Ref": "AWS::NoValue"
1404
+ }]
1405
+ },
1406
+ "WAFAutoBlockRule": {
1407
+ "Fn::If": ["LogParserActivated", {
1408
+ "Ref": "WAFAutoBlockRule"
1409
+ }, {
1410
+ "Ref": "AWS::NoValue"
1411
+ }]
1412
+ },
1413
+ "WAFIPReputationListsRule1": {
1414
+ "Fn::If": ["ReputationListsProtectionActivated", {
1415
+ "Ref": "WAFIPReputationListsRule1"
1416
+ }, {
1417
+ "Ref": "AWS::NoValue"
1418
+ }]
1419
+ },
1420
+ "WAFIPReputationListsRule2": {
1421
+ "Fn::If": ["ReputationListsProtectionActivated", {
1422
+ "Ref": "WAFIPReputationListsRule2"
1423
+ }, {
1424
+ "Ref": "AWS::NoValue"
1425
+ }]
1426
+ },
1427
+ "WAFBadBotRule": {
1428
+ "Fn::If": ["BadBotProtectionActivated", {
1429
+ "Ref": "WAFBadBotRule"
1430
+ }, {
1431
+ "Ref": "AWS::NoValue"
1432
+ }]
1433
+ },
1434
+ "WAFSqlInjectionRule": {
1435
+ "Fn::If": ["SqlInjectionProtectionActivated", {
1436
+ "Ref": "WAFSqlInjectionRule"
1437
+ }, {
1438
+ "Ref": "AWS::NoValue"
1439
+ }]
1440
+ },
1441
+ "WAFXssRule": {
1442
+ "Fn::If": ["CrossSiteScriptingProtectionActivated", {
1443
+ "Ref": "WAFXssRule"
1444
+ }, {
1445
+ "Ref": "AWS::NoValue"
1446
+ }]
1447
+ },
1448
+ "SqlInjectionProtection": {
1449
+ "Ref": "SqlInjectionProtectionParam"
1450
+ },
1451
+ "CrossSiteScriptingProtection": {
1452
+ "Ref": "CrossSiteScriptingProtectionParam"
1453
+ },
1454
+ "ActivateHttpFloodProtection": {
1455
+ "Ref": "ActivateHttpFloodProtectionParam"
1456
+ },
1457
+ "ActivateScansProbesProtection": {
1458
+ "Ref": "ActivateScansProbesProtectionParam"
1459
+ },
1460
+ "ActivateReputationListsProtection": {
1461
+ "Ref": "ActivateReputationListsProtectionParam"
1462
+ },
1463
+ "ActivateBadBotProtection": {
1464
+ "Ref": "ActivateBadBotProtectionParam"
1465
+ },
1466
+ "RequestThreshold": {
1467
+ "Ref": "RequestThreshold"
1468
+ },
1469
+ "ErrorThreshold": {
1470
+ "Ref": "ErrorThreshold"
1471
+ },
1472
+ "WAFBlockPeriod": {
1473
+ "Ref": "WAFBlockPeriod"
1474
+ },
1475
+ "SendAnonymousUsageData": {
1476
+ "Ref": "SendAnonymousUsageData"
1477
+ },
1478
+ "UUID": {
1479
+ "Fn::GetAtt": ["CreateUniqueID", "UUID"]
1480
+ },
1481
+ "LOG_TYPE": "cloudfront"
1482
+ }
1483
+ },
1484
+ "SolutionHelperRole": {
1485
+ "Type": "AWS::IAM::Role",
1486
+ "Properties": {
1487
+ "AssumeRolePolicyDocument": {
1488
+ "Version": "2012-10-17",
1489
+ "Statement": [{
1490
+ "Effect": "Allow",
1491
+ "Principal": {
1492
+ "Service": "lambda.amazonaws.com"
1493
+ },
1494
+ "Action": "sts:AssumeRole"
1495
+ }]
1496
+ },
1497
+ "Path": "/",
1498
+ "Policies": [{
1499
+ "PolicyName": "Solution_Helper_Permissions",
1500
+ "PolicyDocument": {
1501
+ "Version": "2012-10-17",
1502
+ "Statement": [{
1503
+ "Effect": "Allow",
1504
+ "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
1505
+ "Resource": { "Fn::Join" : [":", ["arn:aws:logs",{"Ref" : "AWS::Region"},{ "Ref" : "AWS::AccountId" }, "log-group:/aws/lambda/*" ]]}
1506
+ }]
1507
+ }
1508
+ }]
1509
+ }
1510
+ },
1511
+ "SolutionHelper": {
1512
+ "Type": "AWS::Lambda::Function",
1513
+ "DependsOn": "SolutionHelperRole",
1514
+ "Properties": {
1515
+ "Handler": "solution-helper.lambda_handler",
1516
+ "Role": {
1517
+ "Fn::GetAtt": [
1518
+ "SolutionHelperRole",
1519
+ "Arn"
1520
+ ]
1521
+ },
1522
+ "Description": "This lambda function executes generic common tasks to support this solution.",
1523
+ "Code": {
1524
+ "S3Bucket": {
1525
+ "Fn::Join": [
1526
+ "", [
1527
+ "solutions-", {
1528
+ "Ref": "AWS::Region"
1529
+ }
1530
+ ]
1531
+ ]
1532
+ },
1533
+ "S3Key": "library/solution-helper/v1/solution-helper.zip"
1534
+ },
1535
+ "Runtime": "python2.7",
1536
+ "Timeout": "300"
1537
+ }
1538
+ },
1539
+ "CreateUniqueID": {
1540
+ "Type": "Custom::CreateUUID",
1541
+ "DependsOn": "SolutionHelper",
1542
+ "Properties": {
1543
+ "ServiceToken": {
1544
+ "Fn::GetAtt": [
1545
+ "SolutionHelper",
1546
+ "Arn"
1547
+ ]
1548
+ },
1549
+ "Region": {
1550
+ "Ref": "AWS::Region"
1551
+ },
1552
+ "CreateUniqueID": "true"
1553
+ }
1554
+ }
1555
+ },
1556
+ "Outputs": {
1557
+ "BadBotHoneypotEndpoint": {
1558
+ "Description": "Bad Bot Honeypot Endpoint",
1559
+ "Value": {
1560
+ "Fn::Join": ["", [
1561
+ "https://", {
1562
+ "Ref": "ApiGatewayBadBot"
1563
+ },
1564
+ ".execute-api.", {
1565
+ "Ref": "AWS::Region"
1566
+ },
1567
+ ".amazonaws.com/", {
1568
+ "Ref": "ApiGatewayBadBotStage"
1569
+ }
1570
+ ]]
1571
+ },
1572
+ "Condition": "BadBotProtectionActivated"
1573
+ }
1574
+ }
1575
+ }