diff --git a/.github/workflows/pull-request-sast.yaml b/.github/workflows/pull-request-sast.yaml index 2754324..ee1c002 100644 --- a/.github/workflows/pull-request-sast.yaml +++ b/.github/workflows/pull-request-sast.yaml @@ -23,4 +23,5 @@ jobs: uses: aquasecurity/trivy-action@0.18.0 with: scan-type: 'config' + trivyignores: ".trivyignore" exit-code: '1' diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000..fd85f5b --- /dev/null +++ b/.trivyignore @@ -0,0 +1,5 @@ +#Ignore requirement for S3 logging bucket as per CUDOS setup instructions +AVD-AWS-0089 + +#Ignore requirement for customer managed key for S3 encryption as per CUDOS setup instructions +AVD-AWS-0132 diff --git a/modules/aws/README.md b/modules/aws/README.md index 5351eb3..5bff593 100644 --- a/modules/aws/README.md +++ b/modules/aws/README.md @@ -9,6 +9,8 @@ The following modules are available: - [Group Account Assignments](./group_account_assignments/README.md) - [Group User Memberships](./group_user_memberships/README.md) - [Groups](./groups/README.md) +- [Permission Sets](./permission_sets/README.md) - [Identity Center Instance](./ssoadmin_instance/README.md) - [Permission Sets](./permission_sets/README.md) - [Users](./users/README.md) +- [Cost and Usage Reports](./cost_usage_reports/README.md) diff --git a/modules/aws/cost_usage_reports/.terraform.lock.hcl b/modules/aws/cost_usage_reports/.terraform.lock.hcl new file mode 100644 index 0000000..a6da323 --- /dev/null +++ b/modules/aws/cost_usage_reports/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.40.0" + constraints = "> 5.0.0, < 6.0.0" + hashes = [ + "h1:66UBDb7nDnLJ3MHmRZZRlqvWoeJOFFwN44gifJzs/IE=", + "zh:11f177a2385703740bd26d0652d3dba08575101d7639f386ce5637bdb0e29a13", + "zh:203fc43e69634f1bd487a9dc24b01944dfd568beac78e491f26677d103d343ed", + "zh:3697ebad4929da30ea98276a85d4ce5ebfc48508f4dd149e17e1dcdc7f306c6e", + "zh:421e0799756587e728f75a9024b8d4e38707cd6d65cf0710cb8d189062c85a58", + "zh:4be2adcd4c32a66159c532908f0d425d793c814b3686832e9af549b1515ae032", + "zh:55778b32470212ce6bbfd402529c88e7ea6ba34b0882f85d6ea001ff5c6255a5", + "zh:689a4c1fd1e1d5dab7b169759389c76f25e366f19a470971674321d6fca09791", + "zh:68a23eda608573a053e8738894457bd0c11766bc243e68826c78ab6b5a144710", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a1580115c22564e5752e569dc40482503de6cced44da3e9431885cd9d4bf18ea", + "zh:b127756d7ee513691e76c211570580c10eaa2f7a7e4fd27c3566a48ec214991c", + "zh:b7ccea7a759940c8dcf8726272eed6653eed0b31f7223f71e829a344627afd39", + "zh:bb130fc50494fd45406e04b44d242da9a8f138a4a43feb65cf9e86d13aa13629", + "zh:cf1c972c90d5f22c9705274a33792275e284a0a3fcac12ce4083b5a4480463f4", + "zh:ebe60d3887b23703ca6a4c65b15c6d7b8d93ba27a028d996d17882fe6e98d5c0", + ] +} diff --git a/modules/aws/cost_usage_reports/README.md b/modules/aws/cost_usage_reports/README.md new file mode 100644 index 0000000..f610ef9 --- /dev/null +++ b/modules/aws/cost_usage_reports/README.md @@ -0,0 +1,55 @@ +# Core Cloud AWS Cost & Usage Report Module + +This module is responsible for creating and managing Cost and Usage Reports and their related infrastructure in AWS. + +## Usage + +```hcl +module "cost_usage_reports" { + source = "git::ssh://git@github.com/UKHomeOffice/core-cloud-terraform-modules.git//modules/aws/cost_usage_reports" + + report_name = + time_unit = + format = + compression = + additional_schema_elements = + bucket_name = + bucket_region = + additional_artifacts = + s3_prefix = + refresh_closed_reports = + report_versioning = + iam_role = + lifecycle_rule = + noncurrent_version_expiration_days = + expiration_days = + inline_policy_name = + billing_account = + replication_rule = + destination_bucket = + +} +``` + +## Validation + +This module expects the variables to conform to the following: +- `report_name` - Must be a string between 1 and 256 characters. +- `time_unit` - Valid values for time_unit are DAILY, HOURLY or MONTHLY. +- `format` - Valid values for format are textORcsv or Parquet. +- `compression` - Valid values for time_unit are GZIP, ZIP or Parquet. +- `additional_schema_elements` - Valid values for additional_schema_elements are RESOURCES or SPLIT_COST_ALLOCATION_DATA. +- `bucket_name` - Must be a string between 1 and 64 characters. +- `bucket_region` - - Must be an AWS region. +- `additional_artifacts` - Valid values for time_unit are REDSHIFT, QUICKSHIFT or ATHENA. +- `s3_prefix` - Must be a string between 1 and 256 characters. +- `refresh_closed_reports` - Boolean value. +- `report_versioning` - Valid values for report_versioning are CREATE_NEW_REPORT or OVERWRITE_REPORT. +- `iam_role` - Friendly name of the role. If omitted, Terraform will assign a random, unique name. +- `lifecycle_rule` - Must be a string between 1 and 256 characters. +- `noncurrent_version_expiration_days` - Must be a positive integer. +- `expiration_days` - Must be a positive integer. +- `inline_policy_name` - Must be a string between 1 and 256 characters. +- `billing_account` - Must be a 12 character string. +- `replication_rule` - Must be a string between 1 and 256 characters. +- `destination_bucket` - The destination_bucket ARN must be less than 256 characters. diff --git a/modules/aws/cost_usage_reports/main.tf b/modules/aws/cost_usage_reports/main.tf new file mode 100644 index 0000000..edd904f --- /dev/null +++ b/modules/aws/cost_usage_reports/main.tf @@ -0,0 +1,217 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "> 5.0.0, < 6.0.0" + } + } +} + +provider "aws" { + region = "eu-west-2" +} + +provider "aws" { + region = "us-east-1" + alias = "us-east-1" +} + +#COST AND USAGE REPORT +resource "aws_cur_report_definition" "cur_report_definitions" { + depends_on = [aws_iam_role.cur_role, aws_s3_bucket_policy.cur_S3_bucket_policy] + provider = aws.us-east-1 + report_name = var.report_name + time_unit = var.time_unit + format = var.format + compression = var.compression + additional_schema_elements = var.additional_schema_elements + s3_bucket = aws_s3_bucket.s3_buckets.id + s3_region = var.bucket_region + additional_artifacts = var.additional_artifacts + s3_prefix = "cur/${var.billing_account}" + refresh_closed_reports = var.refresh_closed_reports + report_versioning = var.report_versioning +} + +#S3 BUCKET +resource "aws_s3_bucket" "s3_buckets" { + bucket = var.bucket_name +} + +#S3 SETTINGS +resource "aws_s3_bucket_ownership_controls" "bucket_ownership_controls" { + bucket = aws_s3_bucket.s3_buckets.id + rule { + object_ownership = "BucketOwnerEnforced" + } +} + +resource "aws_s3_bucket_versioning" "versioning_rules" { + bucket = aws_s3_bucket.s3_buckets.id + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "encryption_rules" { + bucket = aws_s3_bucket.s3_buckets.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_public_access_block" "cur_public_access_block" { + bucket = aws_s3_bucket.s3_buckets.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +#IAM ROLE +resource "aws_iam_role" "cur_role" { + name = var.iam_role + assume_role_policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Action" : "sts:AssumeRole", + "Principal" : { + "Service" : "s3.amazonaws.com" + }, + "Effect" : "Allow", + "Sid" : "" + } + ] + }) + + inline_policy { + name = var.inline_policy_name + + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Action" : [ + "s3:GetReplicationConfiguration", + "s3:ListBucket" + ], + "Resource" : "arn:aws:s3:::cid-${var.billing_account}-central-finops-local", + "Effect" : "Allow" + }, + { + "Action" : [ + "s3:GetObjectVersionForReplication", + "s3:GetObjectVersionAcl" + ], + "Resource" : "arn:aws:s3:::cid-${var.billing_account}-central-finops-local/*", + "Effect" : "Allow" + }, + { + "Action" : [ + "s3:ReplicateObject", + "s3:ReplicateDelete", + "s3:ReplicateTags", + "s3:GetObjectVersionTagging" + ], + "Resource" : "arn:aws:s3:::cid-873134405383-shared/cur/${var.billing_account}/*", + "Effect" : "Allow" + } + ] + }) + } +} + +#S3 BUCKET POLICY +resource "aws_s3_bucket_policy" "cur_S3_bucket_policy" { + bucket = aws_s3_bucket.s3_buckets.id + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "billingreports.amazonaws.com" + }, + "Action": [ + "s3:GetBucketAcl", + "s3:GetBucketPolicy" + ], + "Resource": aws_s3_bucket.s3_buckets.arn, + "Condition": { + "StringEquals": { + "aws:SourceAccount": var.billing_account, + "aws:SourceArn": "arn:aws:cur:us-east-1:${var.billing_account}:definition/*" + } + } + }, + { + "Effect": "Allow", + "Principal": { + "Service": "billingreports.amazonaws.com" + }, + "Action": "s3:PutObject", + "Resource": "${aws_s3_bucket.s3_buckets.arn}/*", + "Condition": { + "StringEquals": { + "aws:SourceAccount": var.billing_account, + "aws:SourceArn": "arn:aws:cur:us-east-1:${var.billing_account}:definition/*" + } + } + } + ] + }) +} + + +#S3 LIFECYCLE RULE +resource "aws_s3_bucket_lifecycle_configuration" "cur_bucket_lifecycle_rule" { + depends_on = [aws_s3_bucket_versioning.versioning_rules] + bucket = aws_s3_bucket.s3_buckets.id + rule { + id = var.lifecycle_rule + + filter {} + + noncurrent_version_expiration { + noncurrent_days = var.noncurrent_version_expiration_days + } + + expiration { + days = var.expiration_days + } + status = "Enabled" + } +} + +/* # REPLICATION RULE +resource "aws_s3_bucket_replication_configuration" "cur_bucket_replication_rule" { + depends_on = [aws_s3_bucket_versioning.versioning_rules] + bucket = aws_s3_bucket.s3_buckets.id + role = aws_iam_role.cur_role.arn + rule { + id = var.replication_rule + + filter {} + + destination { + bucket = var.destination_bucket + storage_class = "STANDARD" + } + + delete_marker_replication { + status = "Enabled" + } + + source_selection_criteria { + sse_kms_encrypted_objects { + status = "Disabled" + } + } + status = "Enabled" + } +} */ diff --git a/modules/aws/cost_usage_reports/variables.tf b/modules/aws/cost_usage_reports/variables.tf new file mode 100644 index 0000000..119b072 --- /dev/null +++ b/modules/aws/cost_usage_reports/variables.tf @@ -0,0 +1,164 @@ +variable "report_name" { + type = string + description = "The name of the cost and usage report to create." + + validation { + condition = length(var.report_name) >= 1 && length(var.report_name) <= 256 + error_message = "The report_name name must be less than 256 characters." + } +} + +variable "time_unit" { + type = string + description = "The frequency on which report data are measured and displayed." + + validation { + condition = contains(["DAILY", "HOURLY", "MONTHLY"], var.time_unit) + error_message = "Valid values for time_unit are (DAILY, HOURLY, MONTHLY)" + } +} + +variable "format" { + type = string + description = "The format for the report." + + validation { + condition = contains(["textORcsv", "Parquet"], var.format) + error_message = "Valid values for format are (textORcsv, Parquet)" + } +} + +variable "compression" { + type = string + description = "Compression format for report." + + validation { + condition = contains(["GZIP", "ZIP", "Parquet"], var.compression) + error_message = "Valid values for time_unit are (GZIP, ZIP, Parquet)" + } +} + +variable "additional_schema_elements" { + type = list(string) + description = "A list of schema elements." + + validation { + condition = contains(var.additional_schema_elements, "RESOURCES") || contains(var.additional_schema_elements, "SPLIT_COST_ALLOCATION_DATA" ) + error_message = "Valid values for additional_schema_elements are (RESOURCES, SPLIT_COST_ALLOCATION_DATA)" + } +} + +variable "bucket_name" { + type = string + description = "The name of the existing s3 bucket store generated reports" + + validation { + condition = length(var.bucket_name) >= 1 && length(var.bucket_name) <= 64 + error_message = "The bucket_name name must be less than 64 characters." + } +} + +variable "bucket_region" { + type = string + description = "Region of the existing S3 bucket to hold generated reports." + + validation { + condition = can(regex("[a-z][a-z]-[a-z]+-[1-9]", var.bucket_region)) + error_message = "Must be valid AWS Region names." + } +} + +variable "additional_artifacts" { + type = list(string) + description = "A list of additional artifacts." + + validation { + condition = contains(var.additional_artifacts, "REDSHIFT") || contains(var.additional_artifacts, "QUICKSHIFT") || contains(var.additional_artifacts, "ATHENA") + error_message = "Valid values for time_unit are (REDSHIFT, QUICKSHIFT, ATHENA)" + } +} + +variable "refresh_closed_reports" { + type = string + description = "Set to true to update your reports after they have been finalized if AWS detects charges related to previous months." +} + +variable "report_versioning" { + type = string + description = "Overwrite the previous version of each report or to deliver the report in addition to the previous versions. Valid values are (CREATE_NEW_REPORT and OVERWRITE_REPORT)" +} + +variable "iam_role" { + type = string + description = "Friendly name of the role. If omitted, Terraform will assign a random, unique name" +} + +variable "lifecycle_rule" { + type = string + description = "The name of the lifecycle rule applied to S3" + + validation { + condition = length(var.lifecycle_rule) >= 1 && length(var.lifecycle_rule) <= 256 + error_message = "The lifecycle_rule name must be less than 256 characters." + } +} + +variable "noncurrent_version_expiration_days" { + type = number + description = "The Number of days an object is noncurrent before Amazon S3 can perform the associated action." + + validation { + condition = var.noncurrent_version_expiration_days > 0 + error_message = "The noncurrent_version_expiration_days variable must be a positive integer." + } +} + +variable "expiration_days" { + type = number + description = "The lifetime, in days, of the objects that are subject to the rule.." + + validation { + condition = var.expiration_days > 0 + error_message = "The expiration_days variable must be a positive integer." + } +} + +variable "inline_policy_name" { + type = string + description = "Name of the role policy." + + validation { + condition = length(var.inline_policy_name) >= 1 && length(var.inline_policy_name) <= 256 + error_message = "The inline_policy_name must be less than 256 characters." + } +} + +variable "billing_account" { + type = string + description = "AccountID for the billing account" + + validation { + condition = length(var.billing_account) == 12 + error_message = "The billing_account id must be 12 characters." + } +} + +/* variable "replication_rule" { + type = string + description = "The name of the replication rule applied to S3" + + validation { + condition = length(var.replication_rule) >= 1 && length(var.replication_rule) <= 256 + error_message = "The replication_rule name must be less than 256 characters." + } +} + +#variable "destination_bucket" { + type = string + description = "The ARN of the existing s3 bucket to replicate generated reports to." + + validation { + condition = length(var.destination_bucket) >= 1 && length(var.destination_bucket) <= 256 + error_message = "The destination_bucket ARN must be less than 256 characters." + } +} */