Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Push WAF block logs to SNS #184

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions aws/cloudwatch-log-extract/lambda-script/lambda_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ def lambda_handler(event, context):
if regex_pattern:
matchObject = re.match(regex_pattern, log_message)
if matchObject:
for key,value in capture_group.items():
publish_dict[value] = matchObject.group(int(key))
if capture_group:
for key,value in capture_group.items():
publish_dict[value] = matchObject.group(int(key))
else:
publish_dict["message"] = log_message


else:
publish_dict["message"] = log_message
Expand Down
6 changes: 4 additions & 2 deletions aws/cloudwatch-log-extract/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ locals {

resource "aws_cloudwatch_log_subscription_filter" "cloudwatch_log_filter" {
name = "cloudwatch_log_filter-${random_id.unique_id.dec}"
log_group_name = var.source_cloudwatch_log_group
log_group_name = data.aws_cloudwatch_log_group.log_group.name
filter_pattern = var.log_group_filter_pattern
destination_arn = aws_lambda_function.sql_query_update.arn

depends_on = [aws_lambda_permission.allow_cloudwatch_logs]
}

resource "aws_lambda_function" "sql_query_update" {
Expand Down Expand Up @@ -104,7 +106,7 @@ resource "aws_lambda_permission" "allow_cloudwatch_logs" {
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.sql_query_update.function_name
principal = "logs.${data.aws_region.current.name}.amazonaws.com"
source_arn = data.aws_cloudwatch_log_group.log_group.arn
source_arn = "${data.aws_cloudwatch_log_group.log_group.arn}*"
}

data "aws_region" "current" {}
Expand Down
10 changes: 10 additions & 0 deletions aws/waf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,20 @@ Note: For each rule, if you are providing a country list, you can only specify e
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | ~> 4.0 |

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_cloudwatch_log_extract"></a> [cloudwatch\_log\_extract](#module\_cloudwatch\_log\_extract) | ../cloudwatch-log-extract | n/a |

## Resources

| Name | Type |
|------|------|
| [aws_cloudwatch_log_group.aws_waf_log_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource |
| [aws_sns_topic.waf_logs_sns_subscription](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource |
| [aws_ssm_parameter.aws_waf_acl](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
| [aws_ssm_parameter.aws_waf_sns_log](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
| [aws_wafv2_ip_set.allowed_ip_list](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_ip_set) | resource |
| [aws_wafv2_ip_set.block_ip_list](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_ip_set) | resource |
| [aws_wafv2_web_acl.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl) | resource |
Expand All @@ -82,6 +90,7 @@ Note: For each rule, if you are providing a country list, you can only specify e
| <a name="input_allowed_ip_list"></a> [allowed\_ip\_list](#input\_allowed\_ip\_list) | List of allowed IP addresses, these IP addresses will be exempted from any configured rules | `list(string)` | `[]` | no |
| <a name="input_aws_managed_rule_groups"></a> [aws\_managed\_rule\_groups](#input\_aws\_managed\_rule\_groups) | Rule statement values used to run the rules that are defined in a managed rule group. You may review this list for the available AWS managed rule groups - https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html | <pre>map(object({<br> name = string # Name of the Managed rule group<br> priority = number # Relative processing order for rules processed by AWS WAF. All rules are processed from lowest priority to the highest.<br> count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`.<br> country_list = optional(list(string), []) # List of countries to apply the managed rule to. If populated, from other countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.<br> exempt_country_list = optional(list(string), []) # List of countries to exempt from the managed rule. If populated, the selected countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.<br> }))</pre> | n/a | yes |
| <a name="input_block_ip_list"></a> [block\_ip\_list](#input\_block\_ip\_list) | List of IP addresses to be blocked and denied access to the ingress / cloudfront. | `list(string)` | `[]` | no |
| <a name="input_header_match_rules"></a> [header\_match\_rules](#input\_header\_match\_rules) | Rule statement to inspect and match the header for an incoming request. | <pre>map(object({<br> name = string # Name of the header match rule group<br> priority = number # Relative processing order for header match rule relative to other rules processed by AWS WAF.<br> header_values = map(object({ # Header values contains a map of headers to inspect. You can provide multiple headers and values, all headers will be inspected together with `AND` logic.<br> header_name = string # This is the name of the header to inspect for all incoming requests.<br> header_value = string # This is the value to look out for a matching header name for all incoming requests<br> not_statement = optional(bool, false) # This indicates if the result this header match should be negated. The negated result will be joined with other header match results using `AND` logic if more than 1 header is provided.<br> }))<br> count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false.<br> }))</pre> | `null` | no |
| <a name="input_name"></a> [name](#input\_name) | Friendly name of the WebACL. | `string` | n/a | yes |
| <a name="input_rate_limit_rules"></a> [rate\_limit\_rules](#input\_rate\_limit\_rules) | Rule statement to track and rate limits requests when they are coming at too fast a rate.. For more details, visit - https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html | <pre>map(object({<br> name = string # Name of the Rate limit rule group<br> priority = number # Relative processing order for rate limit rule relative to other rules processed by AWS WAF.<br> limit = optional(number, 2000) # This is the limit on requests from any single IP address within a 5 minute period<br> count_override = optional(bool, false) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false.<br> country_list = optional(list(string), []) # List of countries to apply the rate limit to. If populated, from other countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.<br> exempt_country_list = optional(list(string), []) # List of countries to exempt from the rate limit. If populated, the selected countries will be ignored by this rule. IF empty, the rule will apply to all traffic. You must either specify country_list or exempt_country_list, but not both.<br> }))</pre> | n/a | yes |
| <a name="input_resource_arn"></a> [resource\_arn](#input\_resource\_arn) | The Amazon Resource Name (ARN) of the resource to associate with the web ACL. This must be an ARN of an Application Load Balancer or an Amazon API Gateway stage. Value is required if scope is REGIONAL | `string` | `null` | no |
Expand All @@ -92,4 +101,5 @@ Note: For each rule, if you are providing a country list, you can only specify e
| Name | Description |
|------|-------------|
| <a name="output_aws_waf_arn"></a> [aws\_waf\_arn](#output\_aws\_waf\_arn) | The arn for AWS WAF WebACL. |
| <a name="output_waf_logs_sns_topic_arn"></a> [waf\_logs\_sns\_topic\_arn](#output\_waf\_logs\_sns\_topic\_arn) | The arn for the SNS topic to receive the AWS WAF logs |
<!-- END_TF_DOCS -->
151 changes: 150 additions & 1 deletion aws/waf/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,133 @@ resource "aws_wafv2_web_acl" "main" {
metric_name = "${var.name}-cloudfront-web-acl"
}

dynamic "rule" {
for_each = var.header_match_rules == null ? {} : var.header_match_rules
content {
name = "${rule.value["name"]}-header-match-rule"
priority = rule.value["priority"]

dynamic "action" {
for_each = rule.value["count_override"] == true ? [1] : []
content {
count {}
}
}
dynamic "action" {
for_each = rule.value["count_override"] == false ? [1] : []
content {
block {}
}
}
dynamic "statement" {
for_each = length(rule.value["header_values"]) == 1 ? rule.value["header_values"] : {}
content {
dynamic "byte_match_statement" {
for_each = statement.value["not_statement"] == false ? [1] : []
content {
field_to_match {
single_header {
name = lower(statement.value["header_name"])
}
}

positional_constraint = "CONTAINS"

search_string = statement.value["header_value"]

text_transformation {
priority = 1
type = "LOWERCASE"
}
}
}
dynamic "not_statement" {
for_each = statement.value["not_statement"] == true ? [1] : []
content {
statement {
byte_match_statement {
field_to_match {
single_header {
name = lower(statement.value["header_name"])
}
}

positional_constraint = "CONTAINS"

search_string = statement.value["header_value"]

text_transformation {
priority = 1
type = "LOWERCASE"
}
}
}
}
}
}
}
dynamic "statement" {
for_each = length(rule.value["header_values"]) > 1 ? [1] : []
content {
and_statement {
dynamic "statement" {
for_each = rule.value["header_values"]
content {
dynamic "byte_match_statement" {
for_each = statement.value["not_statement"] == false ? [1] : []
content {
field_to_match {
single_header {
name = lower(statement.value["header_name"])
}
}

positional_constraint = "CONTAINS"

search_string = statement.value["header_value"]

text_transformation {
priority = 1
type = "LOWERCASE"
}
}
}
dynamic "not_statement" {
for_each = statement.value["not_statement"] == true ? [1] : []
content {
statement {
byte_match_statement {
field_to_match {
single_header {
name = lower(statement.value["header_name"])
}
}

positional_constraint = "CONTAINS"

search_string = statement.value["header_value"]

text_transformation {
priority = 1
type = "LOWERCASE"
}
}
}
}
}
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
sampled_requests_enabled = true
metric_name = "${rule.value["name"]}-header-match-rule"
}
}
}

dynamic "rule" {
for_each = var.rate_limit_rules
content {
Expand Down Expand Up @@ -201,4 +328,26 @@ resource "aws_wafv2_ip_set" "block_ip_list" {
scope = var.waf_scope
ip_address_version = "IPV4"
addresses = var.block_ip_list
}
}

module "cloudwatch_log_extract" {
source = "../cloudwatch-log-extract"

source_cloudwatch_log_group = aws_cloudwatch_log_group.aws_waf_log_group.name
log_group_filter_pattern = "{ $.action = \"BLOCK\" }"
message_attributes = {
waf = aws_wafv2_web_acl.main.id
}
destination_sns_topic_arn = aws_sns_topic.waf_logs_sns_subscription.arn
}

resource "aws_sns_topic" "waf_logs_sns_subscription" {
name = "${aws_wafv2_web_acl.main.id}-waf-logs-topic"
}

resource "aws_ssm_parameter" "aws_waf_sns_log" {
name = "/waflogs/sns/${var.name}"
description = "Name of the SNS for the AWS WAF logs - ${var.name}"
type = "SecureString"
value = aws_sns_topic.waf_logs_sns_subscription.arn
}
5 changes: 5 additions & 0 deletions aws/waf/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ output "aws_waf_arn" {
description = "The arn for AWS WAF WebACL."
value = aws_wafv2_web_acl.main.arn
}

output "waf_logs_sns_topic_arn" {
description = "The arn for the SNS topic to receive the AWS WAF logs"
value = aws_sns_topic.waf_logs_sns_subscription.arn
}
16 changes: 16 additions & 0 deletions aws/waf/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ variable "rate_limit_rules" {
}))
}

variable "header_match_rules" {
description = "Rule statement to inspect and match the header for an incoming request."
type = map(object({
name = string # Name of the header match rule group
priority = number # Relative processing order for header match rule relative to other rules processed by AWS WAF.
header_values = map(object({ # Header values contains a map of headers to inspect. You can provide multiple headers and values, all headers will be inspected together with `AND` logic.
header_name = string # This is the name of the header to inspect for all incoming requests.
header_value = string # This is the value to look out for a matching header name for all incoming requests
not_statement = optional(bool, false) # This indicates if the result this header match should be negated. The negated result will be joined with other header match results using `AND` logic if more than 1 header is provided.
}))
count_override = optional(bool, true) # If true, this will override the rule action setting to `count`, if false, the rule action will be set to `block`. Default value is false.
}))

default = null
}

variable "allowed_ip_list" {
description = "List of allowed IP addresses, these IP addresses will be exempted from any configured rules"
type = list(string)
Expand Down
Loading