diff --git a/README.md b/README.md index 2258338..5fa8626 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ module "aws_logs" { | allow\_cloudwatch | Allow Cloudwatch service to export logs to bucket. | `bool` | `false` | no | | allow\_config | Allow Config service to log to bucket. | `bool` | `false` | no | | allow\_elb | Allow ELB service to log to bucket. | `bool` | `false` | no | +| allow\_guardduty | Allow GuardDuty service to log to bucket. | `bool` | `false` | no | | allow\_nlb | Allow NLB service to log to bucket. | `bool` | `false` | no | | allow\_redshift | Allow Redshift service to log to bucket. | `bool` | `false` | no | | cloudtrail\_accounts | List of accounts for CloudTrail logs. By default limits to the current account. | `list(string)` | `[]` | no | @@ -125,6 +126,7 @@ module "aws_logs" { | elb\_accounts | List of accounts for ELB logs. By default limits to the current account. | `list(string)` | `[]` | no | | elb\_logs\_prefix | S3 prefix for ELB logs. | `string` | `"elb"` | no | | force\_destroy | A bool that indicates all objects (including any locked objects) should be deleted from the bucket so the bucket can be destroyed without error. | `bool` | `false` | no | +| guardduty\_logs\_prefixes | S3 key prefixes for GuardDuty logs. | `list(string)` |
[
"guardduty"
]
| no | | nlb\_account | Account for NLB logs. By default limits to the current account. | `string` | `""` | no | | nlb\_logs\_prefixes | S3 key prefixes for NLB logs. | `list(string)` |
[
"nlb"
]
| no | | redshift\_logs\_prefix | S3 prefix for RedShift logs. | `string` | `"redshift"` | no | diff --git a/examples/guardduty/main.tf b/examples/guardduty/main.tf new file mode 100644 index 0000000..da6a76a --- /dev/null +++ b/examples/guardduty/main.tf @@ -0,0 +1,11 @@ +module "aws_logs" { + source = "../../" + + s3_bucket_name = var.test_name + guardduty_logs_prefixes = var.guardduty_logs_prefixes + region = var.region + allow_guardduty = true + default_allow = false + + force_destroy = var.force_destroy +} diff --git a/examples/guardduty/variables.tf b/examples/guardduty/variables.tf new file mode 100644 index 0000000..17308e7 --- /dev/null +++ b/examples/guardduty/variables.tf @@ -0,0 +1,15 @@ +variable "test_name" { + type = string +} + +variable "region" { + type = string +} + +variable "force_destroy" { + type = bool +} + +variable "guardduty_logs_prefixes" { + type = list(string) +} diff --git a/main.tf b/main.tf index a8bb71d..74e2a7a 100644 --- a/main.tf +++ b/main.tf @@ -69,6 +69,14 @@ locals { config_logs_path = var.config_logs_prefix == "" ? "AWSLogs" : "${var.config_logs_prefix}/AWSLogs" + # Config does a writability check by writing to key "[prefix]/AWSLogs/[accountId]/Config/ConfigWritabilityCheckFile". + # When there is an oversize configuration item change notification, Config will write the notification to S3 at the path. + # Therefore, you cannot limit the policy to the region. + # For example: + # config/AWSLogs/[accountId]/Config/global/[year]/[month]/[day]/ + # OversizedChangeNotification/AWS::IAM::Policy/ + # [accountId]_Config_global_ChangeNotification_AWS::IAM::Policy_[resourceId]_[timestamp]_[configurationStateId].json.gz + # Therefore, do not extend the resource path to include the region as shown in the AWS Console. config_resources = sort(formatlist("${local.bucket_arn}/${local.config_logs_path}/%s/Config/*", local.config_accounts)) # @@ -137,6 +145,33 @@ locals { redshift_principal = "arn:${data.aws_partition.current.partition}:iam::${data.aws_redshift_service_account.main.id}:user/logs" redshift_resource = "${local.bucket_arn}/${var.redshift_logs_prefix}/*" + + # + # GuardDuty locals + # + + # doesn't support logging to multiple accounts + # supports logging to multiple prefixes + guardduty_effect = var.default_allow || var.allow_guardduty ? "Allow" : "Deny" + + # if the list of prefixes contains "", set an append_root_prefix flag + guardduty_include_root_prefix = contains(var.guardduty_logs_prefixes, "") ? true : false + + # create a list of paths, but remove any prefixes containing "" using compact + guardduty_logs_path_temp = formatlist("%s/AWSLogs", compact(var.guardduty_logs_prefixes)) + + # now append an "AWSLogs" path to the list if guardduty_include_root_prefix is true + guardduty_logs_path = local.guardduty_include_root_prefix ? concat(local.guardduty_logs_path_temp, ["AWSLogs"]) : local.guardduty_logs_path_temp + + # GuardDuty does a writability check using the [prefix]/AWSLogs path, + # rather than the full path as shown in the AWS Console. + # For example, here is the error you would receive if you used the full path in the bucket policy: + # Failed to configure export options because GuardDuty + # does not have permission to the KMS key, the S3 bucket, or the specified location in the bucke + + # finally, format the full final resources ARN list + guardduty_resources = sort(formatlist("${local.bucket_arn}/%s/*", local.guardduty_logs_path)) + } # @@ -325,6 +360,37 @@ data "aws_iam_policy_document" "main" { resources = [local.bucket_arn] } + # + # GuardDuty bucket policies + # + + statement { + sid = "guardduty-logs-get-location" + effect = local.guardduty_effect + principals { + type = "Service" + identifiers = ["guardduty.amazonaws.com"] + } + actions = ["s3:GetBucketLocation"] + resources = [local.bucket_arn] + } + + statement { + sid = "guardduty-logs-put-object" + effect = local.guardduty_effect + principals { + type = "Service" + identifiers = ["guardduty.amazonaws.com"] + } + actions = ["s3:PutObject"] + resources = local.guardduty_resources + condition { + test = "StringEquals" + variable = "s3:x-amz-acl" + values = ["bucket-owner-full-control"] + } + } + # # Enforce TLS requests only # diff --git a/test/terraform_aws_logs_guardduty_test.go b/test/terraform_aws_logs_guardduty_test.go new file mode 100644 index 0000000..3f203a1 --- /dev/null +++ b/test/terraform_aws_logs_guardduty_test.go @@ -0,0 +1,36 @@ +package test + +import ( + "fmt" + "strings" + "testing" + + "github.com/gruntwork-io/terratest/modules/aws" + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + test_structure "github.com/gruntwork-io/terratest/modules/test-structure" +) + +func TestTerraformAwsLogsGuardDuty(t *testing.T) { + t.Parallel() + + tempTestFolder := test_structure.CopyTerraformFolderToTemp(t, "../", "examples/guardduty") + testName := fmt.Sprintf("terratest-aws-logs-%s", strings.ToLower(random.UniqueId())) + awsRegion := "us-west-2" + + terraformOptions := &terraform.Options{ + TerraformDir: tempTestFolder, + Vars: map[string]interface{}{ + "region": awsRegion, + "test_name": testName, + "force_destroy": true, + "guardduty_logs_prefixes": []string{testName}, + }, + EnvVars: map[string]string{ + "AWS_DEFAULT_REGION": awsRegion, + }, + } + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApply(t, terraformOptions) +} diff --git a/variables.tf b/variables.tf index 3a5042a..9db303b 100644 --- a/variables.tf +++ b/variables.tf @@ -100,6 +100,12 @@ variable "allow_redshift" { type = bool } +variable "allow_guardduty" { + description = "Allow GuardDuty service to log to bucket." + default = false + type = bool +} + variable "create_public_access_block" { description = "Whether to create a public_access_block restricting public access to the bucket." default = true @@ -153,3 +159,9 @@ variable tags { default = {} description = "A mapping of tags to assign to the logs bucket. Please note that tags with a conflicting key will not override the original tag." } + +variable "guardduty_logs_prefixes" { + description = "S3 key prefixes for GuardDuty logs." + default = ["guardduty"] + type = list(string) +}