From 8ae19d02858b457f76d475d402bf3f7c0e51de7e Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Tue, 25 Nov 2025 18:21:10 +0100 Subject: [PATCH 01/26] Preliminary work for management account. --- modules/response-actions/locals.tf | 202 ++++++++++++++++++ modules/response-actions/main.tf | 171 +++++++++++++++ .../modules/lambda-functions/main.tf | 111 ++++++++++ .../modules/lambda-functions/outputs.tf | 9 + .../modules/lambda-functions/variables.tf | 56 +++++ modules/response-actions/outputs.tf | 26 +++ .../configure-resource-access-policy.json | 29 +++ .../create-volume-snapshots-policy.json | 41 ++++ .../delete-volume-snapshots-policy.json | 29 +++ .../policies/fetch-cloud-logs-policy.json | 19 ++ .../policies/quarantine-user-policy.json | 36 ++++ .../policies/remove-policy-policy.json | 24 +++ modules/response-actions/variables.tf | 117 ++++++++++ modules/response-actions/versions.tf | 17 ++ 14 files changed, 887 insertions(+) create mode 100644 modules/response-actions/locals.tf create mode 100644 modules/response-actions/main.tf create mode 100644 modules/response-actions/modules/lambda-functions/main.tf create mode 100644 modules/response-actions/modules/lambda-functions/outputs.tf create mode 100644 modules/response-actions/modules/lambda-functions/variables.tf create mode 100644 modules/response-actions/outputs.tf create mode 100644 modules/response-actions/policies/configure-resource-access-policy.json create mode 100644 modules/response-actions/policies/create-volume-snapshots-policy.json create mode 100644 modules/response-actions/policies/delete-volume-snapshots-policy.json create mode 100644 modules/response-actions/policies/fetch-cloud-logs-policy.json create mode 100644 modules/response-actions/policies/quarantine-user-policy.json create mode 100644 modules/response-actions/policies/remove-policy-policy.json create mode 100644 modules/response-actions/variables.tf create mode 100644 modules/response-actions/versions.tf diff --git a/modules/response-actions/locals.tf b/modules/response-actions/locals.tf new file mode 100644 index 0000000..982499c --- /dev/null +++ b/modules/response-actions/locals.tf @@ -0,0 +1,202 @@ +#---------------------------------------------------------- +# Fetch & compute required data for organizational install +#---------------------------------------------------------- + +data "aws_organizations_organization" "org" { + count = var.is_organizational ? 1 : 0 +} + +locals { + # check if both old and new org parameters are provided, we fail early + both_org_configuration_params = var.is_organizational && length(var.org_units) > 0 && ( + length(var.include_ouids) > 0 || + length(var.exclude_ouids) > 0 || + length(var.include_accounts) > 0 || + length(var.exclude_accounts) > 0 + ) + + # check if old org_units parameter is provided, for backwards compatibility we will always give preference to it + check_old_ouid_param = var.is_organizational && length(var.org_units) > 0 + + # fetch the AWS Root OU under org + # As per https://docs.aws.amazon.com/organizations/latest/userguide/orgs_getting-started_concepts.html#organization-structure, there can be only one root + root_org_unit = var.is_organizational ? [for root in data.aws_organizations_organization.org[0].roots : root.id] : [] +} + +check "validate_org_configuration_params" { + assert { + condition = length(var.org_units) == 0 # if this condition is false we throw warning + error_message = <<-EOT + WARNING: TO BE DEPRECATED 'org_units' on 30th November, 2025. Please work with Sysdig to migrate your Terraform installs to use 'include_ouids' instead. + EOT + } + + assert { + condition = !local.both_org_configuration_params # if this condition is false we throw error + error_message = <<-EOT + ERROR: If both org_units and include_ouids/exclude_ouids/include_accounts/exclude_accounts variables are populated, + ONLY org_units will be considered. Please use only one of the two methods. + + Note: org_units is going to be DEPRECATED on 30th November, 2025. Please work with Sysdig to migrate your Terraform installs. + EOT + } +} + +# ***************************************************************************************************************************************************** +# INCLUDE/EXCLUDE CONFIGURATION SUPPORT +# +# 1. Inclusions will always be handled for TF cloud provisioning. +# NOTE: +# Till AWS issue with UNION filter (https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-cloudformation/issues/100) +# is fixed, we can't deploy using UNION filters for inclusions. As a workaround to ensure we don't skip any accounts, we deploy it to entire org. +# +# 2. We handle exclusions when only exclusion parameters are provided i.e out of all 4 configuration inputs, +# a. only exclude_ouids are provided, OR +# b. only exclude_accounts are provided, OR +# c. only exclude_ouids AND exclude_accounts are provided +# Else we ignore exclusions during cloud resource provisioning through TF. This is because AWS does not allow both operations - to include some +# accounts and to exclude some. Hence, we will always prioritize include over exclude. +# +# 3. Sysdig however will honor all combinations of configuration inputs exactly as desired. +# ***************************************************************************************************************************************************** + +#------------------------------------------------------------ +# Manage configurations to determine OU targets to deploy in +#------------------------------------------------------------ + +locals { + # OU CONFIGURATION (determine user provided org configuration) + org_configuration = ( + # case1 - if old method is used where ONLY org_units is provided, use those + local.check_old_ouid_param ? ( + "old_ouid_param" + ) : ( + # case2 - if no include/exclude ous provided, include entire org + var.is_organizational && length(var.include_ouids) == 0 && length(var.exclude_ouids) == 0 ? ( + "entire_org" + ) : ( + # case3 - if only included ouids provided, include those ous only + var.is_organizational && length(var.include_ouids) > 0 && length(var.exclude_ouids) == 0 ? ( + "included_ous_only" + ) : ( + # case4 - if only excluded ouids provided, exclude their accounts from rest of org + var.is_organizational && length(var.include_ouids) == 0 && length(var.exclude_ouids) > 0 ? ( + "excluded_ous_only" + ) : ( + # case5 - if both include and exclude ouids are provided, includes override excludes + var.is_organizational && length(var.include_ouids) > 0 && length(var.exclude_ouids) > 0 ? ( + "mixed_ous" + ) : "" + ) + ) + ) + ) + ) + + # switch cases for various user provided org configuration to be onboarded + deployment_options = { + old_ouid_param = { + org_units_to_deploy = var.org_units + } + entire_org = { + org_units_to_deploy = local.root_org_unit + } + included_ous_only = { + org_units_to_deploy = var.include_ouids + } + excluded_ous_only = { + # onboard entire org and filter out all accounts in excluded OUs using account filter + org_units_to_deploy = local.root_org_unit + } + mixed_ous = { + # if both include and exclude ouids are provided, includes override excludes + org_units_to_deploy = var.include_ouids + } + default = { + org_units_to_deploy = local.root_org_unit + } + } + + # final targets to deploy organizational resources in + deployment_targets_ous = lookup(local.deployment_options, local.org_configuration, local.deployment_options.default) + + exclude_root_ou = length(local.root_org_unit) > 0 ? contains(var.exclude_ouids, local.root_org_unit[0]) : false +} + +#----------------------------------------------------------------- +# Manage configurations to determine account targets to deploy in +#----------------------------------------------------------------- + +# if only exclude_ouids are provided and as long as it isn't Root OU, fetch all their child accounts to filter exclusions +data "aws_organizations_organizational_unit_descendant_accounts" "ou_accounts_to_exclude" { + for_each = local.org_configuration == "excluded_ous_only" && !local.exclude_root_ou ? var.exclude_ouids : [] + parent_id = each.key +} + +locals { + # ACCOUNTS CONFIGURATION (determine user provided accounts configuration) + accounts_configuration = ( + # case1 - if old method is used where ONLY org_units is provided, this configuration is a noop + local.check_old_ouid_param ? ( + "NONE" + ) : ( + # case2 - if only included accounts provided, include those accts as well + var.is_organizational && length(var.include_accounts) > 0 && length(var.exclude_accounts) == 0 ? ( + "UNION" + ) : ( + # case3 - if only excluded accounts or only excluded ouids provided, exclude those accounts + var.is_organizational && length(var.include_accounts) == 0 && (length(var.exclude_accounts) > 0 || local.org_configuration == "excluded_ous_only") ? ( + "DIFFERENCE" + ) : ( + # case4 - if both include and exclude accounts are provided, includes override excludes + var.is_organizational && length(var.include_accounts) > 0 && length(var.exclude_accounts) > 0 ? ( + "MIXED" + ) : "" + ) + ) + ) + ) + + ou_accounts_to_exclude = flatten([for ou_accounts in data.aws_organizations_organizational_unit_descendant_accounts.ou_accounts_to_exclude : [ou_accounts.accounts[*].id]]) + accounts_to_exclude = setunion(local.ou_accounts_to_exclude, var.exclude_accounts) + + # switch cases for various user provided accounts configuration to be onboarded + deployment_account_options = { + NONE = { + accounts_to_deploy = [] + account_filter_type = "NONE" + } + UNION = { + accounts_to_deploy = var.include_accounts + account_filter_type = "UNION" + } + DIFFERENCE = { + accounts_to_deploy = local.accounts_to_exclude + account_filter_type = "DIFFERENCE" + } + MIXED = { + accounts_to_deploy = var.include_accounts + account_filter_type = "UNION" + } + default = { + # default when neither of include/exclude accounts are provided + accounts_to_deploy = [] + account_filter_type = "NONE" + } + } + + # list of accounts to deploy organizational resources in + deployment_targets_accounts = lookup(local.deployment_account_options, local.accounts_configuration, local.deployment_account_options.default) +} + +# ----------------------------------------------------------------------------------------------------- +# Remove below conditional once AWS issue is fixed - +# https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-cloudformation/issues/100 +# ----------------------------------------------------------------------------------------------------- +locals { + # XXX: due to AWS bug of not having UNION filter fully working, there is no way to add those extra accounts requested. + # to not miss out on those extra accounts, deploy the cloud resources across entire org and noop the UNION filter. + # i.e till we can't deploy UNION, we deploy it all + deployment_targets_org_units = local.deployment_targets_accounts.account_filter_type == "UNION" ? local.root_org_unit : local.deployment_targets_ous.org_units_to_deploy + deployment_targets_accounts_filter = local.deployment_targets_accounts.account_filter_type == "UNION" ? "NONE" : local.deployment_targets_accounts.account_filter_type +} diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf new file mode 100644 index 0000000..e05b5b5 --- /dev/null +++ b/modules/response-actions/main.tf @@ -0,0 +1,171 @@ +data "aws_caller_identity" "current" {} + +locals { + quarantine_user_policy = templatefile("${path.module}/policies/quarantine-user-policy.json", {}) + fetch_cloud_logs_policy = templatefile("${path.module}/policies/fetch-cloud-logs-policy.json", {}) + remove_policy_policy = templatefile("${path.module}/policies/remove-policy-policy.json", {}) + configure_resource_access_policy = templatefile("${path.module}/policies/configure-resource-access-policy.json", {}) + create_volume_snapshots_policy = templatefile("${path.module}/policies/create-volume-snapshots-policy.json", {}) + delete_volume_snapshots_policy = templatefile("${path.module}/policies/delete-volume-snapshots-policy.json", {}) + + region_set = toset(var.regions) + trusted_identity = var.is_gov_cloud_onboarding ? data.sysdig_secure_trusted_cloud_identity.trusted_identity.gov_identity : data.sysdig_secure_trusted_cloud_identity.trusted_identity.identity + arn_prefix = var.is_gov_cloud_onboarding ? "arn:aws-us-gov" : "arn:aws" + responder_component_type = "COMPONENT_CLOUD_RESPONDER" + roles_component_type = "COMPONENT_CLOUD_RESPONDER_ROLES" + account_id_hash = substr(md5(data.aws_caller_identity.current.account_id), 0, 4) + ra_resource_name = "${var.name}-${random_id.suffix.hex}-${local.account_id_hash}" + common_environment_variables = { + API_BASE_URL = var.api_base_url + } +} + +resource "random_id" "suffix" { + byte_length = 3 +} + +data "sysdig_secure_trusted_cloud_identity" "trusted_identity" { + cloud_provider = "aws" +} + + +resource "aws_iam_role" "shared_cross_account_lambda_invoker" { + name = "${ra_resource_name}-cross-account-invoker" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + AWS = local.trusted_identity + } + Condition = { + StringEquals = { + "sts:ExternalId" = "cloud-actions-lambda-invoke-access" + } + } + } + ] + }) + + tags = { + Name = "${ra_resource_name}-cross-account-invoker" + } +} + +# Inline policy for invoking all Lambda functions +resource "aws_iam_role_policy" "shared_lambda_invoke_policy" { + name = "${ra_resource_name}-invoke-policy" + role = aws_iam_role.shared_cross_account_lambda_invoker.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "lambda:InvokeFunction", + "lambda:GetFunction" + ] + Resource = [ + "${arn_prefix}:lambda:*:*:function:${ra_resource_name}-quarantine-user", + "${arn_prefix}:lambda:*:*:function:${ra_resource_name}-fetch-cloud-logs", + "${arn_prefix}:lambda:*:*:function:${ra_resource_name}-remove-policy", + "${arn_prefix}:lambda:*:*:function:${ra_resource_name}-configure-resource-access", + "${arn_prefix}:lambda:*:*:function:${ra_resource_name}-create-volume-snapshots", + "${arn_prefix}:lambda:*:*:function:${ra_resource_name}-delete-volume-snapshots" + ] + }, + { + Effect : "Allow" + Action : [ + "tag:GetResources" + ] + Resource : "*" + } + ] + }) +} + +# Lambda Function: Quarantine User +module "quarantine_user_function" { + source = "./modules/lambda-functions" + + function_name = "${ra_resource_name}-quarantine-user" + function_zip_file = "quarantine_user.zip" + environment_variables = local.common_environment_variables + function_policies = [local.quarantine_user_policy] + lambda_handler = "app.index.handler" + response_actions_version = var.response_actions_version + arn_prefix = local.arn_prefix +} + +# Lambda Function: Fetch cloud logs +module "fetch_cloud_logs_function" { + source = "./modules/lambda-functions" + + function_name = "${ra_resource_name}-fetch-cloud-logs" + function_zip_file = "fetch_cloud_logs.zip" + environment_variables = local.common_environment_variables + function_policies = [local.fetch_cloud_logs_policy] + lambda_handler = "app.index.handler" + response_actions_version = var.response_actions_version + arn_prefix = local.arn_prefix +} + +# Lambda Function: Remove Policy +module "remove_policy_function" { + source = "./modules/lambda-functions" + + function_name = "${ra_resource_name}-remove-policy" + function_zip_file = "remove_policy.zip" + environment_variables = local.common_environment_variables + function_policies = [local.remove_policy_policy] + lambda_handler = "app.index.handler" + response_actions_version = var.response_actions_version + arn_prefix = local.arn_prefix +} + +# Lambda Function: Configure Resource Access +module "configure_resource_access_function" { + source = "./modules/lambda-functions" + + function_name = "${ra_resource_name}-configure-resource-access" + function_zip_file = "configure_resource_access.zip" + environment_variables = local.common_environment_variables + function_policies = [local.configure_resource_access_policy] + lambda_handler = "app.index.handler" + response_actions_version = var.response_actions_version + arn_prefix = local.arn_prefix +} + +# Lambda Function: Create Volume Snapshots +module "create_volume_snapshots_function" { + source = "./modules/lambda-functions" + + function_name = "${ra_resource_name}-create-volume-snapshots" + function_zip_file = "create_volume_snapshot.zip" + environment_variables = local.common_environment_variables + function_policies = [local.create_volume_snapshots_policy] + lambda_handler = "app.index.handler" + response_actions_version = var.response_actions_version + arn_prefix = local.arn_prefix +} + +# Lambda Function: Delete Volume Snapshots +module "delete_volume_snapshots_function" { + source = "./modules/lambda-functions" + + function_name = "${ra_resource_name}-delete-volume-snapshots" + function_zip_file = "delete_volume_snapshot.zip" + environment_variables = local.common_environment_variables + function_policies = [local.delete_volume_snapshots_policy] + lambda_handler = "app.index.handler" + response_actions_version = var.response_actions_version + arn_prefix = local.arn_prefix +} + +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} diff --git a/modules/response-actions/modules/lambda-functions/main.tf b/modules/response-actions/modules/lambda-functions/main.tf new file mode 100644 index 0000000..64b97d5 --- /dev/null +++ b/modules/response-actions/modules/lambda-functions/main.tf @@ -0,0 +1,111 @@ +# Lambda execution role +resource "aws_iam_role" "lambda_execution_role" { + name = "${var.function_name}-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) + + tags = { + Name = "${var.function_name}-role" + } +} + +# Basic Lambda execution policy +resource "aws_iam_role_policy_attachment" "lambda_basic_execution" { + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = aws_iam_role.lambda_execution_role.name +} + +# Validation using check blocks (Terraform 1.5+) +check "deployment_mode_validation" { + assert { + error_message = "Either local mode (function_zip_file) or S3 mode (s3_bucket, s3_key, s3_key_sha256) must be properly configured." + } +} + +data "http" "lambda_zip_resource" { + url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/${var.function_zip_file}" +} + +resource "local_file" "lambda_zip_file" { + filename = "${path.module}/lambda.zip" + content = data.http.lambda_zip_resource.body +} + +# Local values for hash calculation +locals { + function_sha256 = filebase64sha256(var.function_zip_file) + + # Merge environment variables with DELEGATE_ROLE_NAME if provided + merged_environment_variables = merge( + var.environment_variables, + { + # This will be the name of the role assumed in subaccounts. + # It's the name is conventionally the same as the one assumed by the lambda itself upon execution. + DELEGATE_ROLE_NAME = aws_iam_role.lambda_execution_role.name + } + ) +} + +# Inline (to avoid quota issues) custom policies for the Lambda execution role +resource "aws_iam_role_policy" "function_policies" { + count = length(var.function_policies) + + name = "${var.function_name}-custom-policy-${count.index + 1}" + role = aws_iam_role.lambda_execution_role.id + + policy = var.function_policies[count.index] +} + +# CloudWatch Log Group for Lambda function +resource "aws_cloudwatch_log_group" "lambda_log_group" { + name = "/aws/lambda/${var.function_name}" + retention_in_days = 7 + + tags = { + Name = "${var.function_name}-logs" + "sysdig.com/response-actions/cloud-actions" = "true" + } +} + +# Lambda function +resource "aws_lambda_function" "function" { + # Local mode configuration + filename = local_file.lambda_zip_file.filename + + function_name = var.function_name + role = aws_iam_role.lambda_execution_role.arn + handler = var.lambda_handler + runtime = var.lambda_runtime + timeout = var.lambda_timeout + memory_size = var.lambda_memory_size + source_code_hash = local.function_sha256 + publish = true + + dynamic "environment" { + for_each = length(local.merged_environment_variables) > 0 ? [1] : [] + content { + variables = local.merged_environment_variables + } + } + + tags = { + Name = var.function_name + "sysdig.com/response-actions/cloud-actions" = "true" + } + + depends_on = [ + aws_cloudwatch_log_group.lambda_log_group, + aws_iam_role_policy_attachment.lambda_basic_execution + ] +} diff --git a/modules/response-actions/modules/lambda-functions/outputs.tf b/modules/response-actions/modules/lambda-functions/outputs.tf new file mode 100644 index 0000000..6087ffc --- /dev/null +++ b/modules/response-actions/modules/lambda-functions/outputs.tf @@ -0,0 +1,9 @@ +output "function_arn" { + description = "ARN of the Lambda function" + value = aws_lambda_function.function.arn +} + +output "function_name" { + description = "Name of the Lambda function" + value = aws_lambda_function.function.function_name +} diff --git a/modules/response-actions/modules/lambda-functions/variables.tf b/modules/response-actions/modules/lambda-functions/variables.tf new file mode 100644 index 0000000..3ca1146 --- /dev/null +++ b/modules/response-actions/modules/lambda-functions/variables.tf @@ -0,0 +1,56 @@ +variable "function_name" { + description = "Name of the Lambda function" + type = string +} + +variable "function_zip_file" { + description = "Path to the local Lambda deployment package zip file (optional, for local mode)" + type = string + default = null +} + +variable "lambda_handler" { + description = "Lambda function handler" + type = string + default = "index.handler" +} + +variable "lambda_runtime" { + description = "Lambda function runtime" + type = string + default = "python3.13" +} + +variable "lambda_timeout" { + description = "Lambda function timeout in seconds" + type = number + default = 300 +} + +variable "lambda_memory_size" { + description = "Memory (MB) to allocate to the Lambda function" + type = number + default = 128 +} + +variable "function_policies" { + description = "List of policy documents for the Lambda function" + type = list(string) + default = [] +} + +variable "environment_variables" { + description = "Environment variables for the Lambda function" + type = map(string) + default = {} +} + +variable "response_actions_version" { + description = "Name of the IAM role to assume for cross-account operations" + type = string +} + +variable "arn_prefix" { + description = "The prefix for any AWS ARN" + type = "string" +} \ No newline at end of file diff --git a/modules/response-actions/outputs.tf b/modules/response-actions/outputs.tf new file mode 100644 index 0000000..a0d456d --- /dev/null +++ b/modules/response-actions/outputs.tf @@ -0,0 +1,26 @@ +output "cross_account_role_arn" { + description = "ARN of the cross-account role for Lambda invocation" + value = aws_iam_role.shared_cross_account_lambda_invoker.arn +} + +output "lambda_functions" { + description = "Information about deployed Lambda functions" + value = { + quarantine_user = { + name = module.quarantine_user_function.function_name + arn = module.quarantine_user_function.function_arn + } + remove_policy = { + name = module.remove_policy_function.function_name + arn = module.remove_policy_function.function_arn + } + fetch_cloud_logs = { + name = module.fetch_cloud_logs_function.function_name + arn = module.fetch_cloud_logs_function.function_arn + } + create_volume_snapshots = { + name = module.create_volume_snapshots_function.function_name + arn = module.create_volume_snapshots_function.function_arn + } + } +} diff --git a/modules/response-actions/policies/configure-resource-access-policy.json b/modules/response-actions/policies/configure-resource-access-policy.json new file mode 100644 index 0000000..3adfd9c --- /dev/null +++ b/modules/response-actions/policies/configure-resource-access-policy.json @@ -0,0 +1,29 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:GetBucketLocation", + "s3:GetBucketPublicAccessBlock", + "s3:PutBucketPublicAccessBlock", + "s3:ListAllMyBuckets" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "rds:DescribeDBInstances", + "rds:ModifyDBInstance", + "sts:GetCallerIdentity" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam::*:role/ResponseActionsConfigureAccessDelegateRole" + } + ] +} diff --git a/modules/response-actions/policies/create-volume-snapshots-policy.json b/modules/response-actions/policies/create-volume-snapshots-policy.json new file mode 100644 index 0000000..10b24f6 --- /dev/null +++ b/modules/response-actions/policies/create-volume-snapshots-policy.json @@ -0,0 +1,41 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "arn:aws:logs:*:*:*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstances", + "ec2:DescribeVolumes", + "ec2:DescribeSnapshots", + "ec2:CreateSnapshot", + "ec2:CreateTags", + "sts:GetCallerIdentity" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:CreateTags" + ], + "Resource": [ + "arn:aws:ec2:*:*:snapshot/*" + ] + }, + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam::*:role/ResponseActionsCreateVolumeSnapshotDelegateRole" + } + + ] +} diff --git a/modules/response-actions/policies/delete-volume-snapshots-policy.json b/modules/response-actions/policies/delete-volume-snapshots-policy.json new file mode 100644 index 0000000..588051f --- /dev/null +++ b/modules/response-actions/policies/delete-volume-snapshots-policy.json @@ -0,0 +1,29 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "arn:aws:logs:*:*:*" + }, + { + "Effect": "Allow", + "Action": [ + "ec2:DescribeSnapshots", + "ec2:DeleteSnapshot", + "sts:GetCallerIdentity" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam::*:role/ResponseActionsDeleteVolumeSnapshotDelegateRole" + } + + ] +} diff --git a/modules/response-actions/policies/fetch-cloud-logs-policy.json b/modules/response-actions/policies/fetch-cloud-logs-policy.json new file mode 100644 index 0000000..8e2e0cf --- /dev/null +++ b/modules/response-actions/policies/fetch-cloud-logs-policy.json @@ -0,0 +1,19 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "cloudtrail:LookupEvents", + "sts:GetCallerIdentity" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam::*:role/ResponseActionsFetchCloudLogsDelegateRole" + } + + ] +} diff --git a/modules/response-actions/policies/quarantine-user-policy.json b/modules/response-actions/policies/quarantine-user-policy.json new file mode 100644 index 0000000..a892681 --- /dev/null +++ b/modules/response-actions/policies/quarantine-user-policy.json @@ -0,0 +1,36 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:AttachUserPolicy", + "iam:DetachUserPolicy", + "iam:PutUserPolicy", + "iam:DeleteUserPolicy", + "iam:ListUserPolicies", + "iam:ListAttachedUserPolicies", + "iam:GetUser", + "iam:GetUserPolicy", + "iam:TagUser", + "iam:UntagUser", + "iam:ListUserTags", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:GetRole", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + "iam:GetPolicy", + "iam:CreatePolicy", + "sts:GetCallerIdentity" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam::*:role/ResponseActionsQuarantineUserRoleDelegateRole" + } + + ] +} diff --git a/modules/response-actions/policies/remove-policy-policy.json b/modules/response-actions/policies/remove-policy-policy.json new file mode 100644 index 0000000..5226b7c --- /dev/null +++ b/modules/response-actions/policies/remove-policy-policy.json @@ -0,0 +1,24 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:DetachUserPolicy", + "iam:DetachRolePolicy", + "iam:ListAttachedUserPolicies", + "iam:ListAttachedRolePolicies", + "iam:GetUser", + "iam:GetRole", + "sts:GetCallerIdentity" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam::*:role/ResponseActionsRemovePolicyDelegateRole" + } + + ] +} diff --git a/modules/response-actions/variables.tf b/modules/response-actions/variables.tf new file mode 100644 index 0000000..e0cef60 --- /dev/null +++ b/modules/response-actions/variables.tf @@ -0,0 +1,117 @@ +variable "is_organizational" { + description = "(Optional) Set this field to 'true' to deploy EventBridge to an AWS Organization (Or specific OUs)" + type = bool + default = false +} + +variable "org_units" { + description = <<-EOF + TO BE DEPRECATED on 30th November, 2025: Please work with Sysdig to migrate to using `include_ouids` instead. + When set, list of Organization Unit IDs in which to setup EventBridge. By default, EventBridge will be setup in all accounts within the Organization. + This field is ignored if `is_organizational = false` + EOF + type = set(string) + default = [] +} + +variable "regions" { + description = "(Optional) List of regions in which to setup EventBridge. By default, current region is selected" + type = set(string) + default = [] +} + +variable "name" { + description = "(Optional) Name to be assigned to all child resources. A suffix may be added internally when required. Use default value unless you need to install multiple instances" + type = string + default = "sysdig-secure-events" +} + +variable "tags" { + description = "(Optional) Tags to be attached to all Sysdig resources." + type = map(string) + default = { + "product" = "sysdig-secure-for-cloud" + } +} + +variable "timeout" { + type = string + description = "Default timeout values for create, update, and delete operations" + default = "30m" +} + +variable "failure_tolerance_percentage" { + type = number + description = "The percentage of accounts, per Region, for which stack operations can fail before AWS CloudFormation stops the operation in that Region" + default = 90 +} + +variable "auto_create_stackset_roles" { + description = "Whether to auto create the custom stackset roles to run SELF_MANAGED stackset. Default is true" + type = bool + default = true +} + +variable "stackset_admin_role_arn" { + description = "(Optional) stackset admin role arn to run SELF_MANAGED stackset" + type = string + default = "" +} + +variable "stackset_execution_role_name" { + description = "(Optional) stackset execution role name to run SELF_MANAGED stackset" + type = string + default = "" +} + +variable "sysdig_secure_account_id" { + type = string + description = "ID of the Sysdig Cloud Account to enable Event Bridge integration for (incase of organization, ID of the Sysdig management account)" +} + +variable "is_gov_cloud_onboarding" { + type = bool + default = false + description = "true/false whether EventBridge should be deployed in a govcloud account/org or not" +} + +variable "include_ouids" { + description = "(Optional) ouids to include for organization" + type = set(string) + default = [] +} + +variable "exclude_ouids" { + description = "(Optional) ouids to exclude for organization" + type = set(string) + default = [] +} + +variable "include_accounts" { + description = "(Optional) accounts to include for organization" + type = set(string) + default = [] +} + +variable "exclude_accounts" { + description = "(Optional) accounts to exclude for organization" + type = set(string) + default = [] +} + +variable "api_dest_rate_limit" { + type = number + default = 300 + description = "Rate limit for API Destinations" +} + +variable "api_base_url" { + description = "Base URL for the API service" + type = string +} + +variable "response_actions_version" { + description = "Response Actions version" + type = string + default = "0.0.15" +} \ No newline at end of file diff --git a/modules/response-actions/versions.tf b/modules/response-actions/versions.tf new file mode 100644 index 0000000..db226f1 --- /dev/null +++ b/modules/response-actions/versions.tf @@ -0,0 +1,17 @@ +terraform { + required_version = ">= 1.0.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.1" + } + sysdig = { + source = "sysdiglabs/sysdig" + version = "~> 1.48" + } + } +} From ac094fcb9a3fa861f7fb7e1e3e0033f0b687d6e9 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Tue, 25 Nov 2025 18:47:50 +0100 Subject: [PATCH 02/26] Attempt to use stackset to deploy lambdas to all specified regions. --- modules/response-actions/main.tf | 340 +++++++++++++---- modules/response-actions/outputs.tf | 48 ++- .../templates/lambda-stackset.yaml | 352 ++++++++++++++++++ 3 files changed, 652 insertions(+), 88 deletions(-) create mode 100644 modules/response-actions/templates/lambda-stackset.yaml diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index e05b5b5..d274624 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -1,4 +1,5 @@ data "aws_caller_identity" "current" {} +data "aws_region" "current" {} locals { quarantine_user_policy = templatefile("${path.module}/policies/quarantine-user-policy.json", {}) @@ -8,18 +9,121 @@ locals { create_volume_snapshots_policy = templatefile("${path.module}/policies/create-volume-snapshots-policy.json", {}) delete_volume_snapshots_policy = templatefile("${path.module}/policies/delete-volume-snapshots-policy.json", {}) - region_set = toset(var.regions) + # Use provided regions or default to current region + region_set = length(var.regions) > 0 ? toset(var.regions) : toset([data.aws_region.current.name]) trusted_identity = var.is_gov_cloud_onboarding ? data.sysdig_secure_trusted_cloud_identity.trusted_identity.gov_identity : data.sysdig_secure_trusted_cloud_identity.trusted_identity.identity arn_prefix = var.is_gov_cloud_onboarding ? "arn:aws-us-gov" : "arn:aws" responder_component_type = "COMPONENT_CLOUD_RESPONDER" roles_component_type = "COMPONENT_CLOUD_RESPONDER_ROLES" account_id_hash = substr(md5(data.aws_caller_identity.current.account_id), 0, 4) ra_resource_name = "${var.name}-${random_id.suffix.hex}-${local.account_id_hash}" - common_environment_variables = { - API_BASE_URL = var.api_base_url + + # StackSet role configuration + administration_role_arn = var.auto_create_stackset_roles ? aws_iam_role.lambda_stackset_admin_role[0].arn : var.stackset_admin_role_arn + execution_role_name = var.auto_create_stackset_roles ? aws_iam_role.lambda_stackset_execution_role[0].name : var.stackset_execution_role_name +} + +#------------------------------------------------------ +# StackSet IAM Roles for multi-region deployment +#------------------------------------------------------ + +# StackSet Administration Role +resource "aws_iam_role" "lambda_stackset_admin_role" { + count = var.auto_create_stackset_roles ? 1 : 0 + name = "${local.ra_resource_name}-stackset-admin" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "cloudformation.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = { + Name = "${local.ra_resource_name}-stackset-admin" + } +} + +resource "aws_iam_role_policy" "lambda_stackset_admin_policy" { + count = var.auto_create_stackset_roles ? 1 : 0 + name = "${local.ra_resource_name}-stackset-admin-policy" + role = aws_iam_role.lambda_stackset_admin_role[0].id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "sts:AssumeRole" + Resource = "arn:aws:iam::*:role/${local.ra_resource_name}-stackset-execution" + } + ] + }) +} + +# StackSet Execution Role +resource "aws_iam_role" "lambda_stackset_execution_role" { + count = var.auto_create_stackset_roles ? 1 : 0 + name = "${local.ra_resource_name}-stackset-execution" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + AWS = var.auto_create_stackset_roles ? aws_iam_role.lambda_stackset_admin_role[0].arn : var.stackset_admin_role_arn + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = { + Name = "${local.ra_resource_name}-stackset-execution" } } +resource "aws_iam_role_policy" "lambda_stackset_execution_policy" { + count = var.auto_create_stackset_roles ? 1 : 0 + name = "${local.ra_resource_name}-stackset-execution-policy" + role = aws_iam_role.lambda_stackset_execution_role[0].id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "cloudformation:*", + "lambda:*", + "iam:CreateRole", + "iam:DeleteRole", + "iam:GetRole", + "iam:PassRole", + "iam:PutRolePolicy", + "iam:DeleteRolePolicy", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:GetRolePolicy", + "logs:CreateLogGroup", + "logs:DeleteLogGroup", + "logs:PutRetentionPolicy", + "logs:TagResource", + "logs:UntagResource" + ] + Resource = "*" + } + ] + }) +} + resource "random_id" "suffix" { byte_length = 3 } @@ -55,9 +159,9 @@ resource "aws_iam_role" "shared_cross_account_lambda_invoker" { } } -# Inline policy for invoking all Lambda functions +# Inline policy for invoking all Lambda functions across all deployed regions resource "aws_iam_role_policy" "shared_lambda_invoke_policy" { - name = "${ra_resource_name}-invoke-policy" + name = "${local.ra_resource_name}-invoke-policy" role = aws_iam_role.shared_cross_account_lambda_invoker.id policy = jsonencode({ @@ -69,103 +173,183 @@ resource "aws_iam_role_policy" "shared_lambda_invoke_policy" { "lambda:InvokeFunction", "lambda:GetFunction" ] - Resource = [ - "${arn_prefix}:lambda:*:*:function:${ra_resource_name}-quarantine-user", - "${arn_prefix}:lambda:*:*:function:${ra_resource_name}-fetch-cloud-logs", - "${arn_prefix}:lambda:*:*:function:${ra_resource_name}-remove-policy", - "${arn_prefix}:lambda:*:*:function:${ra_resource_name}-configure-resource-access", - "${arn_prefix}:lambda:*:*:function:${ra_resource_name}-create-volume-snapshots", - "${arn_prefix}:lambda:*:*:function:${ra_resource_name}-delete-volume-snapshots" - ] + Resource = concat( + # Quarantine User functions in all regions + [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-quarantine-user"], + # Fetch Cloud Logs functions in all regions + [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-fetch-cloud-logs"], + # Remove Policy functions in all regions + [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-remove-policy"], + # Configure Resource Access functions in all regions + [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-configure-resource-access"], + # Create Volume Snapshots functions in all regions + [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-create-volume-snapshots"], + # Delete Volume Snapshots functions in all regions + [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-delete-volume-snapshots"] + ) }, { - Effect : "Allow" - Action : [ + Effect = "Allow" + Action = [ "tag:GetResources" ] - Resource : "*" + Resource = "*" } ] }) } -# Lambda Function: Quarantine User -module "quarantine_user_function" { - source = "./modules/lambda-functions" +#------------------------------------------------------ +# S3 Bucket for Lambda deployment packages +#------------------------------------------------------ + +resource "aws_s3_bucket" "lambda_deployment" { + bucket = "${local.ra_resource_name}-lambda-deployment" - function_name = "${ra_resource_name}-quarantine-user" - function_zip_file = "quarantine_user.zip" - environment_variables = local.common_environment_variables - function_policies = [local.quarantine_user_policy] - lambda_handler = "app.index.handler" - response_actions_version = var.response_actions_version - arn_prefix = local.arn_prefix + tags = { + Name = "${local.ra_resource_name}-lambda-deployment" + "sysdig.com/response-actions/cloud-actions" = "true" + } } -# Lambda Function: Fetch cloud logs -module "fetch_cloud_logs_function" { - source = "./modules/lambda-functions" +resource "aws_s3_bucket_versioning" "lambda_deployment" { + bucket = aws_s3_bucket.lambda_deployment.id + versioning_configuration { + status = "Enabled" + } +} - function_name = "${ra_resource_name}-fetch-cloud-logs" - function_zip_file = "fetch_cloud_logs.zip" - environment_variables = local.common_environment_variables - function_policies = [local.fetch_cloud_logs_policy] - lambda_handler = "app.index.handler" - response_actions_version = var.response_actions_version - arn_prefix = local.arn_prefix +# Download Lambda ZIP files +data "http" "quarantine_user_zip" { + url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/quarantine_user.zip" } -# Lambda Function: Remove Policy -module "remove_policy_function" { - source = "./modules/lambda-functions" +data "http" "fetch_cloud_logs_zip" { + url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/fetch_cloud_logs.zip" +} - function_name = "${ra_resource_name}-remove-policy" - function_zip_file = "remove_policy.zip" - environment_variables = local.common_environment_variables - function_policies = [local.remove_policy_policy] - lambda_handler = "app.index.handler" - response_actions_version = var.response_actions_version - arn_prefix = local.arn_prefix +data "http" "remove_policy_zip" { + url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/remove_policy.zip" } -# Lambda Function: Configure Resource Access -module "configure_resource_access_function" { - source = "./modules/lambda-functions" +data "http" "configure_resource_access_zip" { + url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/configure_resource_access.zip" +} - function_name = "${ra_resource_name}-configure-resource-access" - function_zip_file = "configure_resource_access.zip" - environment_variables = local.common_environment_variables - function_policies = [local.configure_resource_access_policy] - lambda_handler = "app.index.handler" - response_actions_version = var.response_actions_version - arn_prefix = local.arn_prefix +data "http" "create_volume_snapshot_zip" { + url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/create_volume_snapshot.zip" } -# Lambda Function: Create Volume Snapshots -module "create_volume_snapshots_function" { - source = "./modules/lambda-functions" +data "http" "delete_volume_snapshot_zip" { + url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/delete_volume_snapshot.zip" +} - function_name = "${ra_resource_name}-create-volume-snapshots" - function_zip_file = "create_volume_snapshot.zip" - environment_variables = local.common_environment_variables - function_policies = [local.create_volume_snapshots_policy] - lambda_handler = "app.index.handler" - response_actions_version = var.response_actions_version - arn_prefix = local.arn_prefix +# Upload Lambda ZIP files to S3 +resource "aws_s3_object" "quarantine_user_zip" { + bucket = aws_s3_bucket.lambda_deployment.id + key = "quarantine_user.zip" + content = data.http.quarantine_user_zip.response_body + content_type = "application/zip" } -# Lambda Function: Delete Volume Snapshots -module "delete_volume_snapshots_function" { - source = "./modules/lambda-functions" +resource "aws_s3_object" "fetch_cloud_logs_zip" { + bucket = aws_s3_bucket.lambda_deployment.id + key = "fetch_cloud_logs.zip" + content = data.http.fetch_cloud_logs_zip.response_body + content_type = "application/zip" +} - function_name = "${ra_resource_name}-delete-volume-snapshots" - function_zip_file = "delete_volume_snapshot.zip" - environment_variables = local.common_environment_variables - function_policies = [local.delete_volume_snapshots_policy] - lambda_handler = "app.index.handler" - response_actions_version = var.response_actions_version - arn_prefix = local.arn_prefix +resource "aws_s3_object" "remove_policy_zip" { + bucket = aws_s3_bucket.lambda_deployment.id + key = "remove_policy.zip" + content = data.http.remove_policy_zip.response_body + content_type = "application/zip" } -data "aws_caller_identity" "current" {} -data "aws_region" "current" {} +resource "aws_s3_object" "configure_resource_access_zip" { + bucket = aws_s3_bucket.lambda_deployment.id + key = "configure_resource_access.zip" + content = data.http.configure_resource_access_zip.response_body + content_type = "application/zip" +} + +resource "aws_s3_object" "create_volume_snapshot_zip" { + bucket = aws_s3_bucket.lambda_deployment.id + key = "create_volume_snapshot.zip" + content = data.http.create_volume_snapshot_zip.response_body + content_type = "application/zip" +} + +resource "aws_s3_object" "delete_volume_snapshot_zip" { + bucket = aws_s3_bucket.lambda_deployment.id + key = "delete_volume_snapshot.zip" + content = data.http.delete_volume_snapshot_zip.response_body + content_type = "application/zip" +} + +#------------------------------------------------------ +# CloudFormation StackSet for Multi-Region Lambda Deployment +#------------------------------------------------------ + +resource "aws_cloudformation_stack_set" "lambda_functions" { + name = "${local.ra_resource_name}-lambda" + tags = var.tags + permission_model = "SELF_MANAGED" + capabilities = ["CAPABILITY_NAMED_IAM"] + administration_role_arn = local.administration_role_arn + execution_role_name = local.execution_role_name + + managed_execution { + active = true + } + + lifecycle { + ignore_changes = [administration_role_arn] + } + + parameters = { + ResourceName = local.ra_resource_name + S3Bucket = aws_s3_bucket.lambda_deployment.id + ApiBaseUrl = var.api_base_url + ArnPrefix = local.arn_prefix + QuarantineUserPolicy = local.quarantine_user_policy + FetchCloudLogsPolicy = local.fetch_cloud_logs_policy + RemovePolicyPolicy = local.remove_policy_policy + ConfigureResourceAccessPolicy = local.configure_resource_access_policy + CreateVolumeSnapshotsPolicy = local.create_volume_snapshots_policy + DeleteVolumeSnapshotsPolicy = local.delete_volume_snapshots_policy + } + + template_body = file("${path.module}/templates/lambda-stackset.yaml") + + depends_on = [ + aws_iam_role.lambda_stackset_admin_role, + aws_iam_role.lambda_stackset_execution_role, + aws_s3_object.quarantine_user_zip, + aws_s3_object.fetch_cloud_logs_zip, + aws_s3_object.remove_policy_zip, + aws_s3_object.configure_resource_access_zip, + aws_s3_object.create_volume_snapshot_zip, + aws_s3_object.delete_volume_snapshot_zip + ] +} + +# StackSet instances to deploy Lambda functions in all specified regions +resource "aws_cloudformation_stack_set_instance" "lambda_functions" { + for_each = local.region_set + stack_set_instance_region = each.key + + stack_set_name = aws_cloudformation_stack_set.lambda_functions.name + operation_preferences { + max_concurrent_percentage = 100 + failure_tolerance_percentage = var.failure_tolerance_percentage + concurrency_mode = "SOFT_FAILURE_TOLERANCE" + region_concurrency_type = "PARALLEL" + } + + timeouts { + create = var.timeout + update = var.timeout + delete = var.timeout + } +} diff --git a/modules/response-actions/outputs.tf b/modules/response-actions/outputs.tf index a0d456d..fadb86c 100644 --- a/modules/response-actions/outputs.tf +++ b/modules/response-actions/outputs.tf @@ -3,24 +3,52 @@ output "cross_account_role_arn" { value = aws_iam_role.shared_cross_account_lambda_invoker.arn } +output "stackset_name" { + description = "Name of the CloudFormation StackSet deploying Lambda functions" + value = aws_cloudformation_stack_set.lambda_functions.name +} + +output "stackset_id" { + description = "ID of the CloudFormation StackSet" + value = aws_cloudformation_stack_set.lambda_functions.id +} + +output "deployment_regions" { + description = "List of regions where Lambda functions are deployed" + value = local.region_set +} + +output "s3_deployment_bucket" { + description = "S3 bucket containing Lambda deployment packages" + value = aws_s3_bucket.lambda_deployment.id +} + output "lambda_functions" { - description = "Information about deployed Lambda functions" + description = "Information about deployed Lambda functions across all regions" value = { quarantine_user = { - name = module.quarantine_user_function.function_name - arn = module.quarantine_user_function.function_arn + name = "${local.ra_resource_name}-quarantine-user" + arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-quarantine-user"] + } + fetch_cloud_logs = { + name = "${local.ra_resource_name}-fetch-cloud-logs" + arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-fetch-cloud-logs"] } remove_policy = { - name = module.remove_policy_function.function_name - arn = module.remove_policy_function.function_arn + name = "${local.ra_resource_name}-remove-policy" + arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-remove-policy"] } - fetch_cloud_logs = { - name = module.fetch_cloud_logs_function.function_name - arn = module.fetch_cloud_logs_function.function_arn + configure_resource_access = { + name = "${local.ra_resource_name}-configure-resource-access" + arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-configure-resource-access"] } create_volume_snapshots = { - name = module.create_volume_snapshots_function.function_name - arn = module.create_volume_snapshots_function.function_arn + name = "${local.ra_resource_name}-create-volume-snapshots" + arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-create-volume-snapshots"] + } + delete_volume_snapshots = { + name = "${local.ra_resource_name}-delete-volume-snapshots" + arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-delete-volume-snapshots"] } } } diff --git a/modules/response-actions/templates/lambda-stackset.yaml b/modules/response-actions/templates/lambda-stackset.yaml new file mode 100644 index 0000000..b8ad3d9 --- /dev/null +++ b/modules/response-actions/templates/lambda-stackset.yaml @@ -0,0 +1,352 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'Sysdig Response Actions Lambda Functions - Multi-Region Deployment' + +Parameters: + ResourceName: + Type: String + Description: Base name for all resources + S3Bucket: + Type: String + Description: S3 bucket containing Lambda deployment packages + ApiBaseUrl: + Type: String + Description: API base URL for Lambda functions + ArnPrefix: + Type: String + Description: ARN prefix (arn:aws or arn:aws-us-gov) + Default: arn:aws + QuarantineUserPolicy: + Type: String + Description: JSON policy for quarantine user function + FetchCloudLogsPolicy: + Type: String + Description: JSON policy for fetch cloud logs function + RemovePolicyPolicy: + Type: String + Description: JSON policy for remove policy function + ConfigureResourceAccessPolicy: + Type: String + Description: JSON policy for configure resource access function + CreateVolumeSnapshotsPolicy: + Type: String + Description: JSON policy for create volume snapshots function + DeleteVolumeSnapshotsPolicy: + Type: String + Description: JSON policy for delete volume snapshots function + +Resources: + # Lambda Execution Role for Quarantine User + QuarantineUserRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub '${ResourceName}-quarantine-user-role' + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: 'sts:AssumeRole' + ManagedPolicyArns: + - !Sub '${ArnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' + Policies: + - PolicyName: QuarantineUserPolicy + PolicyDocument: !Sub '${QuarantineUserPolicy}' + Tags: + - Key: Name + Value: !Sub '${ResourceName}-quarantine-user-role' + + # Lambda Execution Role for Fetch Cloud Logs + FetchCloudLogsRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub '${ResourceName}-fetch-cloud-logs-role' + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: 'sts:AssumeRole' + ManagedPolicyArns: + - !Sub '${ArnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' + Policies: + - PolicyName: FetchCloudLogsPolicy + PolicyDocument: !Sub '${FetchCloudLogsPolicy}' + Tags: + - Key: Name + Value: !Sub '${ResourceName}-fetch-cloud-logs-role' + + # Lambda Execution Role for Remove Policy + RemovePolicyRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub '${ResourceName}-remove-policy-role' + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: 'sts:AssumeRole' + ManagedPolicyArns: + - !Sub '${ArnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' + Policies: + - PolicyName: RemovePolicyPolicy + PolicyDocument: !Sub '${RemovePolicyPolicy}' + Tags: + - Key: Name + Value: !Sub '${ResourceName}-remove-policy-role' + + # Lambda Execution Role for Configure Resource Access + ConfigureResourceAccessRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub '${ResourceName}-configure-resource-access-role' + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: 'sts:AssumeRole' + ManagedPolicyArns: + - !Sub '${ArnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' + Policies: + - PolicyName: ConfigureResourceAccessPolicy + PolicyDocument: !Sub '${ConfigureResourceAccessPolicy}' + Tags: + - Key: Name + Value: !Sub '${ResourceName}-configure-resource-access-role' + + # Lambda Execution Role for Create Volume Snapshots + CreateVolumeSnapshotsRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub '${ResourceName}-create-volume-snapshots-role' + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: 'sts:AssumeRole' + ManagedPolicyArns: + - !Sub '${ArnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' + Policies: + - PolicyName: CreateVolumeSnapshotsPolicy + PolicyDocument: !Sub '${CreateVolumeSnapshotsPolicy}' + Tags: + - Key: Name + Value: !Sub '${ResourceName}-create-volume-snapshots-role' + + # Lambda Execution Role for Delete Volume Snapshots + DeleteVolumeSnapshotsRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub '${ResourceName}-delete-volume-snapshots-role' + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: 'sts:AssumeRole' + ManagedPolicyArns: + - !Sub '${ArnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' + Policies: + - PolicyName: DeleteVolumeSnapshotsPolicy + PolicyDocument: !Sub '${DeleteVolumeSnapshotsPolicy}' + Tags: + - Key: Name + Value: !Sub '${ResourceName}-delete-volume-snapshots-role' + + # CloudWatch Log Groups + QuarantineUserLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub '/aws/lambda/${ResourceName}-quarantine-user' + RetentionInDays: 7 + + FetchCloudLogsLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub '/aws/lambda/${ResourceName}-fetch-cloud-logs' + RetentionInDays: 7 + + RemovePolicyLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub '/aws/lambda/${ResourceName}-remove-policy' + RetentionInDays: 7 + + ConfigureResourceAccessLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub '/aws/lambda/${ResourceName}-configure-resource-access' + RetentionInDays: 7 + + CreateVolumeSnapshotsLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub '/aws/lambda/${ResourceName}-create-volume-snapshots' + RetentionInDays: 7 + + DeleteVolumeSnapshotsLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub '/aws/lambda/${ResourceName}-delete-volume-snapshots' + RetentionInDays: 7 + + # Lambda Functions + QuarantineUserFunction: + Type: AWS::Lambda::Function + DependsOn: QuarantineUserLogGroup + Properties: + FunctionName: !Sub '${ResourceName}-quarantine-user' + Runtime: python3.12 + Handler: app.index.handler + Role: !GetAtt QuarantineUserRole.Arn + Code: + S3Bucket: !Ref S3Bucket + S3Key: quarantine_user.zip + Timeout: 300 + MemorySize: 128 + Environment: + Variables: + API_BASE_URL: !Ref ApiBaseUrl + DELEGATE_ROLE_NAME: !Ref QuarantineUserRole + Tags: + - Key: Name + Value: !Sub '${ResourceName}-quarantine-user' + - Key: 'sysdig.com/response-actions/cloud-actions' + Value: 'true' + + FetchCloudLogsFunction: + Type: AWS::Lambda::Function + DependsOn: FetchCloudLogsLogGroup + Properties: + FunctionName: !Sub '${ResourceName}-fetch-cloud-logs' + Runtime: python3.12 + Handler: app.index.handler + Role: !GetAtt FetchCloudLogsRole.Arn + Code: + S3Bucket: !Ref S3Bucket + S3Key: fetch_cloud_logs.zip + Timeout: 300 + MemorySize: 128 + Environment: + Variables: + API_BASE_URL: !Ref ApiBaseUrl + DELEGATE_ROLE_NAME: !Ref FetchCloudLogsRole + Tags: + - Key: Name + Value: !Sub '${ResourceName}-fetch-cloud-logs' + - Key: 'sysdig.com/response-actions/cloud-actions' + Value: 'true' + + RemovePolicyFunction: + Type: AWS::Lambda::Function + DependsOn: RemovePolicyLogGroup + Properties: + FunctionName: !Sub '${ResourceName}-remove-policy' + Runtime: python3.12 + Handler: app.index.handler + Role: !GetAtt RemovePolicyRole.Arn + Code: + S3Bucket: !Ref S3Bucket + S3Key: remove_policy.zip + Timeout: 300 + MemorySize: 128 + Environment: + Variables: + API_BASE_URL: !Ref ApiBaseUrl + DELEGATE_ROLE_NAME: !Ref RemovePolicyRole + Tags: + - Key: Name + Value: !Sub '${ResourceName}-remove-policy' + - Key: 'sysdig.com/response-actions/cloud-actions' + Value: 'true' + + ConfigureResourceAccessFunction: + Type: AWS::Lambda::Function + DependsOn: ConfigureResourceAccessLogGroup + Properties: + FunctionName: !Sub '${ResourceName}-configure-resource-access' + Runtime: python3.12 + Handler: app.index.handler + Role: !GetAtt ConfigureResourceAccessRole.Arn + Code: + S3Bucket: !Ref S3Bucket + S3Key: configure_resource_access.zip + Timeout: 300 + MemorySize: 128 + Environment: + Variables: + API_BASE_URL: !Ref ApiBaseUrl + DELEGATE_ROLE_NAME: !Ref ConfigureResourceAccessRole + Tags: + - Key: Name + Value: !Sub '${ResourceName}-configure-resource-access' + - Key: 'sysdig.com/response-actions/cloud-actions' + Value: 'true' + + CreateVolumeSnapshotsFunction: + Type: AWS::Lambda::Function + DependsOn: CreateVolumeSnapshotsLogGroup + Properties: + FunctionName: !Sub '${ResourceName}-create-volume-snapshots' + Runtime: python3.12 + Handler: app.index.handler + Role: !GetAtt CreateVolumeSnapshotsRole.Arn + Code: + S3Bucket: !Ref S3Bucket + S3Key: create_volume_snapshot.zip + Timeout: 300 + MemorySize: 128 + Environment: + Variables: + API_BASE_URL: !Ref ApiBaseUrl + DELEGATE_ROLE_NAME: !Ref CreateVolumeSnapshotsRole + Tags: + - Key: Name + Value: !Sub '${ResourceName}-create-volume-snapshots' + - Key: 'sysdig.com/response-actions/cloud-actions' + Value: 'true' + + DeleteVolumeSnapshotsFunction: + Type: AWS::Lambda::Function + DependsOn: DeleteVolumeSnapshotsLogGroup + Properties: + FunctionName: !Sub '${ResourceName}-delete-volume-snapshots' + Runtime: python3.12 + Handler: app.index.handler + Role: !GetAtt DeleteVolumeSnapshotsRole.Arn + Code: + S3Bucket: !Ref S3Bucket + S3Key: delete_volume_snapshot.zip + Timeout: 300 + MemorySize: 128 + Environment: + Variables: + API_BASE_URL: !Ref ApiBaseUrl + DELEGATE_ROLE_NAME: !Ref DeleteVolumeSnapshotsRole + Tags: + - Key: Name + Value: !Sub '${ResourceName}-delete-volume-snapshots' + - Key: 'sysdig.com/response-actions/cloud-actions' + Value: 'true' + +Outputs: + QuarantineUserFunctionArn: + Value: !GetAtt QuarantineUserFunction.Arn + FetchCloudLogsFunctionArn: + Value: !GetAtt FetchCloudLogsFunction.Arn + RemovePolicyFunctionArn: + Value: !GetAtt RemovePolicyFunction.Arn + ConfigureResourceAccessFunctionArn: + Value: !GetAtt ConfigureResourceAccessFunction.Arn + CreateVolumeSnapshotsFunctionArn: + Value: !GetAtt CreateVolumeSnapshotsFunction.Arn + DeleteVolumeSnapshotsFunctionArn: + Value: !GetAtt DeleteVolumeSnapshotsFunction.Arn From 0f93d87a6797bf9888a4afc344eb18f2af044f9c Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Tue, 25 Nov 2025 19:01:18 +0100 Subject: [PATCH 03/26] Roles extracted from cloudformation stack as they are not regional. --- modules/response-actions/main.tf | 233 +++++++++++++++++- .../templates/lambda-stackset.yaml | 198 ++++----------- 2 files changed, 266 insertions(+), 165 deletions(-) diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index d274624..e15c2d6 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -199,6 +199,214 @@ resource "aws_iam_role_policy" "shared_lambda_invoke_policy" { }) } +#------------------------------------------------------ +# IAM Roles for Lambda Functions (Global Resources) +#------------------------------------------------------ + +# Lambda Execution Role: Quarantine User +resource "aws_iam_role" "quarantine_user_role" { + name = "${local.ra_resource_name}-quarantine-user-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = { + Name = "${local.ra_resource_name}-quarantine-user-role" + "sysdig.com/response-actions/cloud-actions" = "true" + } +} + +resource "aws_iam_role_policy_attachment" "quarantine_user_basic" { + policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = aws_iam_role.quarantine_user_role.name +} + +resource "aws_iam_role_policy" "quarantine_user_policy" { + name = "${local.ra_resource_name}-quarantine-user-policy" + role = aws_iam_role.quarantine_user_role.id + policy = local.quarantine_user_policy +} + +# Lambda Execution Role: Fetch Cloud Logs +resource "aws_iam_role" "fetch_cloud_logs_role" { + name = "${local.ra_resource_name}-fetch-cloud-logs-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = { + Name = "${local.ra_resource_name}-fetch-cloud-logs-role" + "sysdig.com/response-actions/cloud-actions" = "true" + } +} + +resource "aws_iam_role_policy_attachment" "fetch_cloud_logs_basic" { + policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = aws_iam_role.fetch_cloud_logs_role.name +} + +resource "aws_iam_role_policy" "fetch_cloud_logs_policy" { + name = "${local.ra_resource_name}-fetch-cloud-logs-policy" + role = aws_iam_role.fetch_cloud_logs_role.id + policy = local.fetch_cloud_logs_policy +} + +# Lambda Execution Role: Remove Policy +resource "aws_iam_role" "remove_policy_role" { + name = "${local.ra_resource_name}-remove-policy-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = { + Name = "${local.ra_resource_name}-remove-policy-role" + "sysdig.com/response-actions/cloud-actions" = "true" + } +} + +resource "aws_iam_role_policy_attachment" "remove_policy_basic" { + policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = aws_iam_role.remove_policy_role.name +} + +resource "aws_iam_role_policy" "remove_policy_policy" { + name = "${local.ra_resource_name}-remove-policy-policy" + role = aws_iam_role.remove_policy_role.id + policy = local.remove_policy_policy +} + +# Lambda Execution Role: Configure Resource Access +resource "aws_iam_role" "configure_resource_access_role" { + name = "${local.ra_resource_name}-configure-resource-access-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = { + Name = "${local.ra_resource_name}-configure-resource-access-role" + "sysdig.com/response-actions/cloud-actions" = "true" + } +} + +resource "aws_iam_role_policy_attachment" "configure_resource_access_basic" { + policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = aws_iam_role.configure_resource_access_role.name +} + +resource "aws_iam_role_policy" "configure_resource_access_policy" { + name = "${local.ra_resource_name}-configure-resource-access-policy" + role = aws_iam_role.configure_resource_access_role.id + policy = local.configure_resource_access_policy +} + +# Lambda Execution Role: Create Volume Snapshots +resource "aws_iam_role" "create_volume_snapshots_role" { + name = "${local.ra_resource_name}-create-volume-snapshots-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = { + Name = "${local.ra_resource_name}-create-volume-snapshots-role" + "sysdig.com/response-actions/cloud-actions" = "true" + } +} + +resource "aws_iam_role_policy_attachment" "create_volume_snapshots_basic" { + policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = aws_iam_role.create_volume_snapshots_role.name +} + +resource "aws_iam_role_policy" "create_volume_snapshots_policy" { + name = "${local.ra_resource_name}-create-volume-snapshots-policy" + role = aws_iam_role.create_volume_snapshots_role.id + policy = local.create_volume_snapshots_policy +} + +# Lambda Execution Role: Delete Volume Snapshots +resource "aws_iam_role" "delete_volume_snapshots_role" { + name = "${local.ra_resource_name}-delete-volume-snapshots-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = { + Name = "${local.ra_resource_name}-delete-volume-snapshots-role" + "sysdig.com/response-actions/cloud-actions" = "true" + } +} + +resource "aws_iam_role_policy_attachment" "delete_volume_snapshots_basic" { + policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = aws_iam_role.delete_volume_snapshots_role.name +} + +resource "aws_iam_role_policy" "delete_volume_snapshots_policy" { + name = "${local.ra_resource_name}-delete-volume-snapshots-policy" + role = aws_iam_role.delete_volume_snapshots_role.id + policy = local.delete_volume_snapshots_policy +} + #------------------------------------------------------ # S3 Bucket for Lambda deployment packages #------------------------------------------------------ @@ -308,16 +516,21 @@ resource "aws_cloudformation_stack_set" "lambda_functions" { } parameters = { - ResourceName = local.ra_resource_name - S3Bucket = aws_s3_bucket.lambda_deployment.id - ApiBaseUrl = var.api_base_url - ArnPrefix = local.arn_prefix - QuarantineUserPolicy = local.quarantine_user_policy - FetchCloudLogsPolicy = local.fetch_cloud_logs_policy - RemovePolicyPolicy = local.remove_policy_policy - ConfigureResourceAccessPolicy = local.configure_resource_access_policy - CreateVolumeSnapshotsPolicy = local.create_volume_snapshots_policy - DeleteVolumeSnapshotsPolicy = local.delete_volume_snapshots_policy + ResourceName = local.ra_resource_name + S3Bucket = aws_s3_bucket.lambda_deployment.id + ApiBaseUrl = var.api_base_url + QuarantineUserRoleArn = aws_iam_role.quarantine_user_role.arn + FetchCloudLogsRoleArn = aws_iam_role.fetch_cloud_logs_role.arn + RemovePolicyRoleArn = aws_iam_role.remove_policy_role.arn + ConfigureResourceAccessRoleArn = aws_iam_role.configure_resource_access_role.arn + CreateVolumeSnapshotsRoleArn = aws_iam_role.create_volume_snapshots_role.arn + DeleteVolumeSnapshotsRoleArn = aws_iam_role.delete_volume_snapshots_role.arn + QuarantineUserRoleName = aws_iam_role.quarantine_user_role.name + FetchCloudLogsRoleName = aws_iam_role.fetch_cloud_logs_role.name + RemovePolicyRoleName = aws_iam_role.remove_policy_role.name + ConfigureResourceAccessRoleName = aws_iam_role.configure_resource_access_role.name + CreateVolumeSnapshotsRoleName = aws_iam_role.create_volume_snapshots_role.name + DeleteVolumeSnapshotsRoleName = aws_iam_role.delete_volume_snapshots_role.name } template_body = file("${path.module}/templates/lambda-stackset.yaml") diff --git a/modules/response-actions/templates/lambda-stackset.yaml b/modules/response-actions/templates/lambda-stackset.yaml index b8ad3d9..0d9dbb3 100644 --- a/modules/response-actions/templates/lambda-stackset.yaml +++ b/modules/response-actions/templates/lambda-stackset.yaml @@ -11,157 +11,45 @@ Parameters: ApiBaseUrl: Type: String Description: API base URL for Lambda functions - ArnPrefix: + QuarantineUserRoleArn: Type: String - Description: ARN prefix (arn:aws or arn:aws-us-gov) - Default: arn:aws - QuarantineUserPolicy: + Description: ARN of the IAM role for quarantine user function + FetchCloudLogsRoleArn: Type: String - Description: JSON policy for quarantine user function - FetchCloudLogsPolicy: + Description: ARN of the IAM role for fetch cloud logs function + RemovePolicyRoleArn: Type: String - Description: JSON policy for fetch cloud logs function - RemovePolicyPolicy: + Description: ARN of the IAM role for remove policy function + ConfigureResourceAccessRoleArn: Type: String - Description: JSON policy for remove policy function - ConfigureResourceAccessPolicy: + Description: ARN of the IAM role for configure resource access function + CreateVolumeSnapshotsRoleArn: Type: String - Description: JSON policy for configure resource access function - CreateVolumeSnapshotsPolicy: + Description: ARN of the IAM role for create volume snapshots function + DeleteVolumeSnapshotsRoleArn: Type: String - Description: JSON policy for create volume snapshots function - DeleteVolumeSnapshotsPolicy: + Description: ARN of the IAM role for delete volume snapshots function + QuarantineUserRoleName: Type: String - Description: JSON policy for delete volume snapshots function + Description: Name of the IAM role for quarantine user function + FetchCloudLogsRoleName: + Type: String + Description: Name of the IAM role for fetch cloud logs function + RemovePolicyRoleName: + Type: String + Description: Name of the IAM role for remove policy function + ConfigureResourceAccessRoleName: + Type: String + Description: Name of the IAM role for configure resource access function + CreateVolumeSnapshotsRoleName: + Type: String + Description: Name of the IAM role for create volume snapshots function + DeleteVolumeSnapshotsRoleName: + Type: String + Description: Name of the IAM role for delete volume snapshots function Resources: - # Lambda Execution Role for Quarantine User - QuarantineUserRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub '${ResourceName}-quarantine-user-role' - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: 'sts:AssumeRole' - ManagedPolicyArns: - - !Sub '${ArnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' - Policies: - - PolicyName: QuarantineUserPolicy - PolicyDocument: !Sub '${QuarantineUserPolicy}' - Tags: - - Key: Name - Value: !Sub '${ResourceName}-quarantine-user-role' - - # Lambda Execution Role for Fetch Cloud Logs - FetchCloudLogsRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub '${ResourceName}-fetch-cloud-logs-role' - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: 'sts:AssumeRole' - ManagedPolicyArns: - - !Sub '${ArnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' - Policies: - - PolicyName: FetchCloudLogsPolicy - PolicyDocument: !Sub '${FetchCloudLogsPolicy}' - Tags: - - Key: Name - Value: !Sub '${ResourceName}-fetch-cloud-logs-role' - - # Lambda Execution Role for Remove Policy - RemovePolicyRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub '${ResourceName}-remove-policy-role' - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: 'sts:AssumeRole' - ManagedPolicyArns: - - !Sub '${ArnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' - Policies: - - PolicyName: RemovePolicyPolicy - PolicyDocument: !Sub '${RemovePolicyPolicy}' - Tags: - - Key: Name - Value: !Sub '${ResourceName}-remove-policy-role' - - # Lambda Execution Role for Configure Resource Access - ConfigureResourceAccessRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub '${ResourceName}-configure-resource-access-role' - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: 'sts:AssumeRole' - ManagedPolicyArns: - - !Sub '${ArnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' - Policies: - - PolicyName: ConfigureResourceAccessPolicy - PolicyDocument: !Sub '${ConfigureResourceAccessPolicy}' - Tags: - - Key: Name - Value: !Sub '${ResourceName}-configure-resource-access-role' - - # Lambda Execution Role for Create Volume Snapshots - CreateVolumeSnapshotsRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub '${ResourceName}-create-volume-snapshots-role' - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: 'sts:AssumeRole' - ManagedPolicyArns: - - !Sub '${ArnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' - Policies: - - PolicyName: CreateVolumeSnapshotsPolicy - PolicyDocument: !Sub '${CreateVolumeSnapshotsPolicy}' - Tags: - - Key: Name - Value: !Sub '${ResourceName}-create-volume-snapshots-role' - - # Lambda Execution Role for Delete Volume Snapshots - DeleteVolumeSnapshotsRole: - Type: AWS::IAM::Role - Properties: - RoleName: !Sub '${ResourceName}-delete-volume-snapshots-role' - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: 'sts:AssumeRole' - ManagedPolicyArns: - - !Sub '${ArnPrefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' - Policies: - - PolicyName: DeleteVolumeSnapshotsPolicy - PolicyDocument: !Sub '${DeleteVolumeSnapshotsPolicy}' - Tags: - - Key: Name - Value: !Sub '${ResourceName}-delete-volume-snapshots-role' - - # CloudWatch Log Groups + # CloudWatch Log Groups (Regional Resources) QuarantineUserLogGroup: Type: AWS::Logs::LogGroup Properties: @@ -198,7 +86,7 @@ Resources: LogGroupName: !Sub '/aws/lambda/${ResourceName}-delete-volume-snapshots' RetentionInDays: 7 - # Lambda Functions + # Lambda Functions (Regional Resources) QuarantineUserFunction: Type: AWS::Lambda::Function DependsOn: QuarantineUserLogGroup @@ -206,7 +94,7 @@ Resources: FunctionName: !Sub '${ResourceName}-quarantine-user' Runtime: python3.12 Handler: app.index.handler - Role: !GetAtt QuarantineUserRole.Arn + Role: !Ref QuarantineUserRoleArn Code: S3Bucket: !Ref S3Bucket S3Key: quarantine_user.zip @@ -215,7 +103,7 @@ Resources: Environment: Variables: API_BASE_URL: !Ref ApiBaseUrl - DELEGATE_ROLE_NAME: !Ref QuarantineUserRole + DELEGATE_ROLE_NAME: !Ref QuarantineUserRoleName Tags: - Key: Name Value: !Sub '${ResourceName}-quarantine-user' @@ -229,7 +117,7 @@ Resources: FunctionName: !Sub '${ResourceName}-fetch-cloud-logs' Runtime: python3.12 Handler: app.index.handler - Role: !GetAtt FetchCloudLogsRole.Arn + Role: !Ref FetchCloudLogsRoleArn Code: S3Bucket: !Ref S3Bucket S3Key: fetch_cloud_logs.zip @@ -238,7 +126,7 @@ Resources: Environment: Variables: API_BASE_URL: !Ref ApiBaseUrl - DELEGATE_ROLE_NAME: !Ref FetchCloudLogsRole + DELEGATE_ROLE_NAME: !Ref FetchCloudLogsRoleName Tags: - Key: Name Value: !Sub '${ResourceName}-fetch-cloud-logs' @@ -252,7 +140,7 @@ Resources: FunctionName: !Sub '${ResourceName}-remove-policy' Runtime: python3.12 Handler: app.index.handler - Role: !GetAtt RemovePolicyRole.Arn + Role: !Ref RemovePolicyRoleArn Code: S3Bucket: !Ref S3Bucket S3Key: remove_policy.zip @@ -261,7 +149,7 @@ Resources: Environment: Variables: API_BASE_URL: !Ref ApiBaseUrl - DELEGATE_ROLE_NAME: !Ref RemovePolicyRole + DELEGATE_ROLE_NAME: !Ref RemovePolicyRoleName Tags: - Key: Name Value: !Sub '${ResourceName}-remove-policy' @@ -275,7 +163,7 @@ Resources: FunctionName: !Sub '${ResourceName}-configure-resource-access' Runtime: python3.12 Handler: app.index.handler - Role: !GetAtt ConfigureResourceAccessRole.Arn + Role: !Ref ConfigureResourceAccessRoleArn Code: S3Bucket: !Ref S3Bucket S3Key: configure_resource_access.zip @@ -284,7 +172,7 @@ Resources: Environment: Variables: API_BASE_URL: !Ref ApiBaseUrl - DELEGATE_ROLE_NAME: !Ref ConfigureResourceAccessRole + DELEGATE_ROLE_NAME: !Ref ConfigureResourceAccessRoleName Tags: - Key: Name Value: !Sub '${ResourceName}-configure-resource-access' @@ -298,7 +186,7 @@ Resources: FunctionName: !Sub '${ResourceName}-create-volume-snapshots' Runtime: python3.12 Handler: app.index.handler - Role: !GetAtt CreateVolumeSnapshotsRole.Arn + Role: !Ref CreateVolumeSnapshotsRoleArn Code: S3Bucket: !Ref S3Bucket S3Key: create_volume_snapshot.zip @@ -307,7 +195,7 @@ Resources: Environment: Variables: API_BASE_URL: !Ref ApiBaseUrl - DELEGATE_ROLE_NAME: !Ref CreateVolumeSnapshotsRole + DELEGATE_ROLE_NAME: !Ref CreateVolumeSnapshotsRoleName Tags: - Key: Name Value: !Sub '${ResourceName}-create-volume-snapshots' @@ -321,7 +209,7 @@ Resources: FunctionName: !Sub '${ResourceName}-delete-volume-snapshots' Runtime: python3.12 Handler: app.index.handler - Role: !GetAtt DeleteVolumeSnapshotsRole.Arn + Role: !Ref DeleteVolumeSnapshotsRoleArn Code: S3Bucket: !Ref S3Bucket S3Key: delete_volume_snapshot.zip @@ -330,7 +218,7 @@ Resources: Environment: Variables: API_BASE_URL: !Ref ApiBaseUrl - DELEGATE_ROLE_NAME: !Ref DeleteVolumeSnapshotsRole + DELEGATE_ROLE_NAME: !Ref DeleteVolumeSnapshotsRoleName Tags: - Key: Name Value: !Sub '${ResourceName}-delete-volume-snapshots' From 2f23ad9ca642feaef34648bffd1841d88a4d6eb5 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Wed, 26 Nov 2025 15:18:27 +0100 Subject: [PATCH 04/26] Some fixes --- modules/response-actions/main.tf | 70 +++++++++++++-------------- modules/response-actions/variables.tf | 4 +- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index e15c2d6..7d87b18 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -10,7 +10,7 @@ locals { delete_volume_snapshots_policy = templatefile("${path.module}/policies/delete-volume-snapshots-policy.json", {}) # Use provided regions or default to current region - region_set = length(var.regions) > 0 ? toset(var.regions) : toset([data.aws_region.current.name]) + region_set = length(var.regions) > 0 ? toset(var.regions) : toset([data.aws_region.current.id]) trusted_identity = var.is_gov_cloud_onboarding ? data.sysdig_secure_trusted_cloud_identity.trusted_identity.gov_identity : data.sysdig_secure_trusted_cloud_identity.trusted_identity.identity arn_prefix = var.is_gov_cloud_onboarding ? "arn:aws-us-gov" : "arn:aws" responder_component_type = "COMPONENT_CLOUD_RESPONDER" @@ -134,7 +134,7 @@ data "sysdig_secure_trusted_cloud_identity" "trusted_identity" { resource "aws_iam_role" "shared_cross_account_lambda_invoker" { - name = "${ra_resource_name}-cross-account-invoker" + name = "${local.ra_resource_name}-cross-account-invoker" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -155,7 +155,7 @@ resource "aws_iam_role" "shared_cross_account_lambda_invoker" { }) tags = { - Name = "${ra_resource_name}-cross-account-invoker" + Name = "${local.ra_resource_name}-cross-account-invoker" } } @@ -173,20 +173,20 @@ resource "aws_iam_role_policy" "shared_lambda_invoke_policy" { "lambda:InvokeFunction", "lambda:GetFunction" ] - Resource = concat( + Resource = [ # Quarantine User functions in all regions - [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-quarantine-user"], + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-quarantine-user", # Fetch Cloud Logs functions in all regions - [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-fetch-cloud-logs"], + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-fetch-cloud-logs", # Remove Policy functions in all regions - [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-remove-policy"], + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-remove-policy", # Configure Resource Access functions in all regions - [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-configure-resource-access"], + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-configure-resource-access", # Create Volume Snapshots functions in all regions - [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-create-volume-snapshots"], + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-create-volume-snapshots", # Delete Volume Snapshots functions in all regions - [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-delete-volume-snapshots"] - ) + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-delete-volume-snapshots" + ] }, { Effect = "Allow" @@ -454,45 +454,45 @@ data "http" "delete_volume_snapshot_zip" { # Upload Lambda ZIP files to S3 resource "aws_s3_object" "quarantine_user_zip" { - bucket = aws_s3_bucket.lambda_deployment.id - key = "quarantine_user.zip" - content = data.http.quarantine_user_zip.response_body - content_type = "application/zip" + bucket = aws_s3_bucket.lambda_deployment.id + key = "quarantine_user.zip" + content_base64 = data.http.quarantine_user_zip.response_body_base64 + content_type = "application/zip" } resource "aws_s3_object" "fetch_cloud_logs_zip" { - bucket = aws_s3_bucket.lambda_deployment.id - key = "fetch_cloud_logs.zip" - content = data.http.fetch_cloud_logs_zip.response_body - content_type = "application/zip" + bucket = aws_s3_bucket.lambda_deployment.id + key = "fetch_cloud_logs.zip" + content_base64 = data.http.fetch_cloud_logs_zip.response_body_base64 + content_type = "application/zip" } resource "aws_s3_object" "remove_policy_zip" { - bucket = aws_s3_bucket.lambda_deployment.id - key = "remove_policy.zip" - content = data.http.remove_policy_zip.response_body - content_type = "application/zip" + bucket = aws_s3_bucket.lambda_deployment.id + key = "remove_policy.zip" + content_base64 = data.http.remove_policy_zip.response_body_base64 + content_type = "application/zip" } resource "aws_s3_object" "configure_resource_access_zip" { - bucket = aws_s3_bucket.lambda_deployment.id - key = "configure_resource_access.zip" - content = data.http.configure_resource_access_zip.response_body - content_type = "application/zip" + bucket = aws_s3_bucket.lambda_deployment.id + key = "configure_resource_access.zip" + content_base64 = data.http.configure_resource_access_zip.response_body_base64 + content_type = "application/zip" } resource "aws_s3_object" "create_volume_snapshot_zip" { - bucket = aws_s3_bucket.lambda_deployment.id - key = "create_volume_snapshot.zip" - content = data.http.create_volume_snapshot_zip.response_body - content_type = "application/zip" + bucket = aws_s3_bucket.lambda_deployment.id + key = "create_volume_snapshot.zip" + content_base64 = data.http.create_volume_snapshot_zip.response_body_base64 + content_type = "application/zip" } resource "aws_s3_object" "delete_volume_snapshot_zip" { - bucket = aws_s3_bucket.lambda_deployment.id - key = "delete_volume_snapshot.zip" - content = data.http.delete_volume_snapshot_zip.response_body - content_type = "application/zip" + bucket = aws_s3_bucket.lambda_deployment.id + key = "delete_volume_snapshot.zip" + content_base64 = data.http.delete_volume_snapshot_zip.response_body_base64 + content_type = "application/zip" } #------------------------------------------------------ diff --git a/modules/response-actions/variables.tf b/modules/response-actions/variables.tf index e0cef60..3988b4b 100644 --- a/modules/response-actions/variables.tf +++ b/modules/response-actions/variables.tf @@ -23,7 +23,7 @@ variable "regions" { variable "name" { description = "(Optional) Name to be assigned to all child resources. A suffix may be added internally when required. Use default value unless you need to install multiple instances" type = string - default = "sysdig-secure-events" + default = "sysdig-secure-cloud-responder" } variable "tags" { @@ -66,7 +66,7 @@ variable "stackset_execution_role_name" { variable "sysdig_secure_account_id" { type = string - description = "ID of the Sysdig Cloud Account to enable Event Bridge integration for (incase of organization, ID of the Sysdig management account)" + description = "ID of the Sysdig Cloud Account to enable Response Actions integration for (incase of organization, ID of the Sysdig management account)" } variable "is_gov_cloud_onboarding" { From 4a11f2c5561d23c6508a82097ce9d472635bc405 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Wed, 26 Nov 2025 18:35:08 +0100 Subject: [PATCH 05/26] Adopting the strategy of fetching lambdas from Sysdig public S3 buckets. --- modules/response-actions/main.tf | 105 +++--------------- .../templates/lambda-stackset.yaml | 16 +-- modules/response-actions/variables.tf | 5 + 3 files changed, 26 insertions(+), 100 deletions(-) diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index 7d87b18..2b0bea5 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -410,90 +410,17 @@ resource "aws_iam_role_policy" "delete_volume_snapshots_policy" { #------------------------------------------------------ # S3 Bucket for Lambda deployment packages #------------------------------------------------------ - -resource "aws_s3_bucket" "lambda_deployment" { - bucket = "${local.ra_resource_name}-lambda-deployment" - - tags = { - Name = "${local.ra_resource_name}-lambda-deployment" - "sysdig.com/response-actions/cloud-actions" = "true" - } -} - -resource "aws_s3_bucket_versioning" "lambda_deployment" { - bucket = aws_s3_bucket.lambda_deployment.id - versioning_configuration { - status = "Enabled" - } -} - -# Download Lambda ZIP files -data "http" "quarantine_user_zip" { - url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/quarantine_user.zip" -} - -data "http" "fetch_cloud_logs_zip" { - url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/fetch_cloud_logs.zip" -} - -data "http" "remove_policy_zip" { - url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/remove_policy.zip" -} - -data "http" "configure_resource_access_zip" { - url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/configure_resource_access.zip" -} - -data "http" "create_volume_snapshot_zip" { - url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/create_volume_snapshot.zip" -} - -data "http" "delete_volume_snapshot_zip" { - url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/delete_volume_snapshot.zip" -} - -# Upload Lambda ZIP files to S3 -resource "aws_s3_object" "quarantine_user_zip" { - bucket = aws_s3_bucket.lambda_deployment.id - key = "quarantine_user.zip" - content_base64 = data.http.quarantine_user_zip.response_body_base64 - content_type = "application/zip" -} - -resource "aws_s3_object" "fetch_cloud_logs_zip" { - bucket = aws_s3_bucket.lambda_deployment.id - key = "fetch_cloud_logs.zip" - content_base64 = data.http.fetch_cloud_logs_zip.response_body_base64 - content_type = "application/zip" -} - -resource "aws_s3_object" "remove_policy_zip" { - bucket = aws_s3_bucket.lambda_deployment.id - key = "remove_policy.zip" - content_base64 = data.http.remove_policy_zip.response_body_base64 - content_type = "application/zip" -} - -resource "aws_s3_object" "configure_resource_access_zip" { - bucket = aws_s3_bucket.lambda_deployment.id - key = "configure_resource_access.zip" - content_base64 = data.http.configure_resource_access_zip.response_body_base64 - content_type = "application/zip" -} - -resource "aws_s3_object" "create_volume_snapshot_zip" { - bucket = aws_s3_bucket.lambda_deployment.id - key = "create_volume_snapshot.zip" - content_base64 = data.http.create_volume_snapshot_zip.response_body_base64 - content_type = "application/zip" -} - -resource "aws_s3_object" "delete_volume_snapshot_zip" { - bucket = aws_s3_bucket.lambda_deployment.id - key = "delete_volume_snapshot.zip" - content_base64 = data.http.delete_volume_snapshot_zip.response_body_base64 - content_type = "application/zip" -} +# NOTE: Removed local S3 bucket creation as Lambda functions now fetch from +# regional S3 buckets in a separate account. The bucket naming follows the +# pattern: {s3_bucket_prefix}-{region} +# +# Each regional bucket should contain the following Lambda zip files: +# - quarantine_user.zip +# - fetch_cloud_logs.zip +# - remove_policy.zip +# - configure_resource_access.zip +# - create_volume_snapshot.zip +# - delete_volume_snapshot.zip #------------------------------------------------------ # CloudFormation StackSet for Multi-Region Lambda Deployment @@ -517,7 +444,7 @@ resource "aws_cloudformation_stack_set" "lambda_functions" { parameters = { ResourceName = local.ra_resource_name - S3Bucket = aws_s3_bucket.lambda_deployment.id + S3BucketPrefix = var.s3_bucket_prefix ApiBaseUrl = var.api_base_url QuarantineUserRoleArn = aws_iam_role.quarantine_user_role.arn FetchCloudLogsRoleArn = aws_iam_role.fetch_cloud_logs_role.arn @@ -537,13 +464,7 @@ resource "aws_cloudformation_stack_set" "lambda_functions" { depends_on = [ aws_iam_role.lambda_stackset_admin_role, - aws_iam_role.lambda_stackset_execution_role, - aws_s3_object.quarantine_user_zip, - aws_s3_object.fetch_cloud_logs_zip, - aws_s3_object.remove_policy_zip, - aws_s3_object.configure_resource_access_zip, - aws_s3_object.create_volume_snapshot_zip, - aws_s3_object.delete_volume_snapshot_zip + aws_iam_role.lambda_stackset_execution_role ] } diff --git a/modules/response-actions/templates/lambda-stackset.yaml b/modules/response-actions/templates/lambda-stackset.yaml index 0d9dbb3..be85875 100644 --- a/modules/response-actions/templates/lambda-stackset.yaml +++ b/modules/response-actions/templates/lambda-stackset.yaml @@ -5,9 +5,9 @@ Parameters: ResourceName: Type: String Description: Base name for all resources - S3Bucket: + S3BucketPrefix: Type: String - Description: S3 bucket containing Lambda deployment packages + Description: Prefix for regional S3 buckets containing Lambda deployment packages (bucket name will be {prefix}-{region}) ApiBaseUrl: Type: String Description: API base URL for Lambda functions @@ -96,7 +96,7 @@ Resources: Handler: app.index.handler Role: !Ref QuarantineUserRoleArn Code: - S3Bucket: !Ref S3Bucket + S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' S3Key: quarantine_user.zip Timeout: 300 MemorySize: 128 @@ -119,7 +119,7 @@ Resources: Handler: app.index.handler Role: !Ref FetchCloudLogsRoleArn Code: - S3Bucket: !Ref S3Bucket + S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' S3Key: fetch_cloud_logs.zip Timeout: 300 MemorySize: 128 @@ -142,7 +142,7 @@ Resources: Handler: app.index.handler Role: !Ref RemovePolicyRoleArn Code: - S3Bucket: !Ref S3Bucket + S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' S3Key: remove_policy.zip Timeout: 300 MemorySize: 128 @@ -165,7 +165,7 @@ Resources: Handler: app.index.handler Role: !Ref ConfigureResourceAccessRoleArn Code: - S3Bucket: !Ref S3Bucket + S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' S3Key: configure_resource_access.zip Timeout: 300 MemorySize: 128 @@ -188,7 +188,7 @@ Resources: Handler: app.index.handler Role: !Ref CreateVolumeSnapshotsRoleArn Code: - S3Bucket: !Ref S3Bucket + S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' S3Key: create_volume_snapshot.zip Timeout: 300 MemorySize: 128 @@ -211,7 +211,7 @@ Resources: Handler: app.index.handler Role: !Ref DeleteVolumeSnapshotsRoleArn Code: - S3Bucket: !Ref S3Bucket + S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' S3Key: delete_volume_snapshot.zip Timeout: 300 MemorySize: 128 diff --git a/modules/response-actions/variables.tf b/modules/response-actions/variables.tf index 3988b4b..18dc24d 100644 --- a/modules/response-actions/variables.tf +++ b/modules/response-actions/variables.tf @@ -114,4 +114,9 @@ variable "response_actions_version" { description = "Response Actions version" type = string default = "0.0.15" +} + +variable "s3_bucket_prefix" { + description = "Prefix for regional S3 buckets containing Lambda deployment packages. The bucket name will be constructed as {prefix}-{region}. These buckets should exist in each target region and contain the Lambda zip files." + type = string } \ No newline at end of file From a4377d2339eb4f4316f72728726269c7bfd4986a Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Thu, 27 Nov 2025 12:00:47 +0100 Subject: [PATCH 06/26] WORKING --- modules/response-actions/README.md | 30 +++++++++++++++++++ modules/response-actions/main.tf | 13 +++++--- modules/response-actions/outputs.tf | 5 ---- .../templates/lambda-stackset.yaml | 16 +++++----- modules/response-actions/variables.tf | 10 +++++-- 5 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 modules/response-actions/README.md diff --git a/modules/response-actions/README.md b/modules/response-actions/README.md new file mode 100644 index 0000000..208f17c --- /dev/null +++ b/modules/response-actions/README.md @@ -0,0 +1,30 @@ +Add the following bucket policy in Sysdig S3 storage: +``` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "PublicReadGetObject", + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::/response-actions/cloud-lambdas/*" + } + ] +} +``` + +and disable block public access. + +For testing, create `provisioning.tf`, with: + +``` +provider "sysdig" { + sysdig_secure_url = "" + sysdig_secure_api_token = "" +} + +provider "aws" { + region = "us-east-1" +} +``` diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index 2b0bea5..00a55f0 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -21,6 +21,8 @@ locals { # StackSet role configuration administration_role_arn = var.auto_create_stackset_roles ? aws_iam_role.lambda_stackset_admin_role[0].arn : var.stackset_admin_role_arn execution_role_name = var.auto_create_stackset_roles ? aws_iam_role.lambda_stackset_execution_role[0].name : var.stackset_execution_role_name + + cloud_lambdas_path = "${var.cloud_lambdas_path}/${var.response_actions_version}" } #------------------------------------------------------ @@ -116,7 +118,9 @@ resource "aws_iam_role_policy" "lambda_stackset_execution_policy" { "logs:DeleteLogGroup", "logs:PutRetentionPolicy", "logs:TagResource", - "logs:UntagResource" + "logs:UntagResource", + "s3:GetObject", + "s3:ListBucket" ] Resource = "*" } @@ -307,7 +311,7 @@ resource "aws_iam_role_policy" "remove_policy_policy" { # Lambda Execution Role: Configure Resource Access resource "aws_iam_role" "configure_resource_access_role" { - name = "${local.ra_resource_name}-configure-resource-access-role" + name = "${local.ra_resource_name}-confi-res-access-role" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -341,7 +345,7 @@ resource "aws_iam_role_policy" "configure_resource_access_policy" { # Lambda Execution Role: Create Volume Snapshots resource "aws_iam_role" "create_volume_snapshots_role" { - name = "${local.ra_resource_name}-create-volume-snapshots-role" + name = "${local.ra_resource_name}-create-vol-snap-role" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -375,7 +379,7 @@ resource "aws_iam_role_policy" "create_volume_snapshots_policy" { # Lambda Execution Role: Delete Volume Snapshots resource "aws_iam_role" "delete_volume_snapshots_role" { - name = "${local.ra_resource_name}-delete-volume-snapshots-role" + name = "${local.ra_resource_name}-delete-vol-snap-role" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -458,6 +462,7 @@ resource "aws_cloudformation_stack_set" "lambda_functions" { ConfigureResourceAccessRoleName = aws_iam_role.configure_resource_access_role.name CreateVolumeSnapshotsRoleName = aws_iam_role.create_volume_snapshots_role.name DeleteVolumeSnapshotsRoleName = aws_iam_role.delete_volume_snapshots_role.name + CloudLambdasPath = local.cloud_lambdas_path } template_body = file("${path.module}/templates/lambda-stackset.yaml") diff --git a/modules/response-actions/outputs.tf b/modules/response-actions/outputs.tf index fadb86c..b5a5220 100644 --- a/modules/response-actions/outputs.tf +++ b/modules/response-actions/outputs.tf @@ -18,11 +18,6 @@ output "deployment_regions" { value = local.region_set } -output "s3_deployment_bucket" { - description = "S3 bucket containing Lambda deployment packages" - value = aws_s3_bucket.lambda_deployment.id -} - output "lambda_functions" { description = "Information about deployed Lambda functions across all regions" value = { diff --git a/modules/response-actions/templates/lambda-stackset.yaml b/modules/response-actions/templates/lambda-stackset.yaml index be85875..51e2566 100644 --- a/modules/response-actions/templates/lambda-stackset.yaml +++ b/modules/response-actions/templates/lambda-stackset.yaml @@ -47,7 +47,9 @@ Parameters: DeleteVolumeSnapshotsRoleName: Type: String Description: Name of the IAM role for delete volume snapshots function - + CloudLambdasPath: + Type: String + Description: The path where lambda code resides inside of regional S3 buckets Resources: # CloudWatch Log Groups (Regional Resources) QuarantineUserLogGroup: @@ -97,7 +99,7 @@ Resources: Role: !Ref QuarantineUserRoleArn Code: S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' - S3Key: quarantine_user.zip + S3Key: !Sub '${CloudLambdasPath}/quarantine_user.zip' Timeout: 300 MemorySize: 128 Environment: @@ -120,7 +122,7 @@ Resources: Role: !Ref FetchCloudLogsRoleArn Code: S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' - S3Key: fetch_cloud_logs.zip + S3Key: !Sub '${CloudLambdasPath}/fetch_cloud_logs.zip' Timeout: 300 MemorySize: 128 Environment: @@ -143,7 +145,7 @@ Resources: Role: !Ref RemovePolicyRoleArn Code: S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' - S3Key: remove_policy.zip + S3Key: !Sub '${CloudLambdasPath}/remove_policy.zip' Timeout: 300 MemorySize: 128 Environment: @@ -166,7 +168,7 @@ Resources: Role: !Ref ConfigureResourceAccessRoleArn Code: S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' - S3Key: configure_resource_access.zip + S3Key: !Sub '${CloudLambdasPath}/configure_resource_access.zip' Timeout: 300 MemorySize: 128 Environment: @@ -189,7 +191,7 @@ Resources: Role: !Ref CreateVolumeSnapshotsRoleArn Code: S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' - S3Key: create_volume_snapshot.zip + S3Key: !Sub '${CloudLambdasPath}/create_volume_snapshots.zip' Timeout: 300 MemorySize: 128 Environment: @@ -212,7 +214,7 @@ Resources: Role: !Ref DeleteVolumeSnapshotsRoleArn Code: S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' - S3Key: delete_volume_snapshot.zip + S3Key: !Sub '${CloudLambdasPath}/delete_volume_snapshots.zip' Timeout: 300 MemorySize: 128 Environment: diff --git a/modules/response-actions/variables.tf b/modules/response-actions/variables.tf index 18dc24d..b25bd37 100644 --- a/modules/response-actions/variables.tf +++ b/modules/response-actions/variables.tf @@ -23,7 +23,7 @@ variable "regions" { variable "name" { description = "(Optional) Name to be assigned to all child resources. A suffix may be added internally when required. Use default value unless you need to install multiple instances" type = string - default = "sysdig-secure-cloud-responder" + default = "sysdig-secure-ra" } variable "tags" { @@ -119,4 +119,10 @@ variable "response_actions_version" { variable "s3_bucket_prefix" { description = "Prefix for regional S3 buckets containing Lambda deployment packages. The bucket name will be constructed as {prefix}-{region}. These buckets should exist in each target region and contain the Lambda zip files." type = string -} \ No newline at end of file +} + +variable "cloud_lambdas_path" { + description = "The path where lambda code resides inside of regional S3 buckets" + type = string + default = "response-actions/cloud-lambdas" +} From 34e95505e707c7acefeda2b9863b6fee3e1c093a Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Thu, 27 Nov 2025 15:01:56 +0100 Subject: [PATCH 07/26] Added the possibility to disable some actions. --- modules/response-actions/main.tf | 140 +++++++++++------- modules/response-actions/outputs.tf | 60 ++++---- .../templates/lambda-stackset.yaml | 45 ++++++ modules/response-actions/variables.tf | 20 +++ 4 files changed, 186 insertions(+), 79 deletions(-) diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index 00a55f0..dcd2246 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -23,6 +23,30 @@ locals { execution_role_name = var.auto_create_stackset_roles ? aws_iam_role.lambda_stackset_execution_role[0].name : var.stackset_execution_role_name cloud_lambdas_path = "${var.cloud_lambdas_path}/${var.response_actions_version}" + + # Response action enablement flags + enable_make_private = contains(var.enabled_response_actions, "make_private") + enable_fetch_cloud_logs = contains(var.enabled_response_actions, "fetch_cloud_logs") + enable_create_volume_snapshot = contains(var.enabled_response_actions, "create_volume_snapshot") + enable_quarantine_user = contains(var.enabled_response_actions, "quarantine_user") + + # Build list of Lambda ARNs for invoke policy based on enabled actions + enabled_lambda_arns = concat( + local.enable_quarantine_user ? [ + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-quarantine-user", + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-remove-policy" + ] : [], + local.enable_fetch_cloud_logs ? [ + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-fetch-cloud-logs" + ] : [], + local.enable_make_private ? [ + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-configure-resource-access" + ] : [], + local.enable_create_volume_snapshot ? [ + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-create-volume-snapshots", + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-delete-volume-snapshots" + ] : [] + ) } #------------------------------------------------------ @@ -170,28 +194,7 @@ resource "aws_iam_role_policy" "shared_lambda_invoke_policy" { policy = jsonencode({ Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "lambda:InvokeFunction", - "lambda:GetFunction" - ] - Resource = [ - # Quarantine User functions in all regions - "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-quarantine-user", - # Fetch Cloud Logs functions in all regions - "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-fetch-cloud-logs", - # Remove Policy functions in all regions - "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-remove-policy", - # Configure Resource Access functions in all regions - "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-configure-resource-access", - # Create Volume Snapshots functions in all regions - "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-create-volume-snapshots", - # Delete Volume Snapshots functions in all regions - "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-delete-volume-snapshots" - ] - }, + Statement = concat([ { Effect = "Allow" Action = [ @@ -199,7 +202,16 @@ resource "aws_iam_role_policy" "shared_lambda_invoke_policy" { ] Resource = "*" } - ] + ], + length(local.enabled_lambda_arns) > 0 ? [{ + Effect = "Allow" + Action = [ + "lambda:InvokeFunction", + "lambda:GetFunction" + ] + Resource = local.enabled_lambda_arns + }] : [] + ) }) } @@ -209,7 +221,8 @@ resource "aws_iam_role_policy" "shared_lambda_invoke_policy" { # Lambda Execution Role: Quarantine User resource "aws_iam_role" "quarantine_user_role" { - name = "${local.ra_resource_name}-quarantine-user-role" + count = local.enable_quarantine_user ? 1 : 0 + name = "${local.ra_resource_name}-quarantine-user-role" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -231,19 +244,22 @@ resource "aws_iam_role" "quarantine_user_role" { } resource "aws_iam_role_policy_attachment" "quarantine_user_basic" { + count = local.enable_quarantine_user ? 1 : 0 policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - role = aws_iam_role.quarantine_user_role.name + role = aws_iam_role.quarantine_user_role[0].name } resource "aws_iam_role_policy" "quarantine_user_policy" { + count = local.enable_quarantine_user ? 1 : 0 name = "${local.ra_resource_name}-quarantine-user-policy" - role = aws_iam_role.quarantine_user_role.id + role = aws_iam_role.quarantine_user_role[0].id policy = local.quarantine_user_policy } # Lambda Execution Role: Fetch Cloud Logs resource "aws_iam_role" "fetch_cloud_logs_role" { - name = "${local.ra_resource_name}-fetch-cloud-logs-role" + count = local.enable_fetch_cloud_logs ? 1 : 0 + name = "${local.ra_resource_name}-fetch-cloud-logs-role" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -265,19 +281,22 @@ resource "aws_iam_role" "fetch_cloud_logs_role" { } resource "aws_iam_role_policy_attachment" "fetch_cloud_logs_basic" { + count = local.enable_fetch_cloud_logs ? 1 : 0 policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - role = aws_iam_role.fetch_cloud_logs_role.name + role = aws_iam_role.fetch_cloud_logs_role[0].name } resource "aws_iam_role_policy" "fetch_cloud_logs_policy" { + count = local.enable_fetch_cloud_logs ? 1 : 0 name = "${local.ra_resource_name}-fetch-cloud-logs-policy" - role = aws_iam_role.fetch_cloud_logs_role.id + role = aws_iam_role.fetch_cloud_logs_role[0].id policy = local.fetch_cloud_logs_policy } # Lambda Execution Role: Remove Policy resource "aws_iam_role" "remove_policy_role" { - name = "${local.ra_resource_name}-remove-policy-role" + count = local.enable_quarantine_user ? 1 : 0 + name = "${local.ra_resource_name}-remove-policy-role" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -299,19 +318,22 @@ resource "aws_iam_role" "remove_policy_role" { } resource "aws_iam_role_policy_attachment" "remove_policy_basic" { + count = local.enable_quarantine_user ? 1 : 0 policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - role = aws_iam_role.remove_policy_role.name + role = aws_iam_role.remove_policy_role[0].name } resource "aws_iam_role_policy" "remove_policy_policy" { + count = local.enable_quarantine_user ? 1 : 0 name = "${local.ra_resource_name}-remove-policy-policy" - role = aws_iam_role.remove_policy_role.id + role = aws_iam_role.remove_policy_role[0].id policy = local.remove_policy_policy } # Lambda Execution Role: Configure Resource Access resource "aws_iam_role" "configure_resource_access_role" { - name = "${local.ra_resource_name}-confi-res-access-role" + count = local.enable_make_private ? 1 : 0 + name = "${local.ra_resource_name}-confi-res-access-role" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -333,19 +355,22 @@ resource "aws_iam_role" "configure_resource_access_role" { } resource "aws_iam_role_policy_attachment" "configure_resource_access_basic" { + count = local.enable_make_private ? 1 : 0 policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - role = aws_iam_role.configure_resource_access_role.name + role = aws_iam_role.configure_resource_access_role[0].name } resource "aws_iam_role_policy" "configure_resource_access_policy" { + count = local.enable_make_private ? 1 : 0 name = "${local.ra_resource_name}-configure-resource-access-policy" - role = aws_iam_role.configure_resource_access_role.id + role = aws_iam_role.configure_resource_access_role[0].id policy = local.configure_resource_access_policy } # Lambda Execution Role: Create Volume Snapshots resource "aws_iam_role" "create_volume_snapshots_role" { - name = "${local.ra_resource_name}-create-vol-snap-role" + count = local.enable_create_volume_snapshot ? 1 : 0 + name = "${local.ra_resource_name}-create-vol-snap-role" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -367,19 +392,22 @@ resource "aws_iam_role" "create_volume_snapshots_role" { } resource "aws_iam_role_policy_attachment" "create_volume_snapshots_basic" { + count = local.enable_create_volume_snapshot ? 1 : 0 policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - role = aws_iam_role.create_volume_snapshots_role.name + role = aws_iam_role.create_volume_snapshots_role[0].name } resource "aws_iam_role_policy" "create_volume_snapshots_policy" { + count = local.enable_create_volume_snapshot ? 1 : 0 name = "${local.ra_resource_name}-create-volume-snapshots-policy" - role = aws_iam_role.create_volume_snapshots_role.id + role = aws_iam_role.create_volume_snapshots_role[0].id policy = local.create_volume_snapshots_policy } # Lambda Execution Role: Delete Volume Snapshots resource "aws_iam_role" "delete_volume_snapshots_role" { - name = "${local.ra_resource_name}-delete-vol-snap-role" + count = local.enable_create_volume_snapshot ? 1 : 0 + name = "${local.ra_resource_name}-delete-vol-snap-role" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -401,13 +429,15 @@ resource "aws_iam_role" "delete_volume_snapshots_role" { } resource "aws_iam_role_policy_attachment" "delete_volume_snapshots_basic" { + count = local.enable_create_volume_snapshot ? 1 : 0 policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - role = aws_iam_role.delete_volume_snapshots_role.name + role = aws_iam_role.delete_volume_snapshots_role[0].name } resource "aws_iam_role_policy" "delete_volume_snapshots_policy" { + count = local.enable_create_volume_snapshot ? 1 : 0 name = "${local.ra_resource_name}-delete-volume-snapshots-policy" - role = aws_iam_role.delete_volume_snapshots_role.id + role = aws_iam_role.delete_volume_snapshots_role[0].id policy = local.delete_volume_snapshots_policy } @@ -450,19 +480,23 @@ resource "aws_cloudformation_stack_set" "lambda_functions" { ResourceName = local.ra_resource_name S3BucketPrefix = var.s3_bucket_prefix ApiBaseUrl = var.api_base_url - QuarantineUserRoleArn = aws_iam_role.quarantine_user_role.arn - FetchCloudLogsRoleArn = aws_iam_role.fetch_cloud_logs_role.arn - RemovePolicyRoleArn = aws_iam_role.remove_policy_role.arn - ConfigureResourceAccessRoleArn = aws_iam_role.configure_resource_access_role.arn - CreateVolumeSnapshotsRoleArn = aws_iam_role.create_volume_snapshots_role.arn - DeleteVolumeSnapshotsRoleArn = aws_iam_role.delete_volume_snapshots_role.arn - QuarantineUserRoleName = aws_iam_role.quarantine_user_role.name - FetchCloudLogsRoleName = aws_iam_role.fetch_cloud_logs_role.name - RemovePolicyRoleName = aws_iam_role.remove_policy_role.name - ConfigureResourceAccessRoleName = aws_iam_role.configure_resource_access_role.name - CreateVolumeSnapshotsRoleName = aws_iam_role.create_volume_snapshots_role.name - DeleteVolumeSnapshotsRoleName = aws_iam_role.delete_volume_snapshots_role.name + QuarantineUserRoleArn = local.enable_quarantine_user ? aws_iam_role.quarantine_user_role[0].arn : "" + FetchCloudLogsRoleArn = local.enable_fetch_cloud_logs ? aws_iam_role.fetch_cloud_logs_role[0].arn : "" + RemovePolicyRoleArn = local.enable_quarantine_user ? aws_iam_role.remove_policy_role[0].arn : "" + ConfigureResourceAccessRoleArn = local.enable_make_private ? aws_iam_role.configure_resource_access_role[0].arn : "" + CreateVolumeSnapshotsRoleArn = local.enable_create_volume_snapshot ? aws_iam_role.create_volume_snapshots_role[0].arn : "" + DeleteVolumeSnapshotsRoleArn = local.enable_create_volume_snapshot ? aws_iam_role.delete_volume_snapshots_role[0].arn : "" + QuarantineUserRoleName = local.enable_quarantine_user ? aws_iam_role.quarantine_user_role[0].name : "" + FetchCloudLogsRoleName = local.enable_fetch_cloud_logs ? aws_iam_role.fetch_cloud_logs_role[0].name : "" + RemovePolicyRoleName = local.enable_quarantine_user ? aws_iam_role.remove_policy_role[0].name : "" + ConfigureResourceAccessRoleName = local.enable_make_private ? aws_iam_role.configure_resource_access_role[0].name : "" + CreateVolumeSnapshotsRoleName = local.enable_create_volume_snapshot ? aws_iam_role.create_volume_snapshots_role[0].name : "" + DeleteVolumeSnapshotsRoleName = local.enable_create_volume_snapshot ? aws_iam_role.delete_volume_snapshots_role[0].name : "" CloudLambdasPath = local.cloud_lambdas_path + EnableQuarantineUser = local.enable_quarantine_user ? "true" : "false" + EnableFetchCloudLogs = local.enable_fetch_cloud_logs ? "true" : "false" + EnableMakePrivate = local.enable_make_private ? "true" : "false" + EnableCreateVolumeSnapshot = local.enable_create_volume_snapshot ? "true" : "false" } template_body = file("${path.module}/templates/lambda-stackset.yaml") diff --git a/modules/response-actions/outputs.tf b/modules/response-actions/outputs.tf index b5a5220..bb7a037 100644 --- a/modules/response-actions/outputs.tf +++ b/modules/response-actions/outputs.tf @@ -20,30 +20,38 @@ output "deployment_regions" { output "lambda_functions" { description = "Information about deployed Lambda functions across all regions" - value = { - quarantine_user = { - name = "${local.ra_resource_name}-quarantine-user" - arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-quarantine-user"] - } - fetch_cloud_logs = { - name = "${local.ra_resource_name}-fetch-cloud-logs" - arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-fetch-cloud-logs"] - } - remove_policy = { - name = "${local.ra_resource_name}-remove-policy" - arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-remove-policy"] - } - configure_resource_access = { - name = "${local.ra_resource_name}-configure-resource-access" - arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-configure-resource-access"] - } - create_volume_snapshots = { - name = "${local.ra_resource_name}-create-volume-snapshots" - arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-create-volume-snapshots"] - } - delete_volume_snapshots = { - name = "${local.ra_resource_name}-delete-volume-snapshots" - arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-delete-volume-snapshots"] - } - } + value = merge( + local.enable_quarantine_user ? { + quarantine_user = { + name = "${local.ra_resource_name}-quarantine-user" + arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-quarantine-user"] + } + remove_policy = { + name = "${local.ra_resource_name}-remove-policy" + arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-remove-policy"] + } + } : {}, + local.enable_fetch_cloud_logs ? { + fetch_cloud_logs = { + name = "${local.ra_resource_name}-fetch-cloud-logs" + arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-fetch-cloud-logs"] + } + } : {}, + local.enable_make_private ? { + configure_resource_access = { + name = "${local.ra_resource_name}-configure-resource-access" + arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-configure-resource-access"] + } + } : {}, + local.enable_create_volume_snapshot ? { + create_volume_snapshots = { + name = "${local.ra_resource_name}-create-volume-snapshots" + arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-create-volume-snapshots"] + } + delete_volume_snapshots = { + name = "${local.ra_resource_name}-delete-volume-snapshots" + arns = [for region in local.region_set : "${local.arn_prefix}:lambda:${region}:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-delete-volume-snapshots"] + } + } : {} + ) } diff --git a/modules/response-actions/templates/lambda-stackset.yaml b/modules/response-actions/templates/lambda-stackset.yaml index 51e2566..2e58c2b 100644 --- a/modules/response-actions/templates/lambda-stackset.yaml +++ b/modules/response-actions/templates/lambda-stackset.yaml @@ -50,40 +50,73 @@ Parameters: CloudLambdasPath: Type: String Description: The path where lambda code resides inside of regional S3 buckets + EnableQuarantineUser: + Type: String + Default: "true" + AllowedValues: ["true", "false"] + Description: Enable quarantine user and remove policy lambdas + EnableFetchCloudLogs: + Type: String + Default: "true" + AllowedValues: ["true", "false"] + Description: Enable fetch cloud logs lambda + EnableMakePrivate: + Type: String + Default: "true" + AllowedValues: ["true", "false"] + Description: Enable configure resource access lambda + EnableCreateVolumeSnapshot: + Type: String + Default: "true" + AllowedValues: ["true", "false"] + Description: Enable create and delete volume snapshot lambdas + +Conditions: + CreateQuarantineUserResources: !Equals [!Ref EnableQuarantineUser, "true"] + CreateFetchCloudLogsResources: !Equals [!Ref EnableFetchCloudLogs, "true"] + CreateMakePrivateResources: !Equals [!Ref EnableMakePrivate, "true"] + CreateVolumeSnapshotResources: !Equals [!Ref EnableCreateVolumeSnapshot, "true"] + Resources: # CloudWatch Log Groups (Regional Resources) QuarantineUserLogGroup: Type: AWS::Logs::LogGroup + Condition: CreateQuarantineUserResources Properties: LogGroupName: !Sub '/aws/lambda/${ResourceName}-quarantine-user' RetentionInDays: 7 FetchCloudLogsLogGroup: Type: AWS::Logs::LogGroup + Condition: CreateFetchCloudLogsResources Properties: LogGroupName: !Sub '/aws/lambda/${ResourceName}-fetch-cloud-logs' RetentionInDays: 7 RemovePolicyLogGroup: Type: AWS::Logs::LogGroup + Condition: CreateQuarantineUserResources Properties: LogGroupName: !Sub '/aws/lambda/${ResourceName}-remove-policy' RetentionInDays: 7 ConfigureResourceAccessLogGroup: Type: AWS::Logs::LogGroup + Condition: CreateMakePrivateResources Properties: LogGroupName: !Sub '/aws/lambda/${ResourceName}-configure-resource-access' RetentionInDays: 7 CreateVolumeSnapshotsLogGroup: Type: AWS::Logs::LogGroup + Condition: CreateVolumeSnapshotResources Properties: LogGroupName: !Sub '/aws/lambda/${ResourceName}-create-volume-snapshots' RetentionInDays: 7 DeleteVolumeSnapshotsLogGroup: Type: AWS::Logs::LogGroup + Condition: CreateVolumeSnapshotResources Properties: LogGroupName: !Sub '/aws/lambda/${ResourceName}-delete-volume-snapshots' RetentionInDays: 7 @@ -91,6 +124,7 @@ Resources: # Lambda Functions (Regional Resources) QuarantineUserFunction: Type: AWS::Lambda::Function + Condition: CreateQuarantineUserResources DependsOn: QuarantineUserLogGroup Properties: FunctionName: !Sub '${ResourceName}-quarantine-user' @@ -114,6 +148,7 @@ Resources: FetchCloudLogsFunction: Type: AWS::Lambda::Function + Condition: CreateFetchCloudLogsResources DependsOn: FetchCloudLogsLogGroup Properties: FunctionName: !Sub '${ResourceName}-fetch-cloud-logs' @@ -137,6 +172,7 @@ Resources: RemovePolicyFunction: Type: AWS::Lambda::Function + Condition: CreateQuarantineUserResources DependsOn: RemovePolicyLogGroup Properties: FunctionName: !Sub '${ResourceName}-remove-policy' @@ -160,6 +196,7 @@ Resources: ConfigureResourceAccessFunction: Type: AWS::Lambda::Function + Condition: CreateMakePrivateResources DependsOn: ConfigureResourceAccessLogGroup Properties: FunctionName: !Sub '${ResourceName}-configure-resource-access' @@ -183,6 +220,7 @@ Resources: CreateVolumeSnapshotsFunction: Type: AWS::Lambda::Function + Condition: CreateVolumeSnapshotResources DependsOn: CreateVolumeSnapshotsLogGroup Properties: FunctionName: !Sub '${ResourceName}-create-volume-snapshots' @@ -206,6 +244,7 @@ Resources: DeleteVolumeSnapshotsFunction: Type: AWS::Lambda::Function + Condition: CreateVolumeSnapshotResources DependsOn: DeleteVolumeSnapshotsLogGroup Properties: FunctionName: !Sub '${ResourceName}-delete-volume-snapshots' @@ -229,14 +268,20 @@ Resources: Outputs: QuarantineUserFunctionArn: + Condition: CreateQuarantineUserResources Value: !GetAtt QuarantineUserFunction.Arn FetchCloudLogsFunctionArn: + Condition: CreateFetchCloudLogsResources Value: !GetAtt FetchCloudLogsFunction.Arn RemovePolicyFunctionArn: + Condition: CreateQuarantineUserResources Value: !GetAtt RemovePolicyFunction.Arn ConfigureResourceAccessFunctionArn: + Condition: CreateMakePrivateResources Value: !GetAtt ConfigureResourceAccessFunction.Arn CreateVolumeSnapshotsFunctionArn: + Condition: CreateVolumeSnapshotResources Value: !GetAtt CreateVolumeSnapshotsFunction.Arn DeleteVolumeSnapshotsFunctionArn: + Condition: CreateVolumeSnapshotResources Value: !GetAtt DeleteVolumeSnapshotsFunction.Arn diff --git a/modules/response-actions/variables.tf b/modules/response-actions/variables.tf index b25bd37..88faab6 100644 --- a/modules/response-actions/variables.tf +++ b/modules/response-actions/variables.tf @@ -126,3 +126,23 @@ variable "cloud_lambdas_path" { type = string default = "response-actions/cloud-lambdas" } + +variable "enabled_response_actions" { + description = <<-EOF + (Optional) List of response actions to enable. Valid values are: + - "make_private": Creates the configure resource access lambda + - "fetch_cloud_logs": Creates the fetch cloud logs lambda + - "create_volume_snapshot": Creates the create and delete volume snapshot lambdas + - "quarantine_user": Creates the quarantine user and remove policy lambdas + By default, all response actions are enabled. + EOF + type = list(string) + default = ["make_private", "fetch_cloud_logs", "create_volume_snapshot", "quarantine_user"] + validation { + condition = alltrue([ + for action in var.enabled_response_actions : + contains(["make_private", "fetch_cloud_logs", "create_volume_snapshot", "quarantine_user"], action) + ]) + error_message = "Valid values for enabled_response_actions are: make_private, fetch_cloud_logs, create_volume_snapshot, quarantine_user" + } +} From 831a65fb265528711333a4b09167a9bec4e3b695 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Thu, 27 Nov 2025 15:48:45 +0100 Subject: [PATCH 08/26] Preliminary support for subaccounts --- modules/response-actions/organizational.tf | 79 +++++ .../templates/delegate_roles_stackset.tpl | 331 ++++++++++++++++++ 2 files changed, 410 insertions(+) create mode 100644 modules/response-actions/organizational.tf create mode 100644 modules/response-actions/templates/delegate_roles_stackset.tpl diff --git a/modules/response-actions/organizational.tf b/modules/response-actions/organizational.tf new file mode 100644 index 0000000..09577ac --- /dev/null +++ b/modules/response-actions/organizational.tf @@ -0,0 +1,79 @@ +resource "aws_cloudformation_stack_set" "ra_delegate_roles" { + count = var.is_organizational ? 1 : 0 + + name = join("-", [local.ra_resource_name, "delegate-roles"]) + tags = var.tags + permission_model = "SERVICE_MANAGED" + capabilities = ["CAPABILITY_NAMED_IAM"] + + managed_execution { + active = true + } + + auto_deployment { + enabled = true + retain_stacks_on_account_removal = false + } + + lifecycle { + ignore_changes = [administration_role_arn] + } + + parameters = { + QuarantineUserLambdaRoleArn = local.enable_quarantine_user ? aws_iam_role.quarantine_user_role[0].arn : "" + QuarantineUserRoleName = local.enable_quarantine_user ? aws_iam_role.quarantine_user_role[0].name : "" + FetchCloudLogsLambdaRoleArn = local.enable_fetch_cloud_logs ? aws_iam_role.fetch_cloud_logs_role[0].arn : "" + FetchCloudLogsRoleName = local.enable_fetch_cloud_logs ? aws_iam_role.fetch_cloud_logs_role[0].name : "" + RemovePolicyLambdaRoleArn = local.enable_quarantine_user ? aws_iam_role.remove_policy_role[0].arn : "" + RemovePolicyRoleName = local.enable_quarantine_user ? aws_iam_role.remove_policy_role[0].name : "" + ConfigureResourceAccessLambdaRoleArn = local.enable_make_private ? aws_iam_role.configure_resource_access_role[0].arn : "" + ConfigureResourceAccessRoleName = local.enable_make_private ? aws_iam_role.configure_resource_access_role[0].name : "" + CreateVolumeSnapshotsLambdaRoleArn = local.enable_create_volume_snapshot ? aws_iam_role.create_volume_snapshots_role[0].arn : "" + CreateVolumeSnapshotsRoleName = local.enable_create_volume_snapshot ? aws_iam_role.create_volume_snapshots_role[0].name : "" + DeleteVolumeSnapshotsLambdaRoleArn = local.enable_create_volume_snapshot ? aws_iam_role.delete_volume_snapshots_role[0].arn : "" + DeleteVolumeSnapshotsRoleName = local.enable_create_volume_snapshot ? aws_iam_role.delete_volume_snapshots_role[0].name : "" + EnableQuarantineUser = local.enable_quarantine_user ? "true" : "false" + EnableFetchCloudLogs = local.enable_fetch_cloud_logs ? "true" : "false" + EnableMakePrivate = local.enable_make_private ? "true" : "false" + EnableCreateVolumeSnapshot = local.enable_create_volume_snapshot ? "true" : "false" + } + + template_body = file("${path.module}/templates/delegate_roles_stackset.tpl") + + depends_on = [ + aws_iam_role.quarantine_user_role, + aws_iam_role.fetch_cloud_logs_role, + aws_iam_role.remove_policy_role, + aws_iam_role.configure_resource_access_role, + aws_iam_role.create_volume_snapshots_role, + aws_iam_role.delete_volume_snapshots_role + ] +} + +resource "aws_cloudformation_stack_set_instance" "ra_delegate_roles" { + for_each = var.is_organizational ? { + for pair in setproduct(local.region_set, local.deployment_targets_org_units) : + "${pair[0]}-${pair[1]}" => pair + } : {} + + stack_set_instance_region = each.value[0] + stack_set_name = aws_cloudformation_stack_set.ra_delegate_roles[0].name + deployment_targets { + organizational_unit_ids = [each.value[1]] + accounts = local.check_old_ouid_param ? null : (local.deployment_targets_accounts_filter == "NONE" ? null : local.deployment_targets_accounts.accounts_to_deploy) + account_filter_type = local.check_old_ouid_param ? null : local.deployment_targets_accounts_filter + } + operation_preferences { + max_concurrent_percentage = 100 + failure_tolerance_percentage = var.failure_tolerance_percentage + concurrency_mode = "SOFT_FAILURE_TOLERANCE" + region_concurrency_type = "PARALLEL" + } + + timeouts { + create = var.timeout + update = var.timeout + delete = var.timeout + } +} + diff --git a/modules/response-actions/templates/delegate_roles_stackset.tpl b/modules/response-actions/templates/delegate_roles_stackset.tpl new file mode 100644 index 0000000..e4b649b --- /dev/null +++ b/modules/response-actions/templates/delegate_roles_stackset.tpl @@ -0,0 +1,331 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'Sysdig Response Actions Delegate Roles - Multi-Account Deployment' + +Parameters: + QuarantineUserLambdaRoleArn: + Type: String + Description: ARN of the Lambda execution role for quarantine user function + QuarantineUserRoleName: + Type: String + Description: Name for the quarantine user delegate role + FetchCloudLogsLambdaRoleArn: + Type: String + Description: ARN of the Lambda execution role for fetch cloud logs function + FetchCloudLogsRoleName: + Type: String + Description: Name for the fetch cloud logs delegate role + RemovePolicyLambdaRoleArn: + Type: String + Description: ARN of the Lambda execution role for remove policy function + RemovePolicyRoleName: + Type: String + Description: Name for the remove policy delegate role + ConfigureResourceAccessLambdaRoleArn: + Type: String + Description: ARN of the Lambda execution role for configure resource access function + ConfigureResourceAccessRoleName: + Type: String + Description: Name for the configure resource access delegate role + CreateVolumeSnapshotsLambdaRoleArn: + Type: String + Description: ARN of the Lambda execution role for create volume snapshots function + CreateVolumeSnapshotsRoleName: + Type: String + Description: Name for the create volume snapshots delegate role + DeleteVolumeSnapshotsLambdaRoleArn: + Type: String + Description: ARN of the Lambda execution role for delete volume snapshots function + DeleteVolumeSnapshotsRoleName: + Type: String + Description: Name for the delete volume snapshots delegate role + EnableQuarantineUser: + Type: String + Default: "true" + AllowedValues: ["true", "false"] + Description: Enable quarantine user and remove policy delegate roles + EnableFetchCloudLogs: + Type: String + Default: "true" + AllowedValues: ["true", "false"] + Description: Enable fetch cloud logs delegate role + EnableMakePrivate: + Type: String + Default: "true" + AllowedValues: ["true", "false"] + Description: Enable configure resource access delegate role + EnableCreateVolumeSnapshot: + Type: String + Default: "true" + AllowedValues: ["true", "false"] + Description: Enable create and delete volume snapshot delegate roles + +Conditions: + CreateQuarantineUserResources: !Equals [!Ref EnableQuarantineUser, "true"] + CreateFetchCloudLogsResources: !Equals [!Ref EnableFetchCloudLogs, "true"] + CreateMakePrivateResources: !Equals [!Ref EnableMakePrivate, "true"] + CreateVolumeSnapshotResources: !Equals [!Ref EnableCreateVolumeSnapshot, "true"] + +Resources: + # Delegate Role: Configure Resource Access + ConfigureAccessDelegateRole: + Type: AWS::IAM::Role + Condition: CreateMakePrivateResources + Properties: + RoleName: !Ref ConfigureResourceAccessRoleName + Description: Delegate role for configuring resource access + MaxSessionDuration: 3600 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + AWS: !Ref ConfigureResourceAccessLambdaRoleArn + Action: 'sts:AssumeRole' + Policies: + - PolicyName: response-actions-configure-resource-access + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - 's3:GetBucketLocation' + - 's3:GetBucketPublicAccessBlock' + - 's3:PutBucketPublicAccessBlock' + - 's3:ListAllMyBuckets' + Resource: '*' + - Effect: Allow + Action: + - 'rds:DescribeDBInstances' + - 'rds:ModifyDBInstance' + - 'sts:GetCallerIdentity' + Resource: '*' + Tags: + - Key: ManagedBy + Value: Terraform + - Key: Purpose + Value: ResponseActions + + # Delegate Role: Create Volume Snapshot + CreateVolumeSnapshotDelegateRole: + Type: AWS::IAM::Role + Condition: CreateVolumeSnapshotResources + Properties: + RoleName: !Ref CreateVolumeSnapshotsRoleName + Description: Delegate role for creating volume snapshots + MaxSessionDuration: 3600 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + AWS: !Ref CreateVolumeSnapshotsLambdaRoleArn + Action: 'sts:AssumeRole' + Policies: + - PolicyName: response-actions-create-volume-snapshot + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - 'logs:CreateLogGroup' + - 'logs:CreateLogStream' + - 'logs:PutLogEvents' + Resource: 'arn:aws:logs:*:*:*' + - Effect: Allow + Action: + - 'ec2:DescribeInstances' + - 'ec2:DescribeVolumes' + - 'ec2:DescribeSnapshots' + - 'ec2:CreateSnapshot' + - 'ec2:CreateTags' + - 'sts:GetCallerIdentity' + Resource: '*' + - Effect: Allow + Action: + - 'ec2:CreateTags' + Resource: + - 'arn:aws:ec2:*:*:snapshot/*' + Tags: + - Key: ManagedBy + Value: Terraform + - Key: Purpose + Value: ResponseActions + + # Delegate Role: Delete Volume Snapshot + DeleteVolumeSnapshotDelegateRole: + Type: AWS::IAM::Role + Condition: CreateVolumeSnapshotResources + Properties: + RoleName: !Ref DeleteVolumeSnapshotsRoleName + Description: Delegate role for deleting volume snapshots + MaxSessionDuration: 3600 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + AWS: !Ref DeleteVolumeSnapshotsLambdaRoleArn + Action: 'sts:AssumeRole' + Policies: + - PolicyName: response-actions-delete-volume-snapshots + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - 'logs:CreateLogGroup' + - 'logs:CreateLogStream' + - 'logs:PutLogEvents' + Resource: 'arn:aws:logs:*:*:*' + - Effect: Allow + Action: + - 'ec2:DescribeSnapshots' + - 'ec2:DeleteSnapshot' + - 'sts:GetCallerIdentity' + Resource: '*' + Tags: + - Key: ManagedBy + Value: Terraform + - Key: Purpose + Value: ResponseActions + + # Delegate Role: Fetch Cloud Logs + FetchCloudLogsDelegateRole: + Type: AWS::IAM::Role + Condition: CreateFetchCloudLogsResources + Properties: + RoleName: !Ref FetchCloudLogsRoleName + Description: Delegate role for fetching cloud logs + MaxSessionDuration: 3600 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + AWS: !Ref FetchCloudLogsLambdaRoleArn + Action: 'sts:AssumeRole' + Policies: + - PolicyName: response-actions-fetch-cloud-logs + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - 'cloudtrail:LookupEvents' + - 'sts:GetCallerIdentity' + Resource: '*' + Tags: + - Key: ManagedBy + Value: Terraform + - Key: Purpose + Value: ResponseActions + + # Delegate Role: Quarantine User + QuarantineUserRoleDelegateRole: + Type: AWS::IAM::Role + Condition: CreateQuarantineUserResources + Properties: + RoleName: !Ref QuarantineUserRoleName + Description: Delegate role for quarantining users + MaxSessionDuration: 3600 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + AWS: !Ref QuarantineUserLambdaRoleArn + Action: 'sts:AssumeRole' + Policies: + - PolicyName: response-actions-quarantine-user + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - 'iam:AttachUserPolicy' + - 'iam:DetachUserPolicy' + - 'iam:PutUserPolicy' + - 'iam:DeleteUserPolicy' + - 'iam:ListUserPolicies' + - 'iam:ListAttachedUserPolicies' + - 'iam:GetUser' + - 'iam:GetUserPolicy' + - 'iam:TagUser' + - 'iam:UntagUser' + - 'iam:ListUserTags' + - 'iam:AttachRolePolicy' + - 'iam:DetachRolePolicy' + - 'iam:GetRole' + - 'iam:ListRolePolicies' + - 'iam:ListAttachedRolePolicies' + - 'iam:GetPolicy' + - 'iam:CreatePolicy' + - 'sts:GetCallerIdentity' + Resource: '*' + Tags: + - Key: ManagedBy + Value: Terraform + - Key: Purpose + Value: ResponseActions + + # Delegate Role: Remove Policy + RemovePolicyDelegateRole: + Type: AWS::IAM::Role + Condition: CreateQuarantineUserResources + Properties: + RoleName: !Ref RemovePolicyRoleName + Description: Delegate role for removing policies + MaxSessionDuration: 3600 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + AWS: !Ref RemovePolicyLambdaRoleArn + Action: 'sts:AssumeRole' + Policies: + - PolicyName: response-actions-remove-policy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - 'iam:DetachUserPolicy' + - 'iam:DetachRolePolicy' + - 'iam:ListAttachedUserPolicies' + - 'iam:ListAttachedRolePolicies' + - 'iam:GetUser' + - 'iam:GetRole' + - 'sts:GetCallerIdentity' + Resource: '*' + Tags: + - Key: ManagedBy + Value: Terraform + - Key: Purpose + Value: ResponseActions + +Outputs: + ConfigureAccessDelegateRoleName: + Condition: CreateMakePrivateResources + Value: !Ref ConfigureAccessDelegateRole + Description: Name of the configure resource access delegate role + CreateVolumeSnapshotDelegateRoleName: + Condition: CreateVolumeSnapshotResources + Value: !Ref CreateVolumeSnapshotDelegateRole + Description: Name of the create volume snapshot delegate role + DeleteVolumeSnapshotDelegateRoleName: + Condition: CreateVolumeSnapshotResources + Value: !Ref DeleteVolumeSnapshotDelegateRole + Description: Name of the delete volume snapshot delegate role + FetchCloudLogsDelegateRoleName: + Condition: CreateFetchCloudLogsResources + Value: !Ref FetchCloudLogsDelegateRole + Description: Name of the fetch cloud logs delegate role + QuarantineUserRoleDelegateRoleName: + Condition: CreateQuarantineUserResources + Value: !Ref QuarantineUserRoleDelegateRole + Description: Name of the quarantine user delegate role + RemovePolicyDelegateRoleName: + Condition: CreateQuarantineUserResources + Value: !Ref RemovePolicyDelegateRole + Description: Name of the remove policy delegate role From 01d8ad960a99683d5645c02fff99b550e25a401b Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Fri, 28 Nov 2025 09:10:37 +0100 Subject: [PATCH 09/26] Fixed organizational stuff --- modules/response-actions/locals.tf | 9 +++++++-- modules/response-actions/organizational.tf | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/modules/response-actions/locals.tf b/modules/response-actions/locals.tf index 982499c..e397440 100644 --- a/modules/response-actions/locals.tf +++ b/modules/response-actions/locals.tf @@ -151,14 +151,19 @@ locals { # case4 - if both include and exclude accounts are provided, includes override excludes var.is_organizational && length(var.include_accounts) > 0 && length(var.exclude_accounts) > 0 ? ( "MIXED" - ) : "" + # case5 - if no explicit include/exclude, we still need to exclude current account + ) : var.is_organizational ? ( + "DIFFERENCE" + ) : "" ) ) ) ) ou_accounts_to_exclude = flatten([for ou_accounts in data.aws_organizations_organizational_unit_descendant_accounts.ou_accounts_to_exclude : [ou_accounts.accounts[*].id]]) - accounts_to_exclude = setunion(local.ou_accounts_to_exclude, var.exclude_accounts) + # Always exclude the current account (management account) from StackSet deployment + # since the Lambda roles already exist there + accounts_to_exclude = setunion(local.ou_accounts_to_exclude, var.exclude_accounts, [data.aws_caller_identity.current.account_id]) # switch cases for various user provided accounts configuration to be onboarded deployment_account_options = { diff --git a/modules/response-actions/organizational.tf b/modules/response-actions/organizational.tf index 09577ac..0be17ea 100644 --- a/modules/response-actions/organizational.tf +++ b/modules/response-actions/organizational.tf @@ -52,14 +52,14 @@ resource "aws_cloudformation_stack_set" "ra_delegate_roles" { resource "aws_cloudformation_stack_set_instance" "ra_delegate_roles" { for_each = var.is_organizational ? { - for pair in setproduct(local.region_set, local.deployment_targets_org_units) : - "${pair[0]}-${pair[1]}" => pair + for ou in local.deployment_targets_org_units : + ou => ou } : {} - stack_set_instance_region = each.value[0] + stack_set_instance_region = tolist(local.region_set)[0] stack_set_name = aws_cloudformation_stack_set.ra_delegate_roles[0].name deployment_targets { - organizational_unit_ids = [each.value[1]] + organizational_unit_ids = [each.value] accounts = local.check_old_ouid_param ? null : (local.deployment_targets_accounts_filter == "NONE" ? null : local.deployment_targets_accounts.accounts_to_deploy) account_filter_type = local.check_old_ouid_param ? null : local.deployment_targets_accounts_filter } From 708f9eaca99de5037367a6334edb589c84e4d379 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Fri, 28 Nov 2025 09:31:28 +0100 Subject: [PATCH 10/26] Other fixes. --- modules/response-actions/main.tf | 47 +++++++++++-------- .../configure-resource-access-policy.json | 2 +- .../create-volume-snapshots-policy.json | 2 +- .../delete-volume-snapshots-policy.json | 2 +- .../policies/fetch-cloud-logs-policy.json | 2 +- .../policies/quarantine-user-policy.json | 2 +- .../policies/remove-policy-policy.json | 2 +- 7 files changed, 34 insertions(+), 25 deletions(-) diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index dcd2246..dec31a0 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -2,13 +2,6 @@ data "aws_caller_identity" "current" {} data "aws_region" "current" {} locals { - quarantine_user_policy = templatefile("${path.module}/policies/quarantine-user-policy.json", {}) - fetch_cloud_logs_policy = templatefile("${path.module}/policies/fetch-cloud-logs-policy.json", {}) - remove_policy_policy = templatefile("${path.module}/policies/remove-policy-policy.json", {}) - configure_resource_access_policy = templatefile("${path.module}/policies/configure-resource-access-policy.json", {}) - create_volume_snapshots_policy = templatefile("${path.module}/policies/create-volume-snapshots-policy.json", {}) - delete_volume_snapshots_policy = templatefile("${path.module}/policies/delete-volume-snapshots-policy.json", {}) - # Use provided regions or default to current region region_set = length(var.regions) > 0 ? toset(var.regions) : toset([data.aws_region.current.id]) trusted_identity = var.is_gov_cloud_onboarding ? data.sysdig_secure_trusted_cloud_identity.trusted_identity.gov_identity : data.sysdig_secure_trusted_cloud_identity.trusted_identity.identity @@ -18,6 +11,22 @@ locals { account_id_hash = substr(md5(data.aws_caller_identity.current.account_id), 0, 4) ra_resource_name = "${var.name}-${random_id.suffix.hex}-${local.account_id_hash}" + # Centralized role names + quarantine_user_role_name = "${local.ra_resource_name}-quarantine-user-role" + fetch_cloud_logs_role_name = "${local.ra_resource_name}-fetch-cloud-logs-role" + remove_policy_role_name = "${local.ra_resource_name}-remove-policy-role" + configure_resource_access_role_name = "${local.ra_resource_name}-confi-res-access-role" + create_volume_snapshots_role_name = "${local.ra_resource_name}-create-vol-snap-role" + delete_volume_snapshots_role_name = "${local.ra_resource_name}-delete-vol-snap-role" + + # Policy templates with role names + quarantine_user_policy = templatefile("${path.module}/policies/quarantine-user-policy.json", { role_name = local.quarantine_user_role_name }) + fetch_cloud_logs_policy = templatefile("${path.module}/policies/fetch-cloud-logs-policy.json", { role_name = local.fetch_cloud_logs_role_name }) + remove_policy_policy = templatefile("${path.module}/policies/remove-policy-policy.json", { role_name = local.remove_policy_role_name }) + configure_resource_access_policy = templatefile("${path.module}/policies/configure-resource-access-policy.json", { role_name = local.configure_resource_access_role_name }) + create_volume_snapshots_policy = templatefile("${path.module}/policies/create-volume-snapshots-policy.json", { role_name = local.create_volume_snapshots_role_name }) + delete_volume_snapshots_policy = templatefile("${path.module}/policies/delete-volume-snapshots-policy.json", { role_name = local.delete_volume_snapshots_role_name }) + # StackSet role configuration administration_role_arn = var.auto_create_stackset_roles ? aws_iam_role.lambda_stackset_admin_role[0].arn : var.stackset_admin_role_arn execution_role_name = var.auto_create_stackset_roles ? aws_iam_role.lambda_stackset_execution_role[0].name : var.stackset_execution_role_name @@ -222,7 +231,7 @@ resource "aws_iam_role_policy" "shared_lambda_invoke_policy" { # Lambda Execution Role: Quarantine User resource "aws_iam_role" "quarantine_user_role" { count = local.enable_quarantine_user ? 1 : 0 - name = "${local.ra_resource_name}-quarantine-user-role" + name = local.quarantine_user_role_name assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -238,7 +247,7 @@ resource "aws_iam_role" "quarantine_user_role" { }) tags = { - Name = "${local.ra_resource_name}-quarantine-user-role" + Name = local.quarantine_user_role_name "sysdig.com/response-actions/cloud-actions" = "true" } } @@ -259,7 +268,7 @@ resource "aws_iam_role_policy" "quarantine_user_policy" { # Lambda Execution Role: Fetch Cloud Logs resource "aws_iam_role" "fetch_cloud_logs_role" { count = local.enable_fetch_cloud_logs ? 1 : 0 - name = "${local.ra_resource_name}-fetch-cloud-logs-role" + name = local.fetch_cloud_logs_role_name assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -275,7 +284,7 @@ resource "aws_iam_role" "fetch_cloud_logs_role" { }) tags = { - Name = "${local.ra_resource_name}-fetch-cloud-logs-role" + Name = local.fetch_cloud_logs_role_name "sysdig.com/response-actions/cloud-actions" = "true" } } @@ -296,7 +305,7 @@ resource "aws_iam_role_policy" "fetch_cloud_logs_policy" { # Lambda Execution Role: Remove Policy resource "aws_iam_role" "remove_policy_role" { count = local.enable_quarantine_user ? 1 : 0 - name = "${local.ra_resource_name}-remove-policy-role" + name = local.remove_policy_role_name assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -312,7 +321,7 @@ resource "aws_iam_role" "remove_policy_role" { }) tags = { - Name = "${local.ra_resource_name}-remove-policy-role" + Name = local.remove_policy_role_name "sysdig.com/response-actions/cloud-actions" = "true" } } @@ -333,7 +342,7 @@ resource "aws_iam_role_policy" "remove_policy_policy" { # Lambda Execution Role: Configure Resource Access resource "aws_iam_role" "configure_resource_access_role" { count = local.enable_make_private ? 1 : 0 - name = "${local.ra_resource_name}-confi-res-access-role" + name = local.configure_resource_access_role_name assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -349,7 +358,7 @@ resource "aws_iam_role" "configure_resource_access_role" { }) tags = { - Name = "${local.ra_resource_name}-configure-resource-access-role" + Name = local.configure_resource_access_role_name "sysdig.com/response-actions/cloud-actions" = "true" } } @@ -370,7 +379,7 @@ resource "aws_iam_role_policy" "configure_resource_access_policy" { # Lambda Execution Role: Create Volume Snapshots resource "aws_iam_role" "create_volume_snapshots_role" { count = local.enable_create_volume_snapshot ? 1 : 0 - name = "${local.ra_resource_name}-create-vol-snap-role" + name = local.create_volume_snapshots_role_name assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -386,7 +395,7 @@ resource "aws_iam_role" "create_volume_snapshots_role" { }) tags = { - Name = "${local.ra_resource_name}-create-volume-snapshots-role" + Name = local.create_volume_snapshots_role_name "sysdig.com/response-actions/cloud-actions" = "true" } } @@ -407,7 +416,7 @@ resource "aws_iam_role_policy" "create_volume_snapshots_policy" { # Lambda Execution Role: Delete Volume Snapshots resource "aws_iam_role" "delete_volume_snapshots_role" { count = local.enable_create_volume_snapshot ? 1 : 0 - name = "${local.ra_resource_name}-delete-vol-snap-role" + name = local.delete_volume_snapshots_role_name assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -423,7 +432,7 @@ resource "aws_iam_role" "delete_volume_snapshots_role" { }) tags = { - Name = "${local.ra_resource_name}-delete-volume-snapshots-role" + Name = local.delete_volume_snapshots_role_name "sysdig.com/response-actions/cloud-actions" = "true" } } diff --git a/modules/response-actions/policies/configure-resource-access-policy.json b/modules/response-actions/policies/configure-resource-access-policy.json index 3adfd9c..cddd88f 100644 --- a/modules/response-actions/policies/configure-resource-access-policy.json +++ b/modules/response-actions/policies/configure-resource-access-policy.json @@ -23,7 +23,7 @@ { "Effect": "Allow", "Action": "sts:AssumeRole", - "Resource": "arn:aws:iam::*:role/ResponseActionsConfigureAccessDelegateRole" + "Resource": "arn:aws:iam::*:role/${role_name}" } ] } diff --git a/modules/response-actions/policies/create-volume-snapshots-policy.json b/modules/response-actions/policies/create-volume-snapshots-policy.json index 10b24f6..6176846 100644 --- a/modules/response-actions/policies/create-volume-snapshots-policy.json +++ b/modules/response-actions/policies/create-volume-snapshots-policy.json @@ -34,7 +34,7 @@ { "Effect": "Allow", "Action": "sts:AssumeRole", - "Resource": "arn:aws:iam::*:role/ResponseActionsCreateVolumeSnapshotDelegateRole" + "Resource": "arn:aws:iam::*:role/${role_name}" } ] diff --git a/modules/response-actions/policies/delete-volume-snapshots-policy.json b/modules/response-actions/policies/delete-volume-snapshots-policy.json index 588051f..fd25ba6 100644 --- a/modules/response-actions/policies/delete-volume-snapshots-policy.json +++ b/modules/response-actions/policies/delete-volume-snapshots-policy.json @@ -22,7 +22,7 @@ { "Effect": "Allow", "Action": "sts:AssumeRole", - "Resource": "arn:aws:iam::*:role/ResponseActionsDeleteVolumeSnapshotDelegateRole" + "Resource": "arn:aws:iam::*:role/${role_name}" } ] diff --git a/modules/response-actions/policies/fetch-cloud-logs-policy.json b/modules/response-actions/policies/fetch-cloud-logs-policy.json index 8e2e0cf..2c91de3 100644 --- a/modules/response-actions/policies/fetch-cloud-logs-policy.json +++ b/modules/response-actions/policies/fetch-cloud-logs-policy.json @@ -12,7 +12,7 @@ { "Effect": "Allow", "Action": "sts:AssumeRole", - "Resource": "arn:aws:iam::*:role/ResponseActionsFetchCloudLogsDelegateRole" + "Resource": "arn:aws:iam::*:role/${role_name}" } ] diff --git a/modules/response-actions/policies/quarantine-user-policy.json b/modules/response-actions/policies/quarantine-user-policy.json index a892681..73dee4e 100644 --- a/modules/response-actions/policies/quarantine-user-policy.json +++ b/modules/response-actions/policies/quarantine-user-policy.json @@ -29,7 +29,7 @@ { "Effect": "Allow", "Action": "sts:AssumeRole", - "Resource": "arn:aws:iam::*:role/ResponseActionsQuarantineUserRoleDelegateRole" + "Resource": "arn:aws:iam::*:role/${role_name}" } ] diff --git a/modules/response-actions/policies/remove-policy-policy.json b/modules/response-actions/policies/remove-policy-policy.json index 5226b7c..f435911 100644 --- a/modules/response-actions/policies/remove-policy-policy.json +++ b/modules/response-actions/policies/remove-policy-policy.json @@ -17,7 +17,7 @@ { "Effect": "Allow", "Action": "sts:AssumeRole", - "Resource": "arn:aws:iam::*:role/ResponseActionsRemovePolicyDelegateRole" + "Resource": "arn:aws:iam::*:role/${role_name}" } ] From 05fb11237b35083ac177d63fb1c6dabe99f24cee Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Fri, 28 Nov 2025 10:51:56 +0100 Subject: [PATCH 11/26] Some comments --- modules/response-actions/locals.tf | 23 +++- modules/response-actions/main.tf | 123 +++++++++++++++++---- modules/response-actions/organizational.tf | 24 ++++ modules/response-actions/variables.tf | 6 +- 4 files changed, 147 insertions(+), 29 deletions(-) diff --git a/modules/response-actions/locals.tf b/modules/response-actions/locals.tf index e397440..e2bfd17 100644 --- a/modules/response-actions/locals.tf +++ b/modules/response-actions/locals.tf @@ -1,3 +1,22 @@ +#----------------------------------------------------------------------------------------------------------------------------------------- +# Organizational Deployment Configuration +# +# This file contains the logic for determining which organizational units (OUs) and accounts to deploy Response Actions to +# in an AWS Organization deployment. +# +# The module supports flexible inclusion/exclusion patterns: +# - include_ouids/exclude_ouids: Include or exclude entire organizational units +# - include_accounts/exclude_accounts: Include or exclude specific accounts +# - Legacy org_units parameter: Deprecated, will be removed November 30, 2025 +# +# Key behavior: +# 1. Inclusions are always prioritized over exclusions +# 2. The management account is automatically excluded from delegate role deployment (roles exist there already) +# 3. Due to AWS CloudFormation limitations with UNION filters, when including specific accounts, the stackset +# deploys to the entire org (with account filtering handled by Sysdig backend) +# +#----------------------------------------------------------------------------------------------------------------------------------------- + #---------------------------------------------------------- # Fetch & compute required data for organizational install #---------------------------------------------------------- @@ -161,8 +180,8 @@ locals { ) ou_accounts_to_exclude = flatten([for ou_accounts in data.aws_organizations_organizational_unit_descendant_accounts.ou_accounts_to_exclude : [ou_accounts.accounts[*].id]]) - # Always exclude the current account (management account) from StackSet deployment - # since the Lambda roles already exist there + # Always exclude the current account (management account) from StackSet deployment since the Lambda execution roles + # already exist in the management account. Delegate roles are only needed in member accounts. accounts_to_exclude = setunion(local.ou_accounts_to_exclude, var.exclude_accounts, [data.aws_caller_identity.current.account_id]) # switch cases for various user provided accounts configuration to be onboarded diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index dec31a0..c3d1da2 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -1,3 +1,23 @@ +#----------------------------------------------------------------------------------------------------------------------------------------- +# This module deploys Sysdig Secure Response Actions for AWS, enabling automated security responses to detected threats. +# +# For both Single Account and Organizational installs, Lambda functions are deployed using CloudFormation StackSets. +# For Organizational installs, see organizational.tf. +# +# Response Actions include: +# - Quarantine User: Attaches a deny-all policy to IAM users to prevent further actions +# - Fetch Cloud Logs: Retrieves CloudTrail and CloudWatch logs +# - Make Private: Removes public access from S3 buckets and RDS instances +# - Create Volume Snapshot: Creates EBS volume snapshots for forensic investigation +# +# For single installs, the resources in this file instrument the singleton account (management or member account). +# For organizational installs, resources in this file are created in the management account, with delegate roles +# deployed to member accounts via service-managed stacksets. +#----------------------------------------------------------------------------------------------------------------------------------------- + +#----------------------------------------------------------------------------------------- +# Fetch the data sources +#----------------------------------------------------------------------------------------- data "aws_caller_identity" "current" {} data "aws_region" "current" {} @@ -58,10 +78,21 @@ locals { ) } -#------------------------------------------------------ -# StackSet IAM Roles for multi-region deployment -#------------------------------------------------------ +#----------------------------------------------------------------------------------------------------------------------- +# A random resource is used to generate unique resource name suffix for Response Actions. +# This prevents conflicts when recreating Response Actions resources with the same name. +#----------------------------------------------------------------------------------------------------------------------- +resource "random_id" "suffix" { + byte_length = 3 +} +#----------------------------------------------------------------------------------------------------------------------------------------- +# Self-managed stacksets require a pair of StackSetAdministrationRole & StackSetExecutionRole IAM roles with self-managed permissions. +# +# If auto_create_stackset_roles is true, terraform will create this IAM Admin role in the source account with permissions to create +# stacksets. If false, and values for stackset Admin role ARN is provided, stackset will use it, else AWS will look for +# predefined/default role. +#----------------------------------------------------------------------------------------------------------------------------------------- # StackSet Administration Role resource "aws_iam_role" "lambda_stackset_admin_role" { count = var.auto_create_stackset_roles ? 1 : 0 @@ -96,12 +127,19 @@ resource "aws_iam_role_policy" "lambda_stackset_admin_policy" { { Effect = "Allow" Action = "sts:AssumeRole" - Resource = "arn:aws:iam::*:role/${local.ra_resource_name}-stackset-execution" + Resource = "${local.arn_prefix}:iam::*:role/${local.ra_resource_name}-stackset-execution" } ] }) } +#----------------------------------------------------------------------------------------------------------------------------------------- +# Self-managed stacksets require a pair of StackSetAdministrationRole & StackSetExecutionRole IAM roles with self-managed permissions. +# +# If auto_create_stackset_roles is true, terraform will create this IAM Execution role in the source account with permissions to +# deploy Lambda functions, create IAM roles, and manage logs. This role is assumed by the StackSet Administration role. +# If false, and values for stackset Execution role name is provided, stackset will use it. +#----------------------------------------------------------------------------------------------------------------------------------------- # StackSet Execution Role resource "aws_iam_role" "lambda_stackset_execution_role" { count = var.auto_create_stackset_roles ? 1 : 0 @@ -161,15 +199,19 @@ resource "aws_iam_role_policy" "lambda_stackset_execution_policy" { }) } -resource "random_id" "suffix" { - byte_length = 3 -} - data "sysdig_secure_trusted_cloud_identity" "trusted_identity" { cloud_provider = "aws" } - +#----------------------------------------------------------------------------------------------------------------------------------------- +# This resource creates an IAM role in the source account with permissions to invoke Response Action Lambda functions. +# This role is assumed by Sysdig's cloud identity to trigger automated response actions. +# +# The role allows: +# 1. Sysdig's trusted identity to assume the role using an external ID for security +# 2. Invoking Lambda functions across all deployed regions based on enabled response actions +# 3. Using AWS Resource Groups Tagging API to discover resources +#----------------------------------------------------------------------------------------------------------------------------------------- resource "aws_iam_role" "shared_cross_account_lambda_invoker" { name = "${local.ra_resource_name}-cross-account-invoker" @@ -196,7 +238,15 @@ resource "aws_iam_role" "shared_cross_account_lambda_invoker" { } } -# Inline policy for invoking all Lambda functions across all deployed regions +#----------------------------------------------------------------------------------------------------------------------------------------- +# This policy grants the necessary permissions for the cross-account Lambda invoker role: +# 1. tag:GetResources - Allows discovering AWS resources by tags for response actions +# 2. lambda:InvokeFunction - Allows invoking enabled Response Action Lambda functions +# 3. lambda:GetFunction - Allows retrieving Lambda function details for validation +# +# The policy dynamically includes only the Lambda ARNs for enabled response actions, based on the +# enabled_response_actions variable configuration. +#----------------------------------------------------------------------------------------------------------------------------------------- resource "aws_iam_role_policy" "shared_lambda_invoke_policy" { name = "${local.ra_resource_name}-invoke-policy" role = aws_iam_role.shared_cross_account_lambda_invoker.id @@ -224,9 +274,19 @@ resource "aws_iam_role_policy" "shared_lambda_invoke_policy" { }) } -#------------------------------------------------------ -# IAM Roles for Lambda Functions (Global Resources) -#------------------------------------------------------ +#----------------------------------------------------------------------------------------------------------------------------------------- +# IAM Roles for Lambda Functions (Created Once in Management Account) +# +# These roles are created globally in the management account and are used by Lambda functions +# across all regions. Each role is tagged with 'sysdig.com/response-actions/cloud-actions = true' for identification. +# +# The roles grant specific permissions needed for each response action type: +# - Quarantine User: IAM policy and user management permissions +# - Fetch Cloud Logs: CloudTrail and CloudWatch Logs read access +# - Remove Policy: IAM policy detachment permissions +# - Configure Resource Access: S3 and EC2 security group modification permissions +# - Create/Delete Volume Snapshots: EBS snapshot management permissions +#----------------------------------------------------------------------------------------------------------------------------------------- # Lambda Execution Role: Quarantine User resource "aws_iam_role" "quarantine_user_role" { @@ -450,25 +510,35 @@ resource "aws_iam_role_policy" "delete_volume_snapshots_policy" { policy = local.delete_volume_snapshots_policy } -#------------------------------------------------------ +#----------------------------------------------------------------------------------------------------------------------------------------- # S3 Bucket for Lambda deployment packages -#------------------------------------------------------ -# NOTE: Removed local S3 bucket creation as Lambda functions now fetch from -# regional S3 buckets in a separate account. The bucket naming follows the -# pattern: {s3_bucket_prefix}-{region} # -# Each regional bucket should contain the following Lambda zip files: +# NOTE: Lambda functions fetch deployment packages from regional S3 buckets in a separate account. +# The bucket naming follows the pattern: {s3_bucket_prefix}-{region} +# +# Each regional bucket should contain the following Lambda zip files under the ${var.cloud_lambdas_path}/${var.response_actions_version} path: # - quarantine_user.zip # - fetch_cloud_logs.zip # - remove_policy.zip # - configure_resource_access.zip # - create_volume_snapshot.zip # - delete_volume_snapshot.zip +#----------------------------------------------------------------------------------------------------------------------------------------- -#------------------------------------------------------ -# CloudFormation StackSet for Multi-Region Lambda Deployment -#------------------------------------------------------ - +#----------------------------------------------------------------------------------------------------------------------------------------- +# This resource creates a stackset to deploy Response Action Lambda functions across multiple regions. +# +# The stackset creates Lambda functions in each specified region with the following configuration: +# 1. Lambda Functions - One per enabled response action, deployed from regional S3 buckets +# 2. CloudWatch Log Groups - For Lambda execution logs with retention policies +# 3. Function Configuration - Environment variables including API base URL and resource names +# +# The Lambda functions are deployed using deployment packages stored in regional S3 buckets. Each function +# assumes the corresponding IAM execution role created in the management account. +# +# Note: Self-managed stacksets require a pair of StackSetAdministrationRole & StackSetExecutionRole IAM roles +# with self-managed permissions. +#----------------------------------------------------------------------------------------------------------------------------------------- resource "aws_cloudformation_stack_set" "lambda_functions" { name = "${local.ra_resource_name}-lambda" tags = var.tags @@ -516,7 +586,12 @@ resource "aws_cloudformation_stack_set" "lambda_functions" { ] } -# StackSet instances to deploy Lambda functions in all specified regions +#----------------------------------------------------------------------------------------------------------------------------------------- +# StackSet instances to deploy Lambda functions in all specified regions. +# +# For each region in the region_set, this creates a stack instance that deploys the Lambda functions and their +# supporting resources. The deployment uses parallel execution across regions for faster rollout. +#----------------------------------------------------------------------------------------------------------------------------------------- resource "aws_cloudformation_stack_set_instance" "lambda_functions" { for_each = local.region_set stack_set_instance_region = each.key diff --git a/modules/response-actions/organizational.tf b/modules/response-actions/organizational.tf index 0be17ea..e8469a1 100644 --- a/modules/response-actions/organizational.tf +++ b/modules/response-actions/organizational.tf @@ -1,3 +1,18 @@ +#----------------------------------------------------------------------------------------------------------------------- +# These resources set up delegate IAM roles in member accounts of an AWS Organization for Response Actions via +# service-managed CloudFormation StackSets. For a single account installation, see main.tf. +# +# In an organizational deployment: +# 1. Lambda functions are created in the management account (main.tf) across all specified regions +# 2. Delegate roles are created in member accounts (this file) that allow Lambda functions to assume cross-account +# access to perform response actions in those accounts +# 3. The delegate roles grant the Lambda execution roles permission to perform actions like quarantine users, +# fetch logs, modify S3 buckets, and create snapshots in member accounts +# +# The delegate roles are deployed to all member accounts within the specified OUs, excluding the management account +# itself (since the Lambda execution roles already exist there). +#----------------------------------------------------------------------------------------------------------------------- + resource "aws_cloudformation_stack_set" "ra_delegate_roles" { count = var.is_organizational ? 1 : 0 @@ -50,6 +65,15 @@ resource "aws_cloudformation_stack_set" "ra_delegate_roles" { ] } +#----------------------------------------------------------------------------------------------------------------------- +# This resource deploys the delegate roles stackset to member accounts in the organization. +# +# Key deployment characteristics: +# - Deployed to organizational units specified in deployment_targets_org_units (from locals.tf) +# - Uses account_filter_type to exclude the management account (always excluded automatically in locals.tf) +# - Deployed to a single region per OU (delegate roles are global IAM resources) +# - Can optionally include/exclude specific accounts based on include_accounts/exclude_accounts variables +#----------------------------------------------------------------------------------------------------------------------- resource "aws_cloudformation_stack_set_instance" "ra_delegate_roles" { for_each = var.is_organizational ? { for ou in local.deployment_targets_org_units : diff --git a/modules/response-actions/variables.tf b/modules/response-actions/variables.tf index 88faab6..53379a0 100644 --- a/modules/response-actions/variables.tf +++ b/modules/response-actions/variables.tf @@ -1,5 +1,5 @@ variable "is_organizational" { - description = "(Optional) Set this field to 'true' to deploy EventBridge to an AWS Organization (Or specific OUs)" + description = "(Optional) Set this field to 'true' to deploy Response Actions to an AWS Organization (Or specific OUs)" type = bool default = false } @@ -15,7 +15,7 @@ variable "org_units" { } variable "regions" { - description = "(Optional) List of regions in which to setup EventBridge. By default, current region is selected" + description = "(Optional) List of regions in which to setup Response Actions lambdas. By default, current region is selected" type = set(string) default = [] } @@ -72,7 +72,7 @@ variable "sysdig_secure_account_id" { variable "is_gov_cloud_onboarding" { type = bool default = false - description = "true/false whether EventBridge should be deployed in a govcloud account/org or not" + description = "true/false whether Response Actions should be deployed in a govcloud account/org or not" } variable "include_ouids" { From d5effcce40f87d99488aac067ce533d393cfa4f5 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Fri, 28 Nov 2025 12:50:27 +0100 Subject: [PATCH 12/26] Short names in tags --- modules/response-actions/main.tf | 50 ++++++++++++------- modules/response-actions/organizational.tf | 4 +- .../templates/delegate_roles_stackset.tpl | 12 +++++ .../templates/lambda-stackset.yaml | 30 +++++++++++ 4 files changed, 77 insertions(+), 19 deletions(-) diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index c3d1da2..2a38866 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -30,6 +30,7 @@ locals { roles_component_type = "COMPONENT_CLOUD_RESPONDER_ROLES" account_id_hash = substr(md5(data.aws_caller_identity.current.account_id), 0, 4) ra_resource_name = "${var.name}-${random_id.suffix.hex}-${local.account_id_hash}" + clean_resource_name = var.name # Centralized role names quarantine_user_role_name = "${local.ra_resource_name}-quarantine-user-role" @@ -112,7 +113,8 @@ resource "aws_iam_role" "lambda_stackset_admin_role" { }) tags = { - Name = "${local.ra_resource_name}-stackset-admin" + Name = "${local.ra_resource_name}-stackset-admin" + "sysdig.com/response-actions/resource-name" = "stackset-admin" } } @@ -159,7 +161,8 @@ resource "aws_iam_role" "lambda_stackset_execution_role" { }) tags = { - Name = "${local.ra_resource_name}-stackset-execution" + Name = "${local.ra_resource_name}-stackset-execution" + "sysdig.com/response-actions/resource-name" = "stackset-execution" } } @@ -190,6 +193,8 @@ resource "aws_iam_role_policy" "lambda_stackset_execution_policy" { "logs:PutRetentionPolicy", "logs:TagResource", "logs:UntagResource", + "logs:TagLogGroup", + "logs:ListTagsForResource", "s3:GetObject", "s3:ListBucket" ] @@ -234,7 +239,8 @@ resource "aws_iam_role" "shared_cross_account_lambda_invoker" { }) tags = { - Name = "${local.ra_resource_name}-cross-account-invoker" + Name = "${local.ra_resource_name}-cross-account-invoker" + "sysdig.com/response-actions/resource-name" = "cross-account-invoker" } } @@ -307,8 +313,9 @@ resource "aws_iam_role" "quarantine_user_role" { }) tags = { - Name = local.quarantine_user_role_name - "sysdig.com/response-actions/cloud-actions" = "true" + Name = local.quarantine_user_role_name + "sysdig.com/response-actions/cloud-actions" = "true" + "sysdig.com/response-actions/resource-name" = "quarantine-user-role" } } @@ -344,8 +351,9 @@ resource "aws_iam_role" "fetch_cloud_logs_role" { }) tags = { - Name = local.fetch_cloud_logs_role_name - "sysdig.com/response-actions/cloud-actions" = "true" + Name = local.fetch_cloud_logs_role_name + "sysdig.com/response-actions/cloud-actions" = "true" + "sysdig.com/response-actions/resource-name" = "fetch-cloud-logs-role" } } @@ -381,8 +389,9 @@ resource "aws_iam_role" "remove_policy_role" { }) tags = { - Name = local.remove_policy_role_name - "sysdig.com/response-actions/cloud-actions" = "true" + Name = local.remove_policy_role_name + "sysdig.com/response-actions/cloud-actions" = "true" + "sysdig.com/response-actions/resource-name" = "remove-policy-role" } } @@ -418,8 +427,9 @@ resource "aws_iam_role" "configure_resource_access_role" { }) tags = { - Name = local.configure_resource_access_role_name - "sysdig.com/response-actions/cloud-actions" = "true" + Name = local.configure_resource_access_role_name + "sysdig.com/response-actions/cloud-actions" = "true" + "sysdig.com/response-actions/resource-name" = "configure-resource-access-role" } } @@ -455,8 +465,9 @@ resource "aws_iam_role" "create_volume_snapshots_role" { }) tags = { - Name = local.create_volume_snapshots_role_name - "sysdig.com/response-actions/cloud-actions" = "true" + Name = local.create_volume_snapshots_role_name + "sysdig.com/response-actions/cloud-actions" = "true" + "sysdig.com/response-actions/resource-name" = "create-volume-snapshots-role" } } @@ -492,8 +503,9 @@ resource "aws_iam_role" "delete_volume_snapshots_role" { }) tags = { - Name = local.delete_volume_snapshots_role_name - "sysdig.com/response-actions/cloud-actions" = "true" + Name = local.delete_volume_snapshots_role_name + "sysdig.com/response-actions/cloud-actions" = "true" + "sysdig.com/response-actions/resource-name" = "delete-volume-snapshots-role" } } @@ -540,9 +552,11 @@ resource "aws_iam_role_policy" "delete_volume_snapshots_policy" { # with self-managed permissions. #----------------------------------------------------------------------------------------------------------------------------------------- resource "aws_cloudformation_stack_set" "lambda_functions" { - name = "${local.ra_resource_name}-lambda" - tags = var.tags - permission_model = "SELF_MANAGED" + name = "${local.ra_resource_name}-lambda" + tags = merge(var.tags, { + "sysdig.com/response-actions/resource-name" = "lambda-stackset" + }) + permission_model = "SELF_MANAGED" capabilities = ["CAPABILITY_NAMED_IAM"] administration_role_arn = local.administration_role_arn execution_role_name = local.execution_role_name diff --git a/modules/response-actions/organizational.tf b/modules/response-actions/organizational.tf index e8469a1..b97c19f 100644 --- a/modules/response-actions/organizational.tf +++ b/modules/response-actions/organizational.tf @@ -17,7 +17,9 @@ resource "aws_cloudformation_stack_set" "ra_delegate_roles" { count = var.is_organizational ? 1 : 0 name = join("-", [local.ra_resource_name, "delegate-roles"]) - tags = var.tags + tags = merge(var.tags, { + "sysdig.com/response-actions/resource-name" = "delegate-roles-stackset" + }) permission_model = "SERVICE_MANAGED" capabilities = ["CAPABILITY_NAMED_IAM"] diff --git a/modules/response-actions/templates/delegate_roles_stackset.tpl b/modules/response-actions/templates/delegate_roles_stackset.tpl index e4b649b..9cd6701 100644 --- a/modules/response-actions/templates/delegate_roles_stackset.tpl +++ b/modules/response-actions/templates/delegate_roles_stackset.tpl @@ -104,6 +104,8 @@ Resources: Value: Terraform - Key: Purpose Value: ResponseActions + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'configure-resource-access-delegate-role' # Delegate Role: Create Volume Snapshot CreateVolumeSnapshotDelegateRole: @@ -150,6 +152,8 @@ Resources: Value: Terraform - Key: Purpose Value: ResponseActions + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'create-volume-snapshot-delegate-role' # Delegate Role: Delete Volume Snapshot DeleteVolumeSnapshotDelegateRole: @@ -188,6 +192,8 @@ Resources: Value: Terraform - Key: Purpose Value: ResponseActions + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'delete-volume-snapshot-delegate-role' # Delegate Role: Fetch Cloud Logs FetchCloudLogsDelegateRole: @@ -219,6 +225,8 @@ Resources: Value: Terraform - Key: Purpose Value: ResponseActions + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'fetch-cloud-logs-delegate-role' # Delegate Role: Quarantine User QuarantineUserRoleDelegateRole: @@ -267,6 +275,8 @@ Resources: Value: Terraform - Key: Purpose Value: ResponseActions + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'quarantine-user-delegate-role' # Delegate Role: Remove Policy RemovePolicyDelegateRole: @@ -303,6 +313,8 @@ Resources: Value: Terraform - Key: Purpose Value: ResponseActions + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'remove-policy-delegate-role' Outputs: ConfigureAccessDelegateRoleName: diff --git a/modules/response-actions/templates/lambda-stackset.yaml b/modules/response-actions/templates/lambda-stackset.yaml index 2e58c2b..70cd703 100644 --- a/modules/response-actions/templates/lambda-stackset.yaml +++ b/modules/response-actions/templates/lambda-stackset.yaml @@ -85,6 +85,9 @@ Resources: Properties: LogGroupName: !Sub '/aws/lambda/${ResourceName}-quarantine-user' RetentionInDays: 7 + Tags: + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'quarantine-user-log-group' FetchCloudLogsLogGroup: Type: AWS::Logs::LogGroup @@ -92,6 +95,9 @@ Resources: Properties: LogGroupName: !Sub '/aws/lambda/${ResourceName}-fetch-cloud-logs' RetentionInDays: 7 + Tags: + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'fetch-cloud-logs-log-group' RemovePolicyLogGroup: Type: AWS::Logs::LogGroup @@ -99,6 +105,9 @@ Resources: Properties: LogGroupName: !Sub '/aws/lambda/${ResourceName}-remove-policy' RetentionInDays: 7 + Tags: + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'remove-policy-log-group' ConfigureResourceAccessLogGroup: Type: AWS::Logs::LogGroup @@ -106,6 +115,9 @@ Resources: Properties: LogGroupName: !Sub '/aws/lambda/${ResourceName}-configure-resource-access' RetentionInDays: 7 + Tags: + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'configure-resource-access-log-group' CreateVolumeSnapshotsLogGroup: Type: AWS::Logs::LogGroup @@ -113,6 +125,9 @@ Resources: Properties: LogGroupName: !Sub '/aws/lambda/${ResourceName}-create-volume-snapshots' RetentionInDays: 7 + Tags: + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'create-volume-snapshots-log-group' DeleteVolumeSnapshotsLogGroup: Type: AWS::Logs::LogGroup @@ -120,6 +135,9 @@ Resources: Properties: LogGroupName: !Sub '/aws/lambda/${ResourceName}-delete-volume-snapshots' RetentionInDays: 7 + Tags: + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'delete-volume-snapshots-log-group' # Lambda Functions (Regional Resources) QuarantineUserFunction: @@ -145,6 +163,8 @@ Resources: Value: !Sub '${ResourceName}-quarantine-user' - Key: 'sysdig.com/response-actions/cloud-actions' Value: 'true' + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'quarantine-user-lambda' FetchCloudLogsFunction: Type: AWS::Lambda::Function @@ -169,6 +189,8 @@ Resources: Value: !Sub '${ResourceName}-fetch-cloud-logs' - Key: 'sysdig.com/response-actions/cloud-actions' Value: 'true' + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'fetch-cloud-logs-lambda' RemovePolicyFunction: Type: AWS::Lambda::Function @@ -193,6 +215,8 @@ Resources: Value: !Sub '${ResourceName}-remove-policy' - Key: 'sysdig.com/response-actions/cloud-actions' Value: 'true' + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'remove-policy-lambda' ConfigureResourceAccessFunction: Type: AWS::Lambda::Function @@ -217,6 +241,8 @@ Resources: Value: !Sub '${ResourceName}-configure-resource-access' - Key: 'sysdig.com/response-actions/cloud-actions' Value: 'true' + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'configure-resource-access-lambda' CreateVolumeSnapshotsFunction: Type: AWS::Lambda::Function @@ -241,6 +267,8 @@ Resources: Value: !Sub '${ResourceName}-create-volume-snapshots' - Key: 'sysdig.com/response-actions/cloud-actions' Value: 'true' + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'create-volume-snapshots-lambda' DeleteVolumeSnapshotsFunction: Type: AWS::Lambda::Function @@ -265,6 +293,8 @@ Resources: Value: !Sub '${ResourceName}-delete-volume-snapshots' - Key: 'sysdig.com/response-actions/cloud-actions' Value: 'true' + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'delete-volume-snapshots-lambda' Outputs: QuarantineUserFunctionArn: From a2fa0843b87ef4ea263390fb399d86b193b9bd0a Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Fri, 28 Nov 2025 15:15:16 +0100 Subject: [PATCH 13/26] Simple names as tags in resources --- modules/response-actions/main.tf | 2 ++ modules/response-actions/organizational.tf | 2 ++ .../response-actions/templates/delegate_roles_stackset.tpl | 4 ++++ modules/response-actions/templates/lambda-stackset.yaml | 4 ++++ 4 files changed, 12 insertions(+) diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index 2a38866..732e9dd 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -571,6 +571,7 @@ resource "aws_cloudformation_stack_set" "lambda_functions" { parameters = { ResourceName = local.ra_resource_name + TemplateVersion = md5(file("${path.module}/templates/lambda-stackset.yaml")) S3BucketPrefix = var.s3_bucket_prefix ApiBaseUrl = var.api_base_url QuarantineUserRoleArn = local.enable_quarantine_user ? aws_iam_role.quarantine_user_role[0].arn : "" @@ -611,6 +612,7 @@ resource "aws_cloudformation_stack_set_instance" "lambda_functions" { stack_set_instance_region = each.key stack_set_name = aws_cloudformation_stack_set.lambda_functions.name + operation_preferences { max_concurrent_percentage = 100 failure_tolerance_percentage = var.failure_tolerance_percentage diff --git a/modules/response-actions/organizational.tf b/modules/response-actions/organizational.tf index b97c19f..aeaedf5 100644 --- a/modules/response-actions/organizational.tf +++ b/modules/response-actions/organizational.tf @@ -37,6 +37,7 @@ resource "aws_cloudformation_stack_set" "ra_delegate_roles" { } parameters = { + TemplateVersion = md5(file("${path.module}/templates/delegate_roles_stackset.tpl")) QuarantineUserLambdaRoleArn = local.enable_quarantine_user ? aws_iam_role.quarantine_user_role[0].arn : "" QuarantineUserRoleName = local.enable_quarantine_user ? aws_iam_role.quarantine_user_role[0].name : "" FetchCloudLogsLambdaRoleArn = local.enable_fetch_cloud_logs ? aws_iam_role.fetch_cloud_logs_role[0].arn : "" @@ -84,6 +85,7 @@ resource "aws_cloudformation_stack_set_instance" "ra_delegate_roles" { stack_set_instance_region = tolist(local.region_set)[0] stack_set_name = aws_cloudformation_stack_set.ra_delegate_roles[0].name + deployment_targets { organizational_unit_ids = [each.value] accounts = local.check_old_ouid_param ? null : (local.deployment_targets_accounts_filter == "NONE" ? null : local.deployment_targets_accounts.accounts_to_deploy) diff --git a/modules/response-actions/templates/delegate_roles_stackset.tpl b/modules/response-actions/templates/delegate_roles_stackset.tpl index 9cd6701..4f8fb6e 100644 --- a/modules/response-actions/templates/delegate_roles_stackset.tpl +++ b/modules/response-actions/templates/delegate_roles_stackset.tpl @@ -2,6 +2,10 @@ AWSTemplateFormatVersion: '2010-09-09' Description: 'Sysdig Response Actions Delegate Roles - Multi-Account Deployment' Parameters: + TemplateVersion: + Type: String + Description: Template version hash to force updates + Default: "1" QuarantineUserLambdaRoleArn: Type: String Description: ARN of the Lambda execution role for quarantine user function diff --git a/modules/response-actions/templates/lambda-stackset.yaml b/modules/response-actions/templates/lambda-stackset.yaml index 70cd703..03c4b7e 100644 --- a/modules/response-actions/templates/lambda-stackset.yaml +++ b/modules/response-actions/templates/lambda-stackset.yaml @@ -5,6 +5,10 @@ Parameters: ResourceName: Type: String Description: Base name for all resources + TemplateVersion: + Type: String + Description: Template version hash to force updates + Default: "1" S3BucketPrefix: Type: String Description: Prefix for regional S3 buckets containing Lambda deployment packages (bucket name will be {prefix}-{region}) From c6cc6335dee6c9fe01ed9f645433aeecf48b847b Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Fri, 28 Nov 2025 16:36:21 +0100 Subject: [PATCH 14/26] Removing unnecessary stuff --- .../modules/lambda-functions/main.tf | 111 ------------------ .../modules/lambda-functions/outputs.tf | 9 -- .../modules/lambda-functions/variables.tf | 56 --------- 3 files changed, 176 deletions(-) delete mode 100644 modules/response-actions/modules/lambda-functions/main.tf delete mode 100644 modules/response-actions/modules/lambda-functions/outputs.tf delete mode 100644 modules/response-actions/modules/lambda-functions/variables.tf diff --git a/modules/response-actions/modules/lambda-functions/main.tf b/modules/response-actions/modules/lambda-functions/main.tf deleted file mode 100644 index 64b97d5..0000000 --- a/modules/response-actions/modules/lambda-functions/main.tf +++ /dev/null @@ -1,111 +0,0 @@ -# Lambda execution role -resource "aws_iam_role" "lambda_execution_role" { - name = "${var.function_name}-role" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "lambda.amazonaws.com" - } - } - ] - }) - - tags = { - Name = "${var.function_name}-role" - } -} - -# Basic Lambda execution policy -resource "aws_iam_role_policy_attachment" "lambda_basic_execution" { - policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - role = aws_iam_role.lambda_execution_role.name -} - -# Validation using check blocks (Terraform 1.5+) -check "deployment_mode_validation" { - assert { - error_message = "Either local mode (function_zip_file) or S3 mode (s3_bucket, s3_key, s3_key_sha256) must be properly configured." - } -} - -data "http" "lambda_zip_resource" { - url = "https://download.sysdig.com/cloud-response-actions/v${var.response_actions_version}/${var.function_zip_file}" -} - -resource "local_file" "lambda_zip_file" { - filename = "${path.module}/lambda.zip" - content = data.http.lambda_zip_resource.body -} - -# Local values for hash calculation -locals { - function_sha256 = filebase64sha256(var.function_zip_file) - - # Merge environment variables with DELEGATE_ROLE_NAME if provided - merged_environment_variables = merge( - var.environment_variables, - { - # This will be the name of the role assumed in subaccounts. - # It's the name is conventionally the same as the one assumed by the lambda itself upon execution. - DELEGATE_ROLE_NAME = aws_iam_role.lambda_execution_role.name - } - ) -} - -# Inline (to avoid quota issues) custom policies for the Lambda execution role -resource "aws_iam_role_policy" "function_policies" { - count = length(var.function_policies) - - name = "${var.function_name}-custom-policy-${count.index + 1}" - role = aws_iam_role.lambda_execution_role.id - - policy = var.function_policies[count.index] -} - -# CloudWatch Log Group for Lambda function -resource "aws_cloudwatch_log_group" "lambda_log_group" { - name = "/aws/lambda/${var.function_name}" - retention_in_days = 7 - - tags = { - Name = "${var.function_name}-logs" - "sysdig.com/response-actions/cloud-actions" = "true" - } -} - -# Lambda function -resource "aws_lambda_function" "function" { - # Local mode configuration - filename = local_file.lambda_zip_file.filename - - function_name = var.function_name - role = aws_iam_role.lambda_execution_role.arn - handler = var.lambda_handler - runtime = var.lambda_runtime - timeout = var.lambda_timeout - memory_size = var.lambda_memory_size - source_code_hash = local.function_sha256 - publish = true - - dynamic "environment" { - for_each = length(local.merged_environment_variables) > 0 ? [1] : [] - content { - variables = local.merged_environment_variables - } - } - - tags = { - Name = var.function_name - "sysdig.com/response-actions/cloud-actions" = "true" - } - - depends_on = [ - aws_cloudwatch_log_group.lambda_log_group, - aws_iam_role_policy_attachment.lambda_basic_execution - ] -} diff --git a/modules/response-actions/modules/lambda-functions/outputs.tf b/modules/response-actions/modules/lambda-functions/outputs.tf deleted file mode 100644 index 6087ffc..0000000 --- a/modules/response-actions/modules/lambda-functions/outputs.tf +++ /dev/null @@ -1,9 +0,0 @@ -output "function_arn" { - description = "ARN of the Lambda function" - value = aws_lambda_function.function.arn -} - -output "function_name" { - description = "Name of the Lambda function" - value = aws_lambda_function.function.function_name -} diff --git a/modules/response-actions/modules/lambda-functions/variables.tf b/modules/response-actions/modules/lambda-functions/variables.tf deleted file mode 100644 index 3ca1146..0000000 --- a/modules/response-actions/modules/lambda-functions/variables.tf +++ /dev/null @@ -1,56 +0,0 @@ -variable "function_name" { - description = "Name of the Lambda function" - type = string -} - -variable "function_zip_file" { - description = "Path to the local Lambda deployment package zip file (optional, for local mode)" - type = string - default = null -} - -variable "lambda_handler" { - description = "Lambda function handler" - type = string - default = "index.handler" -} - -variable "lambda_runtime" { - description = "Lambda function runtime" - type = string - default = "python3.13" -} - -variable "lambda_timeout" { - description = "Lambda function timeout in seconds" - type = number - default = 300 -} - -variable "lambda_memory_size" { - description = "Memory (MB) to allocate to the Lambda function" - type = number - default = 128 -} - -variable "function_policies" { - description = "List of policy documents for the Lambda function" - type = list(string) - default = [] -} - -variable "environment_variables" { - description = "Environment variables for the Lambda function" - type = map(string) - default = {} -} - -variable "response_actions_version" { - description = "Name of the IAM role to assume for cross-account operations" - type = string -} - -variable "arn_prefix" { - description = "The prefix for any AWS ARN" - type = "string" -} \ No newline at end of file From b00b92f51b7a8655dc0229e97d8ba5b7d4196e92 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Fri, 28 Nov 2025 17:16:17 +0100 Subject: [PATCH 15/26] Initial change to support sysdig components --- modules/response-actions/main.tf | 39 +++++++++--- modules/response-actions/sysdig_components.tf | 63 +++++++++++++++++++ 2 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 modules/response-actions/sysdig_components.tf diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index 732e9dd..8293d90 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -30,7 +30,6 @@ locals { roles_component_type = "COMPONENT_CLOUD_RESPONDER_ROLES" account_id_hash = substr(md5(data.aws_caller_identity.current.account_id), 0, 4) ra_resource_name = "${var.name}-${random_id.suffix.hex}-${local.account_id_hash}" - clean_resource_name = var.name # Centralized role names quarantine_user_role_name = "${local.ra_resource_name}-quarantine-user-role" @@ -40,6 +39,14 @@ locals { create_volume_snapshots_role_name = "${local.ra_resource_name}-create-vol-snap-role" delete_volume_snapshots_role_name = "${local.ra_resource_name}-delete-vol-snap-role" + # Centralized Lambda function names + quarantine_user_lambda_name = "${local.ra_resource_name}-quarantine-user" + fetch_cloud_logs_lambda_name = "${local.ra_resource_name}-fetch-cloud-logs" + remove_policy_lambda_name = "${local.ra_resource_name}-remove-policy" + configure_resource_access_lambda_name = "${local.ra_resource_name}-configure-resource-access" + create_volume_snapshots_lambda_name = "${local.ra_resource_name}-create-volume-snapshots" + delete_volume_snapshots_lambda_name = "${local.ra_resource_name}-delete-volume-snapshots" + # Policy templates with role names quarantine_user_policy = templatefile("${path.module}/policies/quarantine-user-policy.json", { role_name = local.quarantine_user_role_name }) fetch_cloud_logs_policy = templatefile("${path.module}/policies/fetch-cloud-logs-policy.json", { role_name = local.fetch_cloud_logs_role_name }) @@ -63,18 +70,36 @@ locals { # Build list of Lambda ARNs for invoke policy based on enabled actions enabled_lambda_arns = concat( local.enable_quarantine_user ? [ - "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-quarantine-user", - "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-remove-policy" + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.quarantine_user_lambda_name}", + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.remove_policy_lambda_name}" + ] : [], + local.enable_fetch_cloud_logs ? [ + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.fetch_cloud_logs_lambda_name}" + ] : [], + local.enable_make_private ? [ + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.configure_resource_access_lambda_name}" + ] : [], + local.enable_create_volume_snapshot ? [ + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.create_volume_snapshots_lambda_name}", + "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.delete_volume_snapshots_lambda_name}" + ] : [] + ) + + # Build list of Lambda function names based on enabled actions + enabled_lambda_names = concat( + local.enable_quarantine_user ? [ + local.quarantine_user_lambda_name, + local.remove_policy_lambda_name ] : [], local.enable_fetch_cloud_logs ? [ - "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-fetch-cloud-logs" + local.fetch_cloud_logs_lambda_name ] : [], local.enable_make_private ? [ - "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-configure-resource-access" + local.configure_resource_access_lambda_name ] : [], local.enable_create_volume_snapshot ? [ - "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-create-volume-snapshots", - "${local.arn_prefix}:lambda:*:${data.aws_caller_identity.current.account_id}:function:${local.ra_resource_name}-delete-volume-snapshots" + local.create_volume_snapshots_lambda_name, + local.delete_volume_snapshots_lambda_name ] : [] ) } diff --git a/modules/response-actions/sysdig_components.tf b/modules/response-actions/sysdig_components.tf new file mode 100644 index 0000000..55120a5 --- /dev/null +++ b/modules/response-actions/sysdig_components.tf @@ -0,0 +1,63 @@ +#----------------------------------------------------------------------------------------------------------------------------------------- +# Call Sysdig Backend to add the response actions responder integration to the Sysdig Cloud Account +# +# Note (optional): To ensure this gets called after all cloud resources are created, add +# explicit dependency using depends_on +#----------------------------------------------------------------------------------------------------------------------------------------- +resource "sysdig_secure_cloud_auth_account_component" "aws_responder" { + account_id = var.sysdig_secure_account_id + type = local.responder_component_type + instance = "cloud-responder" + version = var.response_actions_version + cloud_responder_metadata = jsonencode({ + aws = { + responder_lambdas = { + lambda_names = local.enabled_lambda_names + regions = local.region_set + delegate_role_name = aws_iam_role.shared_cross_account_lambda_invoker + } + } + }) + + depends_on = [ + aws_cloudformation_stack_set_instance.lambda_functions, + aws_iam_role.shared_cross_account_lambda_invoker + ] +} + +resource "sysdig_secure_cloud_auth_account_component" "aws_responder_roles" { + account_id = var.sysdig_secure_account_id + type = local.roles_component_type + instance = "cloud-responder" + version = var.response_actions_version + cloud_responder_roles_metadata = jsonencode({ + aws = { + roles = concat( + local.enable_quarantine_user ? [ + aws_iam_role.quarantine_user_role[0].arn, + aws_iam_role.remove_policy_role[0].arn + ] : [], + local.enable_fetch_cloud_logs ? [ + aws_iam_role.fetch_cloud_logs_role[0].arn + ] : [], + local.enable_make_private ? [ + aws_iam_role.configure_resource_access_role[0].arn + ] : [], + local.enable_create_volume_snapshot ? [ + aws_iam_role.create_volume_snapshots_role[0].arn, + aws_iam_role.delete_volume_snapshots_role[0].arn + ] : [] + ) + } + }) + + depends_on = [ + aws_cloudformation_stack_set_instance.lambda_functions, + aws_iam_role.quarantine_user_role, + aws_iam_role.remove_policy_role, + aws_iam_role.fetch_cloud_logs_role, + aws_iam_role.configure_resource_access_role, + aws_iam_role.create_volume_snapshots_role, + aws_iam_role.delete_volume_snapshots_role + ] +} From a15cf123ef8de0e7093b8984781b8a95fa0ede0b Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Mon, 1 Dec 2025 18:02:38 +0100 Subject: [PATCH 16/26] Other fixes. --- modules/response-actions/sysdig_components.tf | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/modules/response-actions/sysdig_components.tf b/modules/response-actions/sysdig_components.tf index 55120a5..781f0b3 100644 --- a/modules/response-actions/sysdig_components.tf +++ b/modules/response-actions/sysdig_components.tf @@ -14,7 +14,7 @@ resource "sysdig_secure_cloud_auth_account_component" "aws_responder" { responder_lambdas = { lambda_names = local.enabled_lambda_names regions = local.region_set - delegate_role_name = aws_iam_role.shared_cross_account_lambda_invoker + delegate_role_name = aws_iam_role.shared_cross_account_lambda_invoker.name } } }) @@ -31,24 +31,46 @@ resource "sysdig_secure_cloud_auth_account_component" "aws_responder_roles" { instance = "cloud-responder" version = var.response_actions_version cloud_responder_roles_metadata = jsonencode({ - aws = { - roles = concat( - local.enable_quarantine_user ? [ - aws_iam_role.quarantine_user_role[0].arn, - aws_iam_role.remove_policy_role[0].arn - ] : [], - local.enable_fetch_cloud_logs ? [ - aws_iam_role.fetch_cloud_logs_role[0].arn - ] : [], - local.enable_make_private ? [ - aws_iam_role.configure_resource_access_role[0].arn - ] : [], - local.enable_create_volume_snapshot ? [ - aws_iam_role.create_volume_snapshots_role[0].arn, - aws_iam_role.delete_volume_snapshots_role[0].arn - ] : [] - ) - } + roles = concat( + local.enable_quarantine_user ? [ + { + aws = { + role_name = aws_iam_role.quarantine_user_role[0].arn + } + }, + { + aws = { + role_name = aws_iam_role.remove_policy_role[0].arn + } + } + ] : [], + local.enable_fetch_cloud_logs ? [ + { + aws = { + role_name = aws_iam_role.fetch_cloud_logs_role[0].arn + } + } + ] : [], + local.enable_make_private ? [ + { + aws = { + role_name = aws_iam_role.configure_resource_access_role[0].arn + } + } + ] : [], + local.enable_create_volume_snapshot ? [ + { + aws = { + role_name = aws_iam_role.create_volume_snapshots_role[0].arn + } + }, + { + aws = { + role_name = aws_iam_role.delete_volume_snapshots_role[0].arn + } + } + ] : [] + ) }) depends_on = [ From 32e011d6029096b4708a77fb0fe54a175b1e01b8 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Tue, 2 Dec 2025 08:13:42 +0100 Subject: [PATCH 17/26] Fixed version. --- modules/response-actions/sysdig_components.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/response-actions/sysdig_components.tf b/modules/response-actions/sysdig_components.tf index 781f0b3..7eb396f 100644 --- a/modules/response-actions/sysdig_components.tf +++ b/modules/response-actions/sysdig_components.tf @@ -8,7 +8,7 @@ resource "sysdig_secure_cloud_auth_account_component" "aws_responder" { account_id = var.sysdig_secure_account_id type = local.responder_component_type instance = "cloud-responder" - version = var.response_actions_version + version = "v${var.response_actions_version}" cloud_responder_metadata = jsonencode({ aws = { responder_lambdas = { @@ -29,7 +29,7 @@ resource "sysdig_secure_cloud_auth_account_component" "aws_responder_roles" { account_id = var.sysdig_secure_account_id type = local.roles_component_type instance = "cloud-responder" - version = var.response_actions_version + version = "v${var.response_actions_version}" cloud_responder_roles_metadata = jsonencode({ roles = concat( local.enable_quarantine_user ? [ From b0c7db0bb78d849f46aeed144913d55f0f20bfd1 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Thu, 4 Dec 2025 18:32:02 +0100 Subject: [PATCH 18/26] Added components outputs --- modules/response-actions/outputs.tf | 12 ++++++++++++ modules/response-actions/versions.tf | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/modules/response-actions/outputs.tf b/modules/response-actions/outputs.tf index bb7a037..a369cd8 100644 --- a/modules/response-actions/outputs.tf +++ b/modules/response-actions/outputs.tf @@ -55,3 +55,15 @@ output "lambda_functions" { } : {} ) } + +output "responder_component_id" { + value = "${sysdig_secure_cloud_auth_account_component.aws_responder.type}/${sysdig_secure_cloud_auth_account_component.aws_responder.instance}" + description = "Component identifier of Response Actions responder integration created in Sysdig Backend" + depends_on = [sysdig_secure_cloud_auth_account_component.aws_responder] +} + +output "responder_roles_component_id" { + value = "${sysdig_secure_cloud_auth_account_component.aws_responder_roles.type}/${sysdig_secure_cloud_auth_account_component.aws_responder_roles.instance}" + description = "Component identifier of Response Actions roles integration created in Sysdig Backend" + depends_on = [sysdig_secure_cloud_auth_account_component.aws_responder_roles] +} diff --git a/modules/response-actions/versions.tf b/modules/response-actions/versions.tf index db226f1..f8151c7 100644 --- a/modules/response-actions/versions.tf +++ b/modules/response-actions/versions.tf @@ -10,8 +10,8 @@ terraform { version = ">= 3.1" } sysdig = { - source = "sysdiglabs/sysdig" - version = "~> 1.48" + source = "local/sysdiglabs/sysdig" + version = "~> 1.0.0" } } } From f344382b7c24b43037e8459cbeda2bf523de9b04 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Tue, 9 Dec 2025 11:44:55 +0100 Subject: [PATCH 19/26] Creating S3 storage and lambda to upload lambda code --- modules/response-actions/main.tf | 113 ++++++++- .../templates/lambda-stackset.yaml | 233 ++++++++++++++++-- modules/response-actions/variables.tf | 11 +- 3 files changed, 317 insertions(+), 40 deletions(-) diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index 8293d90..cd9c24e 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -59,7 +59,8 @@ locals { administration_role_arn = var.auto_create_stackset_roles ? aws_iam_role.lambda_stackset_admin_role[0].arn : var.stackset_admin_role_arn execution_role_name = var.auto_create_stackset_roles ? aws_iam_role.lambda_stackset_execution_role[0].name : var.stackset_execution_role_name - cloud_lambdas_path = "${var.cloud_lambdas_path}/${var.response_actions_version}" + # S3 bucket configuration for Lambda packages + s3_bucket_name = "${var.name}-${random_id.suffix.hex}-packages" # Response action enablement flags enable_make_private = contains(var.enabled_response_actions, "make_private") @@ -202,7 +203,6 @@ resource "aws_iam_role_policy" "lambda_stackset_execution_policy" { { Effect = "Allow" Action = [ - "cloudformation:*", "lambda:*", "iam:CreateRole", "iam:DeleteRole", @@ -213,6 +213,9 @@ resource "aws_iam_role_policy" "lambda_stackset_execution_policy" { "iam:AttachRolePolicy", "iam:DetachRolePolicy", "iam:GetRolePolicy", + "iam:TagRole", + "iam:UntagRole", + "iam:ListRoleTags", "logs:CreateLogGroup", "logs:DeleteLogGroup", "logs:PutRetentionPolicy", @@ -220,7 +223,18 @@ resource "aws_iam_role_policy" "lambda_stackset_execution_policy" { "logs:UntagResource", "logs:TagLogGroup", "logs:ListTagsForResource", + "s3:CreateBucket", + "s3:DeleteBucket", + "s3:GetBucketLocation", + "s3:GetBucketVersioning", + "s3:PutBucketVersioning", + "s3:PutBucketPublicAccessBlock", + "s3:GetBucketPublicAccessBlock", + "s3:PutBucketTagging", + "s3:GetBucketTagging", "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", "s3:ListBucket" ] Resource = "*" @@ -229,6 +243,12 @@ resource "aws_iam_role_policy" "lambda_stackset_execution_policy" { }) } +resource "aws_iam_role_policy_attachment" "lambda_stackset_execution_cloudformation" { + count = var.auto_create_stackset_roles ? 1 : 0 + role = aws_iam_role.lambda_stackset_execution_role[0].name + policy_arn = "${local.arn_prefix}:iam::aws:policy/AWSCloudFormationFullAccess" +} + data "sysdig_secure_trusted_cloud_identity" "trusted_identity" { cloud_provider = "aws" } @@ -547,19 +567,89 @@ resource "aws_iam_role_policy" "delete_volume_snapshots_policy" { policy = local.delete_volume_snapshots_policy } +# Lambda Execution Role: Package Downloader (Global, used by all regions) +resource "aws_iam_role" "package_downloader_role" { + name = "${local.ra_resource_name}-package-downloader-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = { + Name = "${local.ra_resource_name}-package-downloader-role" + "sysdig.com/response-actions/resource-name" = "package-downloader-role" + } +} + +resource "aws_iam_role_policy_attachment" "package_downloader_basic" { + policy_arn = "${local.arn_prefix}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = aws_iam_role.package_downloader_role.name +} + +resource "aws_iam_role_policy" "package_downloader_policy" { + name = "${local.ra_resource_name}-package-downloader-policy" + role = aws_iam_role.package_downloader_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:PutObject", + "s3:DeleteObject", + "s3:GetObject" + ] + Resource = "${local.arn_prefix}:s3:::${local.s3_bucket_name}-*/*" + }, + { + Effect = "Allow" + Action = [ + "s3:ListBucket" + ] + Resource = "${local.arn_prefix}:s3:::${local.s3_bucket_name}-*" + } + ] + }) +} + +# Wait for IAM role to propagate globally before using it +resource "time_sleep" "wait_for_iam_role_propagation" { + depends_on = [ + aws_iam_role.package_downloader_role, + aws_iam_role_policy.package_downloader_policy, + aws_iam_role_policy_attachment.package_downloader_basic + ] + + create_duration = "10s" +} + #----------------------------------------------------------------------------------------------------------------------------------------- # S3 Bucket for Lambda deployment packages # -# NOTE: Lambda functions fetch deployment packages from regional S3 buckets in a separate account. -# The bucket naming follows the pattern: {s3_bucket_prefix}-{region} +# The CloudFormation StackSet will create regional S3 buckets automatically in each deployed region. +# The bucket naming follows the pattern: {s3_bucket_name}-{region} +# +# A custom Lambda function (PackageDownloaderFunction) will download the Lambda zip files from the provided +# lambda_packages_base_url and upload them to the regional S3 buckets. The packages are downloaded from: +# {lambda_packages_base_url}/v{version}/{lambda_name}.zip # -# Each regional bucket should contain the following Lambda zip files under the ${var.cloud_lambdas_path}/${var.response_actions_version} path: +# Lambda packages downloaded: # - quarantine_user.zip # - fetch_cloud_logs.zip # - remove_policy.zip # - configure_resource_access.zip -# - create_volume_snapshot.zip -# - delete_volume_snapshot.zip +# - create_volume_snapshots.zip +# - delete_volume_snapshots.zip #----------------------------------------------------------------------------------------------------------------------------------------- #----------------------------------------------------------------------------------------------------------------------------------------- @@ -597,8 +687,9 @@ resource "aws_cloudformation_stack_set" "lambda_functions" { parameters = { ResourceName = local.ra_resource_name TemplateVersion = md5(file("${path.module}/templates/lambda-stackset.yaml")) - S3BucketPrefix = var.s3_bucket_prefix + S3BucketName = local.s3_bucket_name ApiBaseUrl = var.api_base_url + PackageDownloaderRoleArn = aws_iam_role.package_downloader_role.arn QuarantineUserRoleArn = local.enable_quarantine_user ? aws_iam_role.quarantine_user_role[0].arn : "" FetchCloudLogsRoleArn = local.enable_fetch_cloud_logs ? aws_iam_role.fetch_cloud_logs_role[0].arn : "" RemovePolicyRoleArn = local.enable_quarantine_user ? aws_iam_role.remove_policy_role[0].arn : "" @@ -611,7 +702,8 @@ resource "aws_cloudformation_stack_set" "lambda_functions" { ConfigureResourceAccessRoleName = local.enable_make_private ? aws_iam_role.configure_resource_access_role[0].name : "" CreateVolumeSnapshotsRoleName = local.enable_create_volume_snapshot ? aws_iam_role.create_volume_snapshots_role[0].name : "" DeleteVolumeSnapshotsRoleName = local.enable_create_volume_snapshot ? aws_iam_role.delete_volume_snapshots_role[0].name : "" - CloudLambdasPath = local.cloud_lambdas_path + ResponseActionsVersion = var.response_actions_version + LambdaPackagesBaseUrl = var.lambda_packages_base_url EnableQuarantineUser = local.enable_quarantine_user ? "true" : "false" EnableFetchCloudLogs = local.enable_fetch_cloud_logs ? "true" : "false" EnableMakePrivate = local.enable_make_private ? "true" : "false" @@ -622,7 +714,8 @@ resource "aws_cloudformation_stack_set" "lambda_functions" { depends_on = [ aws_iam_role.lambda_stackset_admin_role, - aws_iam_role.lambda_stackset_execution_role + aws_iam_role.lambda_stackset_execution_role, + time_sleep.wait_for_iam_role_propagation ] } diff --git a/modules/response-actions/templates/lambda-stackset.yaml b/modules/response-actions/templates/lambda-stackset.yaml index 03c4b7e..cbadde3 100644 --- a/modules/response-actions/templates/lambda-stackset.yaml +++ b/modules/response-actions/templates/lambda-stackset.yaml @@ -9,12 +9,15 @@ Parameters: Type: String Description: Template version hash to force updates Default: "1" - S3BucketPrefix: + S3BucketName: Type: String - Description: Prefix for regional S3 buckets containing Lambda deployment packages (bucket name will be {prefix}-{region}) + Description: Name for the regional S3 bucket containing Lambda deployment packages ApiBaseUrl: Type: String Description: API base URL for Lambda functions + PackageDownloaderRoleArn: + Type: String + Description: ARN of the IAM role for package downloader function QuarantineUserRoleArn: Type: String Description: ARN of the IAM role for quarantine user function @@ -51,9 +54,12 @@ Parameters: DeleteVolumeSnapshotsRoleName: Type: String Description: Name of the IAM role for delete volume snapshots function - CloudLambdasPath: + ResponseActionsVersion: + Type: String + Description: Version of response actions packages to download + LambdaPackagesBaseUrl: Type: String - Description: The path where lambda code resides inside of regional S3 buckets + Description: Base URL for downloading Lambda deployment packages EnableQuarantineUser: Type: String Default: "true" @@ -82,6 +88,177 @@ Conditions: CreateVolumeSnapshotResources: !Equals [!Ref EnableCreateVolumeSnapshot, "true"] Resources: + # S3 Bucket for Lambda packages (Regional) + LambdaPackagesBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Sub '${S3BucketName}-${AWS::Region}' + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + VersioningConfiguration: + Status: Enabled + Tags: + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'lambda-packages-bucket' + + # Package Downloader Lambda (per region) + PackageDownloaderLogGroup: + Type: AWS::Logs::LogGroup + DeletionPolicy: Retain + Properties: + LogGroupName: !Sub '/aws/lambda/${ResourceName}-package-downloader' + RetentionInDays: 7 + Tags: + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'package-downloader-log-group' + + PackageDownloaderFunction: + Type: AWS::Lambda::Function + DependsOn: PackageDownloaderLogGroup + Properties: + FunctionName: !Sub '${ResourceName}-package-downloader' + Runtime: python3.12 + Handler: index.handler + Role: !Ref PackageDownloaderRoleArn + Timeout: 300 + MemorySize: 256 + Code: + ZipFile: | + import json + import urllib.request + import boto3 + import cfnresponse + + def handler(event, context): + try: + print(f"Event: {json.dumps(event)}") + + if event['RequestType'] == 'Delete': + # Clean up S3 object on delete + bucket = event['ResourceProperties']['Bucket'] + key = event['ResourceProperties']['Key'] + s3 = boto3.client('s3') + try: + s3.delete_object(Bucket=bucket, Key=key) + print(f"Deleted s3://{bucket}/{key}") + except Exception as e: + print(f"Error deleting object (may not exist): {e}") + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + return + + if event['RequestType'] in ['Create', 'Update']: + url = event['ResourceProperties']['Url'] + bucket = event['ResourceProperties']['Bucket'] + key = event['ResourceProperties']['Key'] + + print(f"Downloading from {url}") + print(f"Target: s3://{bucket}/{key}") + + # Download from URL + try: + req = urllib.request.Request(url) + with urllib.request.urlopen(req, timeout=60) as response: + content = response.read() + print(f"Downloaded {len(content)} bytes, status: {response.status}") + except urllib.error.HTTPError as e: + error_msg = f"HTTP {e.code} downloading {url}: {e.reason}" + print(error_msg) + cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_msg}) + return + except urllib.error.URLError as e: + error_msg = f"URL error downloading {url}: {str(e.reason)}" + print(error_msg) + cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_msg}) + return + + # Upload to S3 + try: + s3 = boto3.client('s3') + s3.put_object(Bucket=bucket, Key=key, Body=content) + print(f"Uploaded to s3://{bucket}/{key}") + except Exception as e: + error_msg = f"S3 upload error: {str(e)}" + print(error_msg) + cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_msg}) + return + + cfnresponse.send(event, context, cfnresponse.SUCCESS, { + 'Bucket': bucket, + 'Key': key, + 'Size': len(content) + }) + else: + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + + except Exception as e: + import traceback + error_msg = f"Unexpected error: {str(e)}\n{traceback.format_exc()}" + print(error_msg) + cfnresponse.send(event, context, cfnresponse.FAILED, { + 'Error': str(e) + }) + Tags: + - Key: 'sysdig.com/response-actions/resource-name' + Value: 'package-downloader-lambda' + + # Custom Resources to download Lambda packages + QuarantineUserPackage: + Type: Custom::LambdaPackage + Condition: CreateQuarantineUserResources + Properties: + ServiceToken: !GetAtt PackageDownloaderFunction.Arn + Url: !Sub '${LambdaPackagesBaseUrl}/v${ResponseActionsVersion}/quarantine_user.zip' + Bucket: !Ref LambdaPackagesBucket + Key: !Sub '${ResponseActionsVersion}/quarantine_user.zip' + + FetchCloudLogsPackage: + Type: Custom::LambdaPackage + Condition: CreateFetchCloudLogsResources + Properties: + ServiceToken: !GetAtt PackageDownloaderFunction.Arn + Url: !Sub '${LambdaPackagesBaseUrl}/v${ResponseActionsVersion}/fetch_cloud_logs.zip' + Bucket: !Ref LambdaPackagesBucket + Key: !Sub '${ResponseActionsVersion}/fetch_cloud_logs.zip' + + RemovePolicyPackage: + Type: Custom::LambdaPackage + Condition: CreateQuarantineUserResources + Properties: + ServiceToken: !GetAtt PackageDownloaderFunction.Arn + Url: !Sub '${LambdaPackagesBaseUrl}/v${ResponseActionsVersion}/remove_policy.zip' + Bucket: !Ref LambdaPackagesBucket + Key: !Sub '${ResponseActionsVersion}/remove_policy.zip' + + ConfigureResourceAccessPackage: + Type: Custom::LambdaPackage + Condition: CreateMakePrivateResources + Properties: + ServiceToken: !GetAtt PackageDownloaderFunction.Arn + Url: !Sub '${LambdaPackagesBaseUrl}/v${ResponseActionsVersion}/configure_resource_access.zip' + Bucket: !Ref LambdaPackagesBucket + Key: !Sub '${ResponseActionsVersion}/configure_resource_access.zip' + + CreateVolumeSnapshotsPackage: + Type: Custom::LambdaPackage + Condition: CreateVolumeSnapshotResources + Properties: + ServiceToken: !GetAtt PackageDownloaderFunction.Arn + Url: !Sub '${LambdaPackagesBaseUrl}/v${ResponseActionsVersion}/create_volume_snapshots.zip' + Bucket: !Ref LambdaPackagesBucket + Key: !Sub '${ResponseActionsVersion}/create_volume_snapshots.zip' + + DeleteVolumeSnapshotsPackage: + Type: Custom::LambdaPackage + Condition: CreateVolumeSnapshotResources + Properties: + ServiceToken: !GetAtt PackageDownloaderFunction.Arn + Url: !Sub '${LambdaPackagesBaseUrl}/v${ResponseActionsVersion}/delete_volume_snapshots.zip' + Bucket: !Ref LambdaPackagesBucket + Key: !Sub '${ResponseActionsVersion}/delete_volume_snapshots.zip' + # CloudWatch Log Groups (Regional Resources) QuarantineUserLogGroup: Type: AWS::Logs::LogGroup @@ -147,15 +324,17 @@ Resources: QuarantineUserFunction: Type: AWS::Lambda::Function Condition: CreateQuarantineUserResources - DependsOn: QuarantineUserLogGroup + DependsOn: + - QuarantineUserLogGroup + - QuarantineUserPackage Properties: FunctionName: !Sub '${ResourceName}-quarantine-user' Runtime: python3.12 Handler: app.index.handler Role: !Ref QuarantineUserRoleArn Code: - S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' - S3Key: !Sub '${CloudLambdasPath}/quarantine_user.zip' + S3Bucket: !Ref LambdaPackagesBucket + S3Key: !Sub '${ResponseActionsVersion}/quarantine_user.zip' Timeout: 300 MemorySize: 128 Environment: @@ -173,15 +352,17 @@ Resources: FetchCloudLogsFunction: Type: AWS::Lambda::Function Condition: CreateFetchCloudLogsResources - DependsOn: FetchCloudLogsLogGroup + DependsOn: + - FetchCloudLogsLogGroup + - FetchCloudLogsPackage Properties: FunctionName: !Sub '${ResourceName}-fetch-cloud-logs' Runtime: python3.12 Handler: app.index.handler Role: !Ref FetchCloudLogsRoleArn Code: - S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' - S3Key: !Sub '${CloudLambdasPath}/fetch_cloud_logs.zip' + S3Bucket: !Ref LambdaPackagesBucket + S3Key: !Sub '${ResponseActionsVersion}/fetch_cloud_logs.zip' Timeout: 300 MemorySize: 128 Environment: @@ -199,15 +380,17 @@ Resources: RemovePolicyFunction: Type: AWS::Lambda::Function Condition: CreateQuarantineUserResources - DependsOn: RemovePolicyLogGroup + DependsOn: + - RemovePolicyLogGroup + - RemovePolicyPackage Properties: FunctionName: !Sub '${ResourceName}-remove-policy' Runtime: python3.12 Handler: app.index.handler Role: !Ref RemovePolicyRoleArn Code: - S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' - S3Key: !Sub '${CloudLambdasPath}/remove_policy.zip' + S3Bucket: !Ref LambdaPackagesBucket + S3Key: !Sub '${ResponseActionsVersion}/remove_policy.zip' Timeout: 300 MemorySize: 128 Environment: @@ -225,15 +408,17 @@ Resources: ConfigureResourceAccessFunction: Type: AWS::Lambda::Function Condition: CreateMakePrivateResources - DependsOn: ConfigureResourceAccessLogGroup + DependsOn: + - ConfigureResourceAccessLogGroup + - ConfigureResourceAccessPackage Properties: FunctionName: !Sub '${ResourceName}-configure-resource-access' Runtime: python3.12 Handler: app.index.handler Role: !Ref ConfigureResourceAccessRoleArn Code: - S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' - S3Key: !Sub '${CloudLambdasPath}/configure_resource_access.zip' + S3Bucket: !Ref LambdaPackagesBucket + S3Key: !Sub '${ResponseActionsVersion}/configure_resource_access.zip' Timeout: 300 MemorySize: 128 Environment: @@ -251,15 +436,17 @@ Resources: CreateVolumeSnapshotsFunction: Type: AWS::Lambda::Function Condition: CreateVolumeSnapshotResources - DependsOn: CreateVolumeSnapshotsLogGroup + DependsOn: + - CreateVolumeSnapshotsLogGroup + - CreateVolumeSnapshotsPackage Properties: FunctionName: !Sub '${ResourceName}-create-volume-snapshots' Runtime: python3.12 Handler: app.index.handler Role: !Ref CreateVolumeSnapshotsRoleArn Code: - S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' - S3Key: !Sub '${CloudLambdasPath}/create_volume_snapshots.zip' + S3Bucket: !Ref LambdaPackagesBucket + S3Key: !Sub '${ResponseActionsVersion}/create_volume_snapshots.zip' Timeout: 300 MemorySize: 128 Environment: @@ -277,15 +464,17 @@ Resources: DeleteVolumeSnapshotsFunction: Type: AWS::Lambda::Function Condition: CreateVolumeSnapshotResources - DependsOn: DeleteVolumeSnapshotsLogGroup + DependsOn: + - DeleteVolumeSnapshotsLogGroup + - DeleteVolumeSnapshotsPackage Properties: FunctionName: !Sub '${ResourceName}-delete-volume-snapshots' Runtime: python3.12 Handler: app.index.handler Role: !Ref DeleteVolumeSnapshotsRoleArn Code: - S3Bucket: !Sub '${S3BucketPrefix}-${AWS::Region}' - S3Key: !Sub '${CloudLambdasPath}/delete_volume_snapshots.zip' + S3Bucket: !Ref LambdaPackagesBucket + S3Key: !Sub '${ResponseActionsVersion}/delete_volume_snapshots.zip' Timeout: 300 MemorySize: 128 Environment: diff --git a/modules/response-actions/variables.tf b/modules/response-actions/variables.tf index 53379a0..07dca5c 100644 --- a/modules/response-actions/variables.tf +++ b/modules/response-actions/variables.tf @@ -116,15 +116,10 @@ variable "response_actions_version" { default = "0.0.15" } -variable "s3_bucket_prefix" { - description = "Prefix for regional S3 buckets containing Lambda deployment packages. The bucket name will be constructed as {prefix}-{region}. These buckets should exist in each target region and contain the Lambda zip files." +variable "lambda_packages_base_url" { + description = "Base URL for downloading Lambda deployment packages (e.g., https://example.com/packages). The module will automatically create regional S3 buckets and download the packages." type = string -} - -variable "cloud_lambdas_path" { - description = "The path where lambda code resides inside of regional S3 buckets" - type = string - default = "response-actions/cloud-lambdas" + default = "https://download.sysdig.com/cloud-response-actions" } variable "enabled_response_actions" { From b475686f45c9f782ab7737c6abc88416fa0fef6e Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Wed, 10 Dec 2025 17:06:16 +0100 Subject: [PATCH 20/26] Fix indent --- modules/response-actions/locals.tf | 6 +-- modules/response-actions/main.tf | 58 +++++++++++----------- modules/response-actions/organizational.tf | 39 +++++++-------- modules/response-actions/variables.tf | 8 +-- 4 files changed, 52 insertions(+), 59 deletions(-) diff --git a/modules/response-actions/locals.tf b/modules/response-actions/locals.tf index e2bfd17..f0f893a 100644 --- a/modules/response-actions/locals.tf +++ b/modules/response-actions/locals.tf @@ -172,8 +172,8 @@ locals { "MIXED" # case5 - if no explicit include/exclude, we still need to exclude current account ) : var.is_organizational ? ( - "DIFFERENCE" - ) : "" + "DIFFERENCE" + ) : "" ) ) ) @@ -182,7 +182,7 @@ locals { ou_accounts_to_exclude = flatten([for ou_accounts in data.aws_organizations_organizational_unit_descendant_accounts.ou_accounts_to_exclude : [ou_accounts.accounts[*].id]]) # Always exclude the current account (management account) from StackSet deployment since the Lambda execution roles # already exist in the management account. Delegate roles are only needed in member accounts. - accounts_to_exclude = setunion(local.ou_accounts_to_exclude, var.exclude_accounts, [data.aws_caller_identity.current.account_id]) + accounts_to_exclude = setunion(local.ou_accounts_to_exclude, var.exclude_accounts, [data.aws_caller_identity.current.account_id]) # switch cases for various user provided accounts configuration to be onboarded deployment_account_options = { diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index cd9c24e..5fc7790 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -139,8 +139,8 @@ resource "aws_iam_role" "lambda_stackset_admin_role" { }) tags = { - Name = "${local.ra_resource_name}-stackset-admin" - "sysdig.com/response-actions/resource-name" = "stackset-admin" + Name = "${local.ra_resource_name}-stackset-admin" + "sysdig.com/response-actions/resource-name" = "stackset-admin" } } @@ -187,8 +187,8 @@ resource "aws_iam_role" "lambda_stackset_execution_role" { }) tags = { - Name = "${local.ra_resource_name}-stackset-execution" - "sysdig.com/response-actions/resource-name" = "stackset-execution" + Name = "${local.ra_resource_name}-stackset-execution" + "sysdig.com/response-actions/resource-name" = "stackset-execution" } } @@ -284,8 +284,8 @@ resource "aws_iam_role" "shared_cross_account_lambda_invoker" { }) tags = { - Name = "${local.ra_resource_name}-cross-account-invoker" - "sysdig.com/response-actions/resource-name" = "cross-account-invoker" + Name = "${local.ra_resource_name}-cross-account-invoker" + "sysdig.com/response-actions/resource-name" = "cross-account-invoker" } } @@ -358,9 +358,9 @@ resource "aws_iam_role" "quarantine_user_role" { }) tags = { - Name = local.quarantine_user_role_name - "sysdig.com/response-actions/cloud-actions" = "true" - "sysdig.com/response-actions/resource-name" = "quarantine-user-role" + Name = local.quarantine_user_role_name + "sysdig.com/response-actions/cloud-actions" = "true" + "sysdig.com/response-actions/resource-name" = "quarantine-user-role" } } @@ -396,9 +396,9 @@ resource "aws_iam_role" "fetch_cloud_logs_role" { }) tags = { - Name = local.fetch_cloud_logs_role_name - "sysdig.com/response-actions/cloud-actions" = "true" - "sysdig.com/response-actions/resource-name" = "fetch-cloud-logs-role" + Name = local.fetch_cloud_logs_role_name + "sysdig.com/response-actions/cloud-actions" = "true" + "sysdig.com/response-actions/resource-name" = "fetch-cloud-logs-role" } } @@ -434,9 +434,9 @@ resource "aws_iam_role" "remove_policy_role" { }) tags = { - Name = local.remove_policy_role_name - "sysdig.com/response-actions/cloud-actions" = "true" - "sysdig.com/response-actions/resource-name" = "remove-policy-role" + Name = local.remove_policy_role_name + "sysdig.com/response-actions/cloud-actions" = "true" + "sysdig.com/response-actions/resource-name" = "remove-policy-role" } } @@ -472,9 +472,9 @@ resource "aws_iam_role" "configure_resource_access_role" { }) tags = { - Name = local.configure_resource_access_role_name - "sysdig.com/response-actions/cloud-actions" = "true" - "sysdig.com/response-actions/resource-name" = "configure-resource-access-role" + Name = local.configure_resource_access_role_name + "sysdig.com/response-actions/cloud-actions" = "true" + "sysdig.com/response-actions/resource-name" = "configure-resource-access-role" } } @@ -510,9 +510,9 @@ resource "aws_iam_role" "create_volume_snapshots_role" { }) tags = { - Name = local.create_volume_snapshots_role_name - "sysdig.com/response-actions/cloud-actions" = "true" - "sysdig.com/response-actions/resource-name" = "create-volume-snapshots-role" + Name = local.create_volume_snapshots_role_name + "sysdig.com/response-actions/cloud-actions" = "true" + "sysdig.com/response-actions/resource-name" = "create-volume-snapshots-role" } } @@ -548,9 +548,9 @@ resource "aws_iam_role" "delete_volume_snapshots_role" { }) tags = { - Name = local.delete_volume_snapshots_role_name - "sysdig.com/response-actions/cloud-actions" = "true" - "sysdig.com/response-actions/resource-name" = "delete-volume-snapshots-role" + Name = local.delete_volume_snapshots_role_name + "sysdig.com/response-actions/cloud-actions" = "true" + "sysdig.com/response-actions/resource-name" = "delete-volume-snapshots-role" } } @@ -585,8 +585,8 @@ resource "aws_iam_role" "package_downloader_role" { }) tags = { - Name = "${local.ra_resource_name}-package-downloader-role" - "sysdig.com/response-actions/resource-name" = "package-downloader-role" + Name = "${local.ra_resource_name}-package-downloader-role" + "sysdig.com/response-actions/resource-name" = "package-downloader-role" } } @@ -667,11 +667,11 @@ resource "time_sleep" "wait_for_iam_role_propagation" { # with self-managed permissions. #----------------------------------------------------------------------------------------------------------------------------------------- resource "aws_cloudformation_stack_set" "lambda_functions" { - name = "${local.ra_resource_name}-lambda" - tags = merge(var.tags, { + name = "${local.ra_resource_name}-lambda" + tags = merge(var.tags, { "sysdig.com/response-actions/resource-name" = "lambda-stackset" }) - permission_model = "SELF_MANAGED" + permission_model = "SELF_MANAGED" capabilities = ["CAPABILITY_NAMED_IAM"] administration_role_arn = local.administration_role_arn execution_role_name = local.execution_role_name diff --git a/modules/response-actions/organizational.tf b/modules/response-actions/organizational.tf index aeaedf5..1c39c4f 100644 --- a/modules/response-actions/organizational.tf +++ b/modules/response-actions/organizational.tf @@ -16,8 +16,8 @@ resource "aws_cloudformation_stack_set" "ra_delegate_roles" { count = var.is_organizational ? 1 : 0 - name = join("-", [local.ra_resource_name, "delegate-roles"]) - tags = merge(var.tags, { + name = join("-", [local.ra_resource_name, "delegate-roles"]) + tags = merge(var.tags, { "sysdig.com/response-actions/resource-name" = "delegate-roles-stackset" }) permission_model = "SERVICE_MANAGED" @@ -37,23 +37,23 @@ resource "aws_cloudformation_stack_set" "ra_delegate_roles" { } parameters = { - TemplateVersion = md5(file("${path.module}/templates/delegate_roles_stackset.tpl")) - QuarantineUserLambdaRoleArn = local.enable_quarantine_user ? aws_iam_role.quarantine_user_role[0].arn : "" - QuarantineUserRoleName = local.enable_quarantine_user ? aws_iam_role.quarantine_user_role[0].name : "" - FetchCloudLogsLambdaRoleArn = local.enable_fetch_cloud_logs ? aws_iam_role.fetch_cloud_logs_role[0].arn : "" - FetchCloudLogsRoleName = local.enable_fetch_cloud_logs ? aws_iam_role.fetch_cloud_logs_role[0].name : "" - RemovePolicyLambdaRoleArn = local.enable_quarantine_user ? aws_iam_role.remove_policy_role[0].arn : "" - RemovePolicyRoleName = local.enable_quarantine_user ? aws_iam_role.remove_policy_role[0].name : "" - ConfigureResourceAccessLambdaRoleArn = local.enable_make_private ? aws_iam_role.configure_resource_access_role[0].arn : "" - ConfigureResourceAccessRoleName = local.enable_make_private ? aws_iam_role.configure_resource_access_role[0].name : "" - CreateVolumeSnapshotsLambdaRoleArn = local.enable_create_volume_snapshot ? aws_iam_role.create_volume_snapshots_role[0].arn : "" - CreateVolumeSnapshotsRoleName = local.enable_create_volume_snapshot ? aws_iam_role.create_volume_snapshots_role[0].name : "" - DeleteVolumeSnapshotsLambdaRoleArn = local.enable_create_volume_snapshot ? aws_iam_role.delete_volume_snapshots_role[0].arn : "" - DeleteVolumeSnapshotsRoleName = local.enable_create_volume_snapshot ? aws_iam_role.delete_volume_snapshots_role[0].name : "" - EnableQuarantineUser = local.enable_quarantine_user ? "true" : "false" - EnableFetchCloudLogs = local.enable_fetch_cloud_logs ? "true" : "false" - EnableMakePrivate = local.enable_make_private ? "true" : "false" - EnableCreateVolumeSnapshot = local.enable_create_volume_snapshot ? "true" : "false" + TemplateVersion = md5(file("${path.module}/templates/delegate_roles_stackset.tpl")) + QuarantineUserLambdaRoleArn = local.enable_quarantine_user ? aws_iam_role.quarantine_user_role[0].arn : "" + QuarantineUserRoleName = local.enable_quarantine_user ? aws_iam_role.quarantine_user_role[0].name : "" + FetchCloudLogsLambdaRoleArn = local.enable_fetch_cloud_logs ? aws_iam_role.fetch_cloud_logs_role[0].arn : "" + FetchCloudLogsRoleName = local.enable_fetch_cloud_logs ? aws_iam_role.fetch_cloud_logs_role[0].name : "" + RemovePolicyLambdaRoleArn = local.enable_quarantine_user ? aws_iam_role.remove_policy_role[0].arn : "" + RemovePolicyRoleName = local.enable_quarantine_user ? aws_iam_role.remove_policy_role[0].name : "" + ConfigureResourceAccessLambdaRoleArn = local.enable_make_private ? aws_iam_role.configure_resource_access_role[0].arn : "" + ConfigureResourceAccessRoleName = local.enable_make_private ? aws_iam_role.configure_resource_access_role[0].name : "" + CreateVolumeSnapshotsLambdaRoleArn = local.enable_create_volume_snapshot ? aws_iam_role.create_volume_snapshots_role[0].arn : "" + CreateVolumeSnapshotsRoleName = local.enable_create_volume_snapshot ? aws_iam_role.create_volume_snapshots_role[0].name : "" + DeleteVolumeSnapshotsLambdaRoleArn = local.enable_create_volume_snapshot ? aws_iam_role.delete_volume_snapshots_role[0].arn : "" + DeleteVolumeSnapshotsRoleName = local.enable_create_volume_snapshot ? aws_iam_role.delete_volume_snapshots_role[0].name : "" + EnableQuarantineUser = local.enable_quarantine_user ? "true" : "false" + EnableFetchCloudLogs = local.enable_fetch_cloud_logs ? "true" : "false" + EnableMakePrivate = local.enable_make_private ? "true" : "false" + EnableCreateVolumeSnapshot = local.enable_create_volume_snapshot ? "true" : "false" } template_body = file("${path.module}/templates/delegate_roles_stackset.tpl") @@ -104,4 +104,3 @@ resource "aws_cloudformation_stack_set_instance" "ra_delegate_roles" { delete = var.timeout } } - diff --git a/modules/response-actions/variables.tf b/modules/response-actions/variables.tf index 07dca5c..01d7062 100644 --- a/modules/response-actions/variables.tf +++ b/modules/response-actions/variables.tf @@ -99,12 +99,6 @@ variable "exclude_accounts" { default = [] } -variable "api_dest_rate_limit" { - type = number - default = 300 - description = "Rate limit for API Destinations" -} - variable "api_base_url" { description = "Base URL for the API service" type = string @@ -113,7 +107,7 @@ variable "api_base_url" { variable "response_actions_version" { description = "Response Actions version" type = string - default = "0.0.15" + default = "0.0.16" } variable "lambda_packages_base_url" { From 178f0cead4d56d68f8d5023ac9a6d36d2535887c Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Wed, 10 Dec 2025 17:11:07 +0100 Subject: [PATCH 21/26] Fixed. --- modules/response-actions/main.tf | 4 ++-- .../templates/{lambda-stackset.yaml => lambda-stackset.tpl} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename modules/response-actions/templates/{lambda-stackset.yaml => lambda-stackset.tpl} (100%) diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index 5fc7790..40f51ea 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -686,7 +686,7 @@ resource "aws_cloudformation_stack_set" "lambda_functions" { parameters = { ResourceName = local.ra_resource_name - TemplateVersion = md5(file("${path.module}/templates/lambda-stackset.yaml")) + TemplateVersion = md5(file("${path.module}/templates/lambda-stackset.tpl")) S3BucketName = local.s3_bucket_name ApiBaseUrl = var.api_base_url PackageDownloaderRoleArn = aws_iam_role.package_downloader_role.arn @@ -710,7 +710,7 @@ resource "aws_cloudformation_stack_set" "lambda_functions" { EnableCreateVolumeSnapshot = local.enable_create_volume_snapshot ? "true" : "false" } - template_body = file("${path.module}/templates/lambda-stackset.yaml") + template_body = file("${path.module}/templates/lambda-stackset.tpl") depends_on = [ aws_iam_role.lambda_stackset_admin_role, diff --git a/modules/response-actions/templates/lambda-stackset.yaml b/modules/response-actions/templates/lambda-stackset.tpl similarity index 100% rename from modules/response-actions/templates/lambda-stackset.yaml rename to modules/response-actions/templates/lambda-stackset.tpl From 479952bd3c5c8468c3a439150000ec6cabe35040 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Fri, 12 Dec 2025 09:45:13 +0100 Subject: [PATCH 22/26] Set new terraform plugin --- modules/response-actions/versions.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/response-actions/versions.tf b/modules/response-actions/versions.tf index f8151c7..4764e74 100644 --- a/modules/response-actions/versions.tf +++ b/modules/response-actions/versions.tf @@ -10,8 +10,8 @@ terraform { version = ">= 3.1" } sysdig = { - source = "local/sysdiglabs/sysdig" - version = "~> 1.0.0" + source = "sysdiglabs/sysdig" + version = "~> 3.3" } } } From c26ad28c6cd821c155e922bbc00db8e10ac63554 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Fri, 12 Dec 2025 10:06:43 +0100 Subject: [PATCH 23/26] Fixed readme --- modules/response-actions/README.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/modules/response-actions/README.md b/modules/response-actions/README.md index 208f17c..e374330 100644 --- a/modules/response-actions/README.md +++ b/modules/response-actions/README.md @@ -1,21 +1,3 @@ -Add the following bucket policy in Sysdig S3 storage: -``` -{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "PublicReadGetObject", - "Effect": "Allow", - "Principal": "*", - "Action": "s3:GetObject", - "Resource": "arn:aws:s3:::/response-actions/cloud-lambdas/*" - } - ] -} -``` - -and disable block public access. - For testing, create `provisioning.tf`, with: ``` From 372418978e1f19481310842a36ee22554aafb557 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Fri, 12 Dec 2025 12:52:39 +0100 Subject: [PATCH 24/26] Some cleanup --- modules/response-actions/locals.tf | 94 ++++++---------------- modules/response-actions/main.tf | 5 ++ modules/response-actions/organizational.tf | 4 +- modules/response-actions/variables.tf | 10 --- 4 files changed, 32 insertions(+), 81 deletions(-) diff --git a/modules/response-actions/locals.tf b/modules/response-actions/locals.tf index f0f893a..e81b12e 100644 --- a/modules/response-actions/locals.tf +++ b/modules/response-actions/locals.tf @@ -7,7 +7,6 @@ # The module supports flexible inclusion/exclusion patterns: # - include_ouids/exclude_ouids: Include or exclude entire organizational units # - include_accounts/exclude_accounts: Include or exclude specific accounts -# - Legacy org_units parameter: Deprecated, will be removed November 30, 2025 # # Key behavior: # 1. Inclusions are always prioritized over exclusions @@ -26,41 +25,11 @@ data "aws_organizations_organization" "org" { } locals { - # check if both old and new org parameters are provided, we fail early - both_org_configuration_params = var.is_organizational && length(var.org_units) > 0 && ( - length(var.include_ouids) > 0 || - length(var.exclude_ouids) > 0 || - length(var.include_accounts) > 0 || - length(var.exclude_accounts) > 0 - ) - - # check if old org_units parameter is provided, for backwards compatibility we will always give preference to it - check_old_ouid_param = var.is_organizational && length(var.org_units) > 0 - # fetch the AWS Root OU under org # As per https://docs.aws.amazon.com/organizations/latest/userguide/orgs_getting-started_concepts.html#organization-structure, there can be only one root root_org_unit = var.is_organizational ? [for root in data.aws_organizations_organization.org[0].roots : root.id] : [] } -check "validate_org_configuration_params" { - assert { - condition = length(var.org_units) == 0 # if this condition is false we throw warning - error_message = <<-EOT - WARNING: TO BE DEPRECATED 'org_units' on 30th November, 2025. Please work with Sysdig to migrate your Terraform installs to use 'include_ouids' instead. - EOT - } - - assert { - condition = !local.both_org_configuration_params # if this condition is false we throw error - error_message = <<-EOT - ERROR: If both org_units and include_ouids/exclude_ouids/include_accounts/exclude_accounts variables are populated, - ONLY org_units will be considered. Please use only one of the two methods. - - Note: org_units is going to be DEPRECATED on 30th November, 2025. Please work with Sysdig to migrate your Terraform installs. - EOT - } -} - # ***************************************************************************************************************************************************** # INCLUDE/EXCLUDE CONFIGURATION SUPPORT # @@ -86,27 +55,22 @@ check "validate_org_configuration_params" { locals { # OU CONFIGURATION (determine user provided org configuration) org_configuration = ( - # case1 - if old method is used where ONLY org_units is provided, use those - local.check_old_ouid_param ? ( - "old_ouid_param" + # case1 - if no include/exclude ous provided, include entire org + var.is_organizational && length(var.include_ouids) == 0 && length(var.exclude_ouids) == 0 ? ( + "entire_org" ) : ( - # case2 - if no include/exclude ous provided, include entire org - var.is_organizational && length(var.include_ouids) == 0 && length(var.exclude_ouids) == 0 ? ( - "entire_org" + # case2 - if only included ouids provided, include those ous only + var.is_organizational && length(var.include_ouids) > 0 && length(var.exclude_ouids) == 0 ? ( + "included_ous_only" ) : ( - # case3 - if only included ouids provided, include those ous only - var.is_organizational && length(var.include_ouids) > 0 && length(var.exclude_ouids) == 0 ? ( - "included_ous_only" + # case3 - if only excluded ouids provided, exclude their accounts from rest of org + var.is_organizational && length(var.include_ouids) == 0 && length(var.exclude_ouids) > 0 ? ( + "excluded_ous_only" ) : ( - # case4 - if only excluded ouids provided, exclude their accounts from rest of org - var.is_organizational && length(var.include_ouids) == 0 && length(var.exclude_ouids) > 0 ? ( - "excluded_ous_only" - ) : ( - # case5 - if both include and exclude ouids are provided, includes override excludes - var.is_organizational && length(var.include_ouids) > 0 && length(var.exclude_ouids) > 0 ? ( - "mixed_ous" - ) : "" - ) + # case4 - if both include and exclude ouids are provided, includes override excludes + var.is_organizational && length(var.include_ouids) > 0 && length(var.exclude_ouids) > 0 ? ( + "mixed_ous" + ) : "" ) ) ) @@ -114,9 +78,6 @@ locals { # switch cases for various user provided org configuration to be onboarded deployment_options = { - old_ouid_param = { - org_units_to_deploy = var.org_units - } entire_org = { org_units_to_deploy = local.root_org_unit } @@ -155,26 +116,21 @@ data "aws_organizations_organizational_unit_descendant_accounts" "ou_accounts_to locals { # ACCOUNTS CONFIGURATION (determine user provided accounts configuration) accounts_configuration = ( - # case1 - if old method is used where ONLY org_units is provided, this configuration is a noop - local.check_old_ouid_param ? ( - "NONE" + # case1 - if only included accounts provided, include those accts as well + var.is_organizational && length(var.include_accounts) > 0 && length(var.exclude_accounts) == 0 ? ( + "UNION" ) : ( - # case2 - if only included accounts provided, include those accts as well - var.is_organizational && length(var.include_accounts) > 0 && length(var.exclude_accounts) == 0 ? ( - "UNION" + # case2 - if only excluded accounts or only excluded ouids provided, exclude those accounts + var.is_organizational && length(var.include_accounts) == 0 && (length(var.exclude_accounts) > 0 || local.org_configuration == "excluded_ous_only") ? ( + "DIFFERENCE" ) : ( - # case3 - if only excluded accounts or only excluded ouids provided, exclude those accounts - var.is_organizational && length(var.include_accounts) == 0 && (length(var.exclude_accounts) > 0 || local.org_configuration == "excluded_ous_only") ? ( + # case3 - if both include and exclude accounts are provided, includes override excludes + var.is_organizational && length(var.include_accounts) > 0 && length(var.exclude_accounts) > 0 ? ( + "MIXED" + # case4 - if no explicit include/exclude, we still need to exclude current account + ) : var.is_organizational ? ( "DIFFERENCE" - ) : ( - # case4 - if both include and exclude accounts are provided, includes override excludes - var.is_organizational && length(var.include_accounts) > 0 && length(var.exclude_accounts) > 0 ? ( - "MIXED" - # case5 - if no explicit include/exclude, we still need to exclude current account - ) : var.is_organizational ? ( - "DIFFERENCE" - ) : "" - ) + ) : "" ) ) ) diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index 40f51ea..196e810 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -743,4 +743,9 @@ resource "aws_cloudformation_stack_set_instance" "lambda_functions" { update = var.timeout delete = var.timeout } + + depends_on = [ + aws_iam_role_policy.lambda_stackset_execution_policy, + aws_iam_role_policy_attachment.lambda_stackset_execution_cloudformation + ] } diff --git a/modules/response-actions/organizational.tf b/modules/response-actions/organizational.tf index 1c39c4f..f38a89f 100644 --- a/modules/response-actions/organizational.tf +++ b/modules/response-actions/organizational.tf @@ -88,8 +88,8 @@ resource "aws_cloudformation_stack_set_instance" "ra_delegate_roles" { deployment_targets { organizational_unit_ids = [each.value] - accounts = local.check_old_ouid_param ? null : (local.deployment_targets_accounts_filter == "NONE" ? null : local.deployment_targets_accounts.accounts_to_deploy) - account_filter_type = local.check_old_ouid_param ? null : local.deployment_targets_accounts_filter + accounts = local.deployment_targets_accounts_filter == "NONE" ? null : local.deployment_targets_accounts.accounts_to_deploy + account_filter_type = local.deployment_targets_accounts_filter } operation_preferences { max_concurrent_percentage = 100 diff --git a/modules/response-actions/variables.tf b/modules/response-actions/variables.tf index 01d7062..9484db5 100644 --- a/modules/response-actions/variables.tf +++ b/modules/response-actions/variables.tf @@ -4,16 +4,6 @@ variable "is_organizational" { default = false } -variable "org_units" { - description = <<-EOF - TO BE DEPRECATED on 30th November, 2025: Please work with Sysdig to migrate to using `include_ouids` instead. - When set, list of Organization Unit IDs in which to setup EventBridge. By default, EventBridge will be setup in all accounts within the Organization. - This field is ignored if `is_organizational = false` - EOF - type = set(string) - default = [] -} - variable "regions" { description = "(Optional) List of regions in which to setup Response Actions lambdas. By default, current region is selected" type = set(string) From ee78421cbd2c7b03c72e4d85f223b0dcf2741138 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Fri, 12 Dec 2025 15:03:04 +0100 Subject: [PATCH 25/26] Removed versioning and normalizing bucket names --- modules/response-actions/main.tf | 4 +--- modules/response-actions/templates/lambda-stackset.tpl | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/response-actions/main.tf b/modules/response-actions/main.tf index 196e810..d114db9 100644 --- a/modules/response-actions/main.tf +++ b/modules/response-actions/main.tf @@ -60,7 +60,7 @@ locals { execution_role_name = var.auto_create_stackset_roles ? aws_iam_role.lambda_stackset_execution_role[0].name : var.stackset_execution_role_name # S3 bucket configuration for Lambda packages - s3_bucket_name = "${var.name}-${random_id.suffix.hex}-packages" + s3_bucket_name = "${local.ra_resource_name}-packages" # Response action enablement flags enable_make_private = contains(var.enabled_response_actions, "make_private") @@ -226,8 +226,6 @@ resource "aws_iam_role_policy" "lambda_stackset_execution_policy" { "s3:CreateBucket", "s3:DeleteBucket", "s3:GetBucketLocation", - "s3:GetBucketVersioning", - "s3:PutBucketVersioning", "s3:PutBucketPublicAccessBlock", "s3:GetBucketPublicAccessBlock", "s3:PutBucketTagging", diff --git a/modules/response-actions/templates/lambda-stackset.tpl b/modules/response-actions/templates/lambda-stackset.tpl index cbadde3..7df7a42 100644 --- a/modules/response-actions/templates/lambda-stackset.tpl +++ b/modules/response-actions/templates/lambda-stackset.tpl @@ -98,8 +98,6 @@ Resources: BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true - VersioningConfiguration: - Status: Enabled Tags: - Key: 'sysdig.com/response-actions/resource-name' Value: 'lambda-packages-bucket' From 70436ba47fde57469414f82ae75f7f1f1ed38936 Mon Sep 17 00:00:00 2001 From: Mirko Bonasorte Date: Fri, 12 Dec 2025 15:50:19 +0100 Subject: [PATCH 26/26] Aligning terraform plugin --- modules/config-posture/versions.tf | 2 +- modules/integrations/cloud-logs/versions.tf | 2 +- modules/integrations/cross-account-event-bridge/versions.tf | 2 +- modules/integrations/event-bridge/versions.tf | 2 +- modules/onboarding/versions.tf | 2 +- modules/vm-workload-scanning/versions.tf | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/config-posture/versions.tf b/modules/config-posture/versions.tf index 48ddb05..16d9ab5 100644 --- a/modules/config-posture/versions.tf +++ b/modules/config-posture/versions.tf @@ -7,7 +7,7 @@ terraform { } sysdig = { source = "sysdiglabs/sysdig" - version = "~> 1.48" + version = "~> 3.3" } } } diff --git a/modules/integrations/cloud-logs/versions.tf b/modules/integrations/cloud-logs/versions.tf index 7bb98df..51e686b 100644 --- a/modules/integrations/cloud-logs/versions.tf +++ b/modules/integrations/cloud-logs/versions.tf @@ -9,7 +9,7 @@ terraform { } sysdig = { source = "sysdiglabs/sysdig" - version = "~> 1.56" + version = "~> 3.3" } random = { source = "hashicorp/random" diff --git a/modules/integrations/cross-account-event-bridge/versions.tf b/modules/integrations/cross-account-event-bridge/versions.tf index 09c0911..6456bf0 100644 --- a/modules/integrations/cross-account-event-bridge/versions.tf +++ b/modules/integrations/cross-account-event-bridge/versions.tf @@ -7,7 +7,7 @@ terraform { } sysdig = { source = "sysdiglabs/sysdig" - version = "~> 1.48" + version = "~> 3.3" } random = { source = "hashicorp/random" diff --git a/modules/integrations/event-bridge/versions.tf b/modules/integrations/event-bridge/versions.tf index b543f52..6456bf0 100644 --- a/modules/integrations/event-bridge/versions.tf +++ b/modules/integrations/event-bridge/versions.tf @@ -7,7 +7,7 @@ terraform { } sysdig = { source = "sysdiglabs/sysdig" - version = "~> 1.51" + version = "~> 3.3" } random = { source = "hashicorp/random" diff --git a/modules/onboarding/versions.tf b/modules/onboarding/versions.tf index db226f1..4764e74 100644 --- a/modules/onboarding/versions.tf +++ b/modules/onboarding/versions.tf @@ -11,7 +11,7 @@ terraform { } sysdig = { source = "sysdiglabs/sysdig" - version = "~> 1.48" + version = "~> 3.3" } } } diff --git a/modules/vm-workload-scanning/versions.tf b/modules/vm-workload-scanning/versions.tf index b9584db..046f6e4 100644 --- a/modules/vm-workload-scanning/versions.tf +++ b/modules/vm-workload-scanning/versions.tf @@ -8,7 +8,7 @@ terraform { } sysdig = { source = "sysdiglabs/sysdig" - version = "~> 1.48" + version = "~> 3.3" } } }