diff --git a/aws_sra_examples/solutions/ami_bakery/ami_bakery_org/lambda/src/sra-ami-bakery-org-ubuntu-pro-20-04-cis-level-1-hardened.yaml b/aws_sra_examples/solutions/ami_bakery/ami_bakery_org/lambda/src/sra-ami-bakery-org-ubuntu-pro-20-04-cis-level-1-hardened.yaml index a2df9128..0f2e3818 100644 --- a/aws_sra_examples/solutions/ami_bakery/ami_bakery_org/lambda/src/sra-ami-bakery-org-ubuntu-pro-20-04-cis-level-1-hardened.yaml +++ b/aws_sra_examples/solutions/ami_bakery/ami_bakery_org/lambda/src/sra-ami-bakery-org-ubuntu-pro-20-04-cis-level-1-hardened.yaml @@ -74,7 +74,7 @@ Parameters: Type: String pSRAAMIBakeryImageBuilderRoleName: AllowedPattern: ^[\w_+=,.@-]{1,64}$ - Default: sra-ami-bakery-org-ec2-imagebuilder-role + Default: "sra-ami-bakery-org-ec2-imagebuilder-role" ConstraintDescription: Must be a string of characters consisting of upper and lowercase alphanumeric characters up to 64 with including [_+=,.@-], but no spaces. Description: The SRA AMI Bakery Role name for Ubuntu Pro CIS Level 1 hardened image. diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/README.md b/aws_sra_examples/solutions/security_lake/security_lake_org/README.md new file mode 100644 index 00000000..0d8cb1ef --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/README.md @@ -0,0 +1,208 @@ +# Security Lake Organization + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: CC-BY-SA-4.0 + +## Table of Contents + +- [Table of Contents](#table-of-contents) +- [Introduction](#introduction) +- [Deployed Resource Details](#deployed-resource-details) +- [Implementation Instructions](#implementation-instructions) +- [References](#references) + +--- + +## Introduction + +AWS SRA Security Lake solution will automate enabling Amazon Security Lake by delegating administration to a Log Archive account and configuring Amazon Security Lake for all existing and future AWS Organization accounts. + +**Key solution features:** + +- Delegates the administration of Amazon Security Lake to a Log Archive account in the Security OU (Organizational Unit). +- Creates the required IAM roles for Amazon Security Lake. +- Configures the ingestion of AWS logs and event sources in all existing or specified accounts. +- Creates an organization configuration to automatically enable Amazon Security Lake for new member accounts in your organization. +- (Optional) Creates an Audit account (Security Tooling) subscriber with data access. +- (Optional) Creates an Audit account (Security Tooling) subscriber with query access. +- (Optional) Creates a resource link to shared tables in an Audit account (Security Tooling). + + +--- + +## Deployed Resource Details + +![Architecture](./documentation/sra-security-lake-org.png) + +### 1.0 Organization Management Account + +#### 1.1 AWS CloudFormation + +- All resources are deployed via AWS CloudFormation as a `StackSet` and `Stack Instance` within the management account or a CloudFormation `Stack` within a specific account. +- The [Customizations for AWS Control Tower](https://aws.amazon.com/solutions/implementations/customizations-for-aws-control-tower/) solution deploys all templates as a CloudFormation `StackSet`. +- For parameter details, review the [AWS CloudFormation templates](templates/). + +#### 1.2 AWS Lambda Function + +- The Lambda function includes logic to enable and configure Security Lake + +#### 1.3 Lambda Execution IAM Role + +- IAM role used by the Lambda function to enable the Security Lake Delegated Administrator Account within each region provided + +#### 1.4 Lambda CloudWatch Log Group + +- All the `AWS Lambda Function` logs are sent to a CloudWatch Log Group `` to help with debugging and traceability of the actions performed. +- By default the `AWS Lambda Function` will create the CloudWatch Log Group and logs are encrypted with a CloudWatch Logs service managed encryption key. + +#### 1.5 Dead Letter Queue (DLQ) + +- SQS dead letter queue used for retaining any failed Lambda events. + +#### 1.6 Alarm SNS Topic + +- SNS Topic used to notify subscribers when messages hit the DLQ. + +#### 1.7 Lambda Layer + +- The python boto3 SDK lambda layer to enable capability for Lambda to enable features of the Security Lake service. +- This is downloaded during the deployment process and packaged into a layer that is used by the Lambda function in this solution. +- The Security Lake API available in the current Lambda environment (as of 09/03/2024) is 1.20.32, however, enhanced functionality of the Security Lake API used in this solution requires at least 1.35.10 (see references below). +- Note: Future revisions to this solution will remove this layer when boto3 is updated within the Lambda environment. + +#### 1.8 Compliance Event Rule + +- The `Organization Compliance Scheduled Event Rule` triggers the `AWS Lambda Function` to capture AWS Account status updates (e.g. suspended to active). + - A parameter is provided to set the schedule frequency. + + +--- + +### 2.0 Log Archive Account(Delegated Administrator) + +#### 2.1 AWS CloudFormation + +- See [1.1 AWS CloudFormation](#11-aws-cloudformation) + +#### 2.2 AmazonSecurityLakeMetaStoreManagerV2 IAM role + +- IAM role used by Security Lake to create data lake or query data from Security Lake. + +#### 2.3 Configuration IAM role + +- The Configuration IAM Role is assumed by the Lambda function to configure Security Lake within the delegated administrator account. + +#### 2.4 Lake Formation service-linked IAM role + +- AWSServiceRoleForLakeFormationDataAccess role provides a set of Amazon Simple Storage Service (Amazon S3) permissions that enable the Lake Formation integrated service (such as Amazon Athena) to access registered locations. + +#### 2.5 KMS key + +- AWS KMS key to encrypt Security Lake data and Security Lake Amazon Simple Queue Service (Amazon SQS) queues. + +#### 2.6 Security Lake + +- Security Lake is enabled in the delegated admin account within each provided region. +- Based on the specified parameters: + - Natively supported AWS log and event sources added in required Regions. + - Organization configuration created to automatically enable Amazon Security Lake for new member accounts in your organization. + - Audit account (Security Tooling) subscriber with data access created. + - Audit account (Security Tooling) subscriber with query access created. + - Resource link to shared tables created in the Audit account (Security Tooling). + +--- + +### 3.0 Audit Account + +The example solutions use `Audit Account` instead of `Security Tooling Account` to align with the default account name used within the AWS Control Tower +setup process for the Security Account. The Account ID for the `Audit Account` SSM parameter is +populated from the `SecurityAccountId` parameter within the `AWSControlTowerBP-BASELINE-CONFIG` StackSet, but is specified manually in other environments, and then stored in an SSM parameter (this is all done in the common prerequisites solution). + +#### 3.1 AWS CloudFormation + +- See [1.1 AWS CloudFormation](#11-aws-cloudformation) + +#### 3.2 Subscriber Configuration IAM role + +- The Subscriber Configuration IAM Role is assumed by the Lambda function to configure resource link to shared tables within the Audit account. + +#### 3.3 AWS RAM resource share + +- The resource share invitation is accepted within the Audit account. + +#### 3.4 AWS Glue resource link + +- A resource link to the shared Lake Formation tables is created in AWS Glue to point the subscriber's account to the shared tables. + +--- + +## Implementation Instructions + +### Prerequisites + +1. [Download and Stage the SRA Solutions](../../../docs/DOWNLOAD-AND-STAGE-SOLUTIONS.md). **Note:** This only needs to be done once for all the solutions. +2. Verify that the [SRA Prerequisites Solution](../../common/common_prerequisites/) has been deployed. +3. Verify that the AmazonSecurityLakeMetaStoreManagerV2 IAM role does not exist in the Log Archive account. If the role exists, either modify the sra-security-lake-org-main-ssm.yaml template or delete the role. +4. Verify that the AWSServiceRoleForLakeFormationDataAccess IAM role does not exist in the Log Archive account. If the role exists, either modify the sra-security-lake-org-main-ssm.yaml template or delete the role. + +### Solution Deployment + +Choose a Deployment Method: + +- [AWS CloudFormation](#aws-cloudformation) +- [Customizations for AWS Control Tower](../../../docs/CFCT-DEPLOYMENT-INSTRUCTIONS.md) + +#### AWS CloudFormation + +In the `management account (home region)`, launch the [sra-security-lake-org-main-ssm.yaml](templates/sra-security-lake-org-main-ssm.yaml) template. This uses an approach where some of the CloudFormation parameters are populated from SSM parameters created by the [SRA Prerequisites Solution](../../common/common_prerequisites/). + + ```bash + aws cloudformation deploy --template-file $PWD/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-main-ssm.yaml --stack-name sra-security-lake-org-main-ssm --capabilities CAPABILITY_NAMED_IAM --parameter-overrides pSecurityLakeWarning= + ``` + +##### Important + +Pay close attention to the `--parameter-overrides` argument. For launching of the AWS Cloudformation stack using the command above to be successful, the `pSecurityLakeWarning` parameter in the `--parameter-overrides` argument must be set to `Accept`. If it is set to `Reject` the stack launch will fail and provide an error. +- To create an Audit account subscriber with data access, add `pRegisterAuditAccountDataSubscriber` parameter in the `--parameter-overrides` with argument set to `true`. Provide value for `pAuditAccountDataSubscriberExternalId` parameter. +- To create an Audit account subscriber with query access, add `pRegisterAuditAccountQuerySubscriber` parameter in the `--parameter-overrides` with argument set to `true`. Provide value for `pAuditAccountQuerySubscriberExternalId` parameter. +- To creates a resource link to shared tables in an Audit account, add `pCreateResourceLink` parameter in the `--parameter-overrides` with argument set to `true` + +#### Verify Solution Deployment + +1. Log into the `Log Archive account` and navigate to the Security Lake page + 1. Select Summary + 2. Verify that Security Lake is enabled for each region + 3. Select Sources + 4. Verify requested sources are enabled for each region and account + 5. To verify that Organization Configuration is ON in each region, run command `aws securitylake get-data-lake-organization-configuration` in the CLI or CloudShell + 6. Select Subscribers + 7. Verify that the Audit account query and/or data access subscribers are created +2. If an Audit account subscriber with query access was created, Log into the `Audit audit` + 1. Navigate to AWS Glue + 2. Select Databases + 3. Verify `amazon_security_lake_glue_db__subscriber` database is created + 4. Select Tables + 5. Verify that resource links to shared tables were created + 6. Navigate to Athena + 7. Create a new query and verify that the query executes successfully. **Note:** The Lake Formation data lake administrator must grant SELECT permissions on the relevant databases and tables to the IAM identity that queries the data. + + +#### Solution Update Instructions + +1. [Download and Stage the SRA Solutions](../../../docs/DOWNLOAD-AND-STAGE-SOLUTIONS.md). **Note:** Get the latest code and run the staging script. +2. Update the existing CloudFormation Stack or CFCT configuration. **Note:** Make sure to update the `SRA Solution Version` parameter and any new added parameters. + +#### Solution Delete Instructions + +1. In the `management account (home region)`, change the `Disable Security Lake log sources and organization configuration` parameter to `true` and update the AWS CloudFormation **Stack** (`sra-security-lake-org-main-ssm`). This will disable the AWS log and event source collection and delete organization configuration in all regions. **Note:** Security Lake will stop collecting logs and events from your AWS sources, but the existing Security Lake settings and the resources that were created in your AWS account, including AmazonSecurityLakeMetaStoreManagerV2, AWSServiceRoleForLakeFormationDataAccess IAM roles and KMS keys, will be retained. Refer to the Amazon Security Lake documentation for the recommended steps to address the service and resources. +2. In the `management account (home region)`, delete the AWS CloudFormation **Stack** (`sra-security-lake-org-main-ssm`). +3. In the `management account (home region)`, delete the AWS CloudWatch **Log Group** (e.g. /aws/lambda/) for the Lambda function deployed. + + +--- + +## References + +- [Amazon Security Lake User Guide](https://docs.aws.amazon.com/security-lake/latest/userguide/what-is-security-lake.html) +- [Managing AWS SDKs in Lambda Functions](https://docs.aws.amazon.com/lambda/latest/operatorguide/sdks-functions.html) +- [Lambda runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) +- [Python Boto3 SDK changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/customizations_for_aws_control_tower/README.md b/aws_sra_examples/solutions/security_lake/security_lake_org/customizations_for_aws_control_tower/README.md new file mode 100644 index 00000000..b8c25d5f --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/customizations_for_aws_control_tower/README.md @@ -0,0 +1,7 @@ +# Customizations for AWS Control Tower + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: CC-BY-SA-4.0 + +--- + +[Customizations for AWS Control Tower Deployment Instructions](../../../../docs/CFCT-DEPLOYMENT-INSTRUCTIONS.md) diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/customizations_for_aws_control_tower/manifest.yaml b/aws_sra_examples/solutions/security_lake/security_lake_org/customizations_for_aws_control_tower/manifest.yaml new file mode 100644 index 00000000..6f9278b5 --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/customizations_for_aws_control_tower/manifest.yaml @@ -0,0 +1,87 @@ +--- +#Default region for deploying Custom Control Tower: Code Pipeline, Step functions, Lambda, SSM parameters, and StackSets +region: us-east-1 +version: 2021-03-15 + +# Control Tower Custom Resources (Service Control Policies or CloudFormation) +resources: + # ----------------------------------------------------------------------------- + # Organization shield + # ----------------------------------------------------------------------------- + - name: sra-security-lake-main-ssm + resource_file: templates/sra-security-lake-main-ssm.yaml + parameters: + - parameter_key: pSecurityLakeOrgLambdaRoleName + parameter_value: sra-security-lake-org-lambda + - parameter_key: pCreateResourceLink + parameter_value: 'false' + - parameter_key: pCreateLakeFormationSlr + parameter_value: 'true' + - parameter_key: pSRASecurityLakeMetaStoreManagerRoleName + parameter_value: AmazonSecurityLakeMetaStoreManagerV2 + - parameter_key: pSourceVersion + parameter_value: '2.0' + - parameter_key: pCloudTrailManagementEvents + parameter_value: ALL + - parameter_key: pCloudTrailLambdaDataEvents + parameter_value: ALL + - parameter_key: pCloudTrailS3DataEvents + parameter_value: '' + - parameter_key: pSecurityHubFindings + parameter_value: ALL + - parameter_key: pVpcFlowLogs + parameter_value: ALL + - parameter_key: pWafLogs + parameter_value: '' + - parameter_key: pRoute53Logs + parameter_value: ALL + - parameter_key: pVpcFlowLogs + parameter_value: ALL + - parameter_key: pOrgConfigurationSources + parameter_value: ROUTE53,VPC_FLOW,SH_FINDINGS,CLOUD_TRAIL_MGMT,LAMBDA_EXECUTION,EKS_AUDIT + - parameter_key: pCreateOrganizationConfiguration + parameter_value: 'true' + - parameter_key: pSecurityLakeOrgKeyAlias + parameter_value: sra-security-lake-org-key + - parameter_key: pComplianceFrequency + parameter_value: 7 + - parameter_key: pControlTowerRegionsOnly + parameter_value: 'true' + - parameter_key: pCreateLambdaLogGroup + parameter_value: 'false' + - parameter_key: pEnabledRegions + parameter_value: '' + - parameter_key: pLambdaLogGroupKmsKey + parameter_value: '' + - parameter_key: pLambdaLogGroupRetention + parameter_value: 14 + - parameter_key: pLambdaLogLevel + parameter_value: INFO + - parameter_key: pSRAAlarmEmail + parameter_value: '' + - parameter_key: pSRASolutionVersion + parameter_value: v1.0 + - parameter_key: pRegisterAuditAccountDataSubscriber + parameter_value: 'false' + - parameter_key: pAuditAccountDataSubscriberPrefix + parameter_value: sra-audit-account-data-subscriber + - parameter_key: pAuditAccountDataSubscriberExternalId + parameter_value: '' + - parameter_key: pAuditAccountQuerySubscriberPrefix + parameter_value: sra-audit-account-query-subscriber + - parameter_key: pAuditAccountQuerySubscriberExternalId + parameter_value: '' + - parameter_key: pRegisterAuditAccountQuerySubscriber + parameter_value: 'false' + - parameter_key: pStackSetAdminRole + parameter_value: sra-stackset + - parameter_key: pStackExecutionRole + parameter_value: sra-execution + - parameter_key: pSecurityLakeWarning + parameter_value: Reject + - parameter_key: pDisableSecurityLake + parameter_value: 'false' + deploy_method: stack_set + deployment_targets: + accounts: + - REPLACE_ME_ORG_MANAGEMENT_ACCOUNT_NAME diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/customizations_for_aws_control_tower/parameters/sra-security-lake-main-ssm.json b/aws_sra_examples/solutions/security_lake/security_lake_org/customizations_for_aws_control_tower/parameters/sra-security-lake-main-ssm.json new file mode 100644 index 00000000..fceea19a --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/customizations_for_aws_control_tower/parameters/sra-security-lake-main-ssm.json @@ -0,0 +1,142 @@ +[ + { + "ParameterKey": "pSecurityLakeOrgLambdaRoleName", + "ParameterValue": "sra-security-lake-org-lambda" + }, + { + "ParameterKey": "pCreateResourceLink", + "ParameterValue": "false" + }, + { + "ParameterKey": "pCreateLakeFormationSlr", + "ParameterValue": "true" + }, + { + "ParameterKey": "pSRASecurityLakeMetaStoreManagerRoleName", + "ParameterValue": "AmazonSecurityLakeMetaStoreManagerV2" + }, + { + "ParameterKey": "pSourceVersion", + "ParameterValue": "2.0" + }, + { + "ParameterKey": "pCloudTrailManagementEvents", + "ParameterValue": "ALL" + }, + { + "ParameterKey": "pCloudTrailLambdaDataEvents", + "ParameterValue": "ALL" + }, + { + "ParameterKey": "pCloudTrailS3DataEvents", + "ParameterValue": "" + }, + { + "ParameterKey": "pSecurityHubFindings", + "ParameterValue": "ALL" + }, + { + "ParameterKey": "pVpcFlowLogs", + "ParameterValue": "ALL" + }, + { + "ParameterKey": "pWafLogs", + "ParameterValue": "" + }, + { + "ParameterKey": "pRoute53Logs", + "ParameterValue": "ALL" + }, + { + "ParameterKey": "pVpcFlowLogs", + "ParameterValue": "ALL" + }, + { + "ParameterKey": "pOrgConfigurationSources", + "ParameterValue": "ROUTE53,VPC_FLOW,SH_FINDINGS,CLOUD_TRAIL_MGMT,LAMBDA_EXECUTION,EKS_AUDIT" + }, + { + "ParameterKey": "pCreateOrganizationConfiguration", + "ParameterValue": "true" + }, + { + "ParameterKey": "pSecurityLakeOrgKeyAlias", + "ParameterValue": "sra-security-lake-org-key" + }, + { + "ParameterKey": "pComplianceFrequency", + "ParameterValue": "7" + }, + { + "ParameterKey": "pControlTowerRegionsOnly", + "ParameterValue": "true" + }, + { + "ParameterKey": "pCreateLambdaLogGroup", + "ParameterValue": "false" + }, + { + "ParameterKey": "pEnabledRegions", + "ParameterValue": "" + }, + { + "ParameterKey": "pLambdaLogGroupKmsKey", + "ParameterValue": "" + }, + { + "ParameterKey": "pLambdaLogGroupRetention", + "ParameterValue": "14" + }, + { + "ParameterKey": "pLambdaLogLevel", + "ParameterValue": "INFO" + }, + { + "ParameterKey": "pSRAAlarmEmail", + "ParameterValue": "" + }, + { + "ParameterKey": "pSRASolutionVersion", + "ParameterValue": "v1.0" + }, + { + "ParameterKey": "pRegisterAuditAccountDataSubscriber", + "ParameterValue": "false" + }, + { + "ParameterKey": "pAuditAccountDataSubscriberPrefix", + "ParameterValue": "sra-audit-account-data-subscriber" + }, + { + "ParameterKey": "pAuditAccountDataSubscriberExternalId", + "ParameterValue": "" + }, + { + "ParameterKey": "pAuditAccountQuerySubscriberPrefix", + "ParameterValue": "sra-audit-account-query-subscriber" + }, + { + "ParameterKey": "pAuditAccountQuerySubscriberExternalId", + "ParameterValue": "" + }, + { + "ParameterKey": "pRegisterAuditAccountQuerySubscriber", + "ParameterValue": "false" + }, + { + "ParameterKey": "pStackSetAdminRole", + "ParameterValue": "sra-stackset" + }, + { + "ParameterKey": "pStackExecutionRole", + "ParameterValue": "sra-execution" + }, + { + "ParameterKey": "pSecurityLakeWarning", + "ParameterValue": "Reject" + }, + { + "ParameterKey": "pDisableSecurityLake", + "ParameterValue": "false" + } +] \ No newline at end of file diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/documentation/sra-security-lake-org.png b/aws_sra_examples/solutions/security_lake/security_lake_org/documentation/sra-security-lake-org.png new file mode 100644 index 00000000..9e0e7829 Binary files /dev/null and b/aws_sra_examples/solutions/security_lake/security_lake_org/documentation/sra-security-lake-org.png differ diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/documentation/sra-security-lake-org.pptx b/aws_sra_examples/solutions/security_lake/security_lake_org/documentation/sra-security-lake-org.pptx new file mode 100644 index 00000000..2dac57e3 Binary files /dev/null and b/aws_sra_examples/solutions/security_lake/security_lake_org/documentation/sra-security-lake-org.pptx differ diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/app.py b/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/app.py new file mode 100644 index 00000000..c01f557a --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/app.py @@ -0,0 +1,686 @@ +"""This script performs operations to enable, configure, update, and disable Security Lake. + +Version: 1.0 + +'security_lake_org' solution in the repo, https://github.com/aws-samples/aws-security-reference-architecture-examples + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +from __future__ import annotations + +import logging +import os +import re +from typing import TYPE_CHECKING, Any + +import boto3 +import common +import security_lake +import sra_ssm_params +from botocore.config import Config +from crhelper import CfnResource + +if TYPE_CHECKING: + from aws_lambda_typing.context import Context + from aws_lambda_typing.events import CloudFormationCustomResourceEvent + from mypy_boto3_securitylake import SecurityLakeClient + + +LOGGER = logging.getLogger("sra") +log_level: str = os.environ.get("LOG_LEVEL", "INFO") +LOGGER.setLevel(log_level) + +ssm = sra_ssm_params.SraSsmParams(LOGGER) +helper = CfnResource(json_logging=True, log_level=log_level, boto_level="CRITICAL", sleep_on_delete=120) + +BOTO3_CONFIG = Config(retries={"max_attempts": 10, "mode": "standard"}) +UNEXPECTED = "Unexpected!" +SERVICE_NAME = "securitylake.amazonaws.com" +HOME_REGION = ssm.get_home_region() +AUDIT_ACCT_ID = ssm.get_security_acct() +AWS_LOG_SOURCES = ["ROUTE53", "VPC_FLOW", "SH_FINDINGS", "CLOUD_TRAIL_MGMT", "LAMBDA_EXECUTION", "S3_DATA", "EKS_AUDIT", "WAF"] +CLOUDFORMATION_PAGE_SIZE = 20 + +try: + MANAGEMENT_ACCOUNT_SESSION = boto3.Session() + PARTITION: str = MANAGEMENT_ACCOUNT_SESSION.get_partition_for_region(HOME_REGION) # type: ignore + CFN_CLIENT = MANAGEMENT_ACCOUNT_SESSION.client("cloudformation") +except Exception: + LOGGER.exception(UNEXPECTED) + raise ValueError("Unexpected error executing Lambda function. Review CloudWatch logs for details.") from None + + +def process_add_event(params: dict, regions: list, accounts: dict) -> None: + """Process Add or Update Events. + + Args: + params: Configuration Parameters + regions: AWS regions + accounts: AWS accounts + + Returns: + Status + """ + LOGGER.info("...process_add_event") + + if params["action"] in ["Add"]: + enable_and_configure_security_lake(params, regions, accounts) + for region in regions: + delegated_admin_session = common.assume_role( + params["CONFIGURATION_ROLE_NAME"], "sra-process-audit-acct-subscriber", params["DELEGATED_ADMIN_ACCOUNT_ID"] + ) + sl_client = delegated_admin_session.client("securitylake", region) + if params["SET_AUDIT_ACCT_DATA_SUBSCRIBER"]: + add_audit_acct_data_subscriber(sl_client, params, region) + if params["SET_AUDIT_ACCT_QUERY_SUBSCRIBER"]: + add_audit_acct_query_subscriber(sl_client, params, region) + + if params["SET_AUDIT_ACCT_QUERY_SUBSCRIBER"] and params["CREATE_RESOURCE_LINK"]: + configure_audit_acct_for_query_access(params, regions) + + LOGGER.info("...ADD_COMPLETE") + return + + LOGGER.info("...ADD_NO_EVENT") + + +def process_update_event(params: dict, regions: list, accounts: dict) -> None: + """Process Add or Update Events. + + Args: + params: Configuration Parameters + regions: AWS regions + accounts: AWS accounts + + Returns: + Status + """ + LOGGER.info("...process_update_event") + + if params["action"] in ["Update"]: + if params["DISABLE_SECURITY_LAKE"]: + disable_security_lake(params, regions, accounts) + else: + update_security_lake(params, regions) + update_log_sources(params, regions, accounts) + if params["SET_AUDIT_ACCT_DATA_SUBSCRIBER"]: + update_audit_acct_data_subscriber(params, regions) + if params["SET_AUDIT_ACCT_QUERY_SUBSCRIBER"]: + update_audit_acct_query_subscriber(params, regions) + + LOGGER.info("...UPDATE_COMPLETE") + return + + LOGGER.info("...UPDATE_NO_EVENT") + + +def process_delete_event(params: dict, regions: list, accounts: dict) -> None: + """Process Add or Update Events. + + Args: + params: Configuration Parameters + regions: AWS regions + accounts: AWS accounts + + Returns: + Status + """ + LOGGER.info("...process_delete_event") + if params["action"] in ["Update"]: + if params["DISABLE_SECURITY_LAKE"]: + LOGGER.info("...Disable Security Lake") + disable_security_lake(params, regions, accounts) + LOGGER.info("...DELETE_COMPLETE") + return + + LOGGER.info("...DELETE_NO_EVENT") + + +def process_event(event: dict) -> None: + """Process Event. + + Args: + event: event data + """ + event_info = {"Event": event} + LOGGER.info(event_info) + params = get_validated_parameters({"RequestType": "Update"}) + accounts = common.get_active_organization_accounts() + regions = common.get_enabled_regions(params["ENABLED_REGIONS"], params["CONTROL_TOWER_REGIONS_ONLY"] == "true") + + process_update_event(params, regions, accounts) + + +def parameter_pattern_validator(parameter_name: str, parameter_value: str | None, pattern: str, is_optional: bool = False) -> dict: + """Validate CloudFormation Custom Resource Properties and/or Lambda Function Environment Variables. + + Args: + parameter_name: CloudFormation custom resource parameter name and/or Lambda function environment variable name + parameter_value: CloudFormation custom resource parameter value and/or Lambda function environment variable value + pattern: REGEX pattern to validate against. + is_optional: Allow empty or missing value when True + + Raises: + ValueError: Parameter has a value of empty string. + ValueError: Parameter is missing + ValueError: Parameter does not follow the allowed pattern + + Returns: + Validated Parameter + """ + if parameter_value == "" and not is_optional: + raise ValueError(f"({parameter_name}) parameter has a value of empty string.") + elif not parameter_value and not is_optional: + raise ValueError(f"({parameter_name}) parameter is missing.") + elif not re.match(pattern, str(parameter_value)): + raise ValueError(f"({parameter_name}) parameter with value of ({parameter_value})" + f" does not follow the allowed pattern: {pattern}.") + return {parameter_name: parameter_value} + + +def get_validated_parameters(event: dict[str, Any]) -> dict: + """Validate AWS CloudFormation parameters. + + Args: + event: event data + + Returns: + Validated parameters + """ + params: dict[str, str | bool] = {} + actions = {"Create": "Add", "Update": "Update", "Delete": "Remove"} + params["action"] = actions[event.get("RequestType", "Create")] + true_false_pattern = r"^true|false$" + log_source_pattern = r"(?i)^((ROUTE53|VPC_FLOW|SH_FINDINGS|CLOUD_TRAIL_MGMT|LAMBDA_EXECUTION|S3_DATA|EKS_AUDIT|WAF),?){0,7}($|ROUTE53|VPC_FLOW|SH_FINDINGS|CLOUD_TRAIL_MGMT|LAMBDA_EXECUTION|S3_DATA|EKS_AUDIT|WAF){1}$" # noqa: E501, B950 + version_pattern = r"^[0-9.]+$" + source_target_pattern = r"^($|ALL|(\d{12})(,\s*\d{12})*)$" + name_pattern = r"^[\w+=,.@-]{1,64}$" + + # Required Parameters + params.update(parameter_pattern_validator("DISABLE_SECURITY_LAKE", os.environ.get("DISABLE_SECURITY_LAKE"), pattern=true_false_pattern)) + params.update(parameter_pattern_validator("DELEGATED_ADMIN_ACCOUNT_ID", os.environ.get("DELEGATED_ADMIN_ACCOUNT_ID"), pattern=r"^\d{12}$")) + params.update(parameter_pattern_validator("MANAGEMENT_ACCOUNT_ID", os.environ.get("MANAGEMENT_ACCOUNT_ID"), pattern=r"^\d{12}$")) + params.update(parameter_pattern_validator("AWS_PARTITION", os.environ.get("AWS_PARTITION"), pattern=r"^(aws[a-zA-Z-]*)?$")) + params.update(parameter_pattern_validator("CONFIGURATION_ROLE_NAME", os.environ.get("CONFIGURATION_ROLE_NAME"), pattern=name_pattern)) + params.update(parameter_pattern_validator("SUBSCRIBER_ROLE_NAME", os.environ.get("SUBSCRIBER_ROLE_NAME"), pattern=name_pattern)) + params.update(parameter_pattern_validator("CONTROL_TOWER_REGIONS_ONLY", os.environ.get("CONTROL_TOWER_REGIONS_ONLY"), pattern=true_false_pattern)) + params.update( + parameter_pattern_validator("SET_AUDIT_ACCT_DATA_SUBSCRIBER", os.environ.get("SET_AUDIT_ACCT_DATA_SUBSCRIBER"), pattern=true_false_pattern) + ) + params.update( + parameter_pattern_validator("SET_AUDIT_ACCT_QUERY_SUBSCRIBER", os.environ.get("SET_AUDIT_ACCT_QUERY_SUBSCRIBER"), pattern=true_false_pattern) + ) + params.update(parameter_pattern_validator("SOURCE_VERSION", os.environ.get("SOURCE_VERSION"), pattern=version_pattern)) + params.update(parameter_pattern_validator("SET_ORG_CONFIGURATION", os.environ.get("SET_ORG_CONFIGURATION"), pattern=true_false_pattern)) + params.update(parameter_pattern_validator("META_STORE_MANAGER_ROLE_NAME", os.environ.get("META_STORE_MANAGER_ROLE_NAME"), pattern=name_pattern)) + params.update(parameter_pattern_validator("CREATE_RESOURCE_LINK", os.environ.get("CREATE_RESOURCE_LINK"), pattern=true_false_pattern)) + params.update(parameter_pattern_validator("KEY_ALIAS", os.environ.get("KEY_ALIAS"), pattern=r"^[a-zA-Z0-9/_-]+$")) + + # Optional Parameters + params.update(parameter_pattern_validator("ENABLED_REGIONS", os.environ.get("ENABLED_REGIONS"), pattern=r"^$|[a-z0-9-, ]+$", is_optional=True)) + params.update( + parameter_pattern_validator("CLOUD_TRAIL_MGMT", os.environ.get("CLOUD_TRAIL_MGMT"), pattern=source_target_pattern, is_optional=True) + ) + params.update(parameter_pattern_validator("ROUTE53", os.environ.get("ROUTE53"), pattern=source_target_pattern, is_optional=True)) + params.update(parameter_pattern_validator("VPC_FLOW", os.environ.get("VPC_FLOW"), pattern=source_target_pattern, is_optional=True)) + params.update(parameter_pattern_validator("SH_FINDINGS", os.environ.get("SH_FINDINGS"), pattern=source_target_pattern, is_optional=True)) + params.update( + parameter_pattern_validator("LAMBDA_EXECUTION", os.environ.get("LAMBDA_EXECUTION"), pattern=source_target_pattern, is_optional=True) + ) + params.update(parameter_pattern_validator("S3_DATA", os.environ.get("S3_DATA"), pattern=source_target_pattern, is_optional=True)) + params.update(parameter_pattern_validator("EKS_AUDIT", os.environ.get("EKS_AUDIT"), pattern=source_target_pattern, is_optional=True)) + params.update(parameter_pattern_validator("WAF", os.environ.get("WAF"), pattern=source_target_pattern, is_optional=True)) + params.update( + parameter_pattern_validator( + "ORG_CONFIGURATION_SOURCES", os.environ.get("ORG_CONFIGURATION_SOURCES"), pattern=log_source_pattern, is_optional=True + ) + ) + + params.update( + parameter_pattern_validator( + "AUDIT_ACCT_DATA_SUBSCRIBER", os.environ.get("AUDIT_ACCT_DATA_SUBSCRIBER"), pattern=name_pattern, is_optional=True + ) + ) + params.update( + parameter_pattern_validator( + "DATA_SUBSCRIBER_EXTERNAL_ID", os.environ.get("DATA_SUBSCRIBER_EXTERNAL_ID"), pattern=r"^(?:[a-zA-Z0-9]{0,64})?$", is_optional=True + ) + ) + + params.update( + parameter_pattern_validator( + "AUDIT_ACCT_QUERY_SUBSCRIBER", os.environ.get("AUDIT_ACCT_QUERY_SUBSCRIBER"), pattern=name_pattern, is_optional=True + ) + ) + params.update( + parameter_pattern_validator( + "QUERY_SUBSCRIBER_EXTERNAL_ID", os.environ.get("QUERY_SUBSCRIBER_EXTERNAL_ID"), pattern=r"^(?:[a-zA-Z0-9]{0,64})?$", is_optional=True + ) + ) + + # Convert true/false string parameters to boolean + params.update({"DISABLE_SECURITY_LAKE": (params["DISABLE_SECURITY_LAKE"] == "true")}) + params.update({"SET_AUDIT_ACCT_DATA_SUBSCRIBER": (params["SET_AUDIT_ACCT_DATA_SUBSCRIBER"] == "true")}) + params.update({"SET_AUDIT_ACCT_QUERY_SUBSCRIBER": (params["SET_AUDIT_ACCT_QUERY_SUBSCRIBER"] == "true")}) + params.update({"CONTROL_TOWER_REGIONS_ONLY": (params["CONTROL_TOWER_REGIONS_ONLY"] == "true")}) + params.update({"SET_ORG_CONFIGURATION": (params["SET_ORG_CONFIGURATION"] == "true")}) + params.update({"CREATE_RESOURCE_LINK": (params["CREATE_RESOURCE_LINK"] == "true")}) + + return params + + +def enable_and_configure_security_lake(params: dict, regions: list, accounts: dict) -> None: + """Enable the security lake service and configure its global settings. + + Args: + params: Configuration Parameters + regions: AWS regions + accounts: AWS accounts + """ + security_lake.register_delegated_admin(params["DELEGATED_ADMIN_ACCOUNT_ID"], HOME_REGION, SERVICE_NAME) + provision_security_lake(params, regions) + add_log_sources(params, regions, accounts) + for region in regions: + key_id = f'alias/{params["KEY_ALIAS"]}-{region}' + security_lake.encrypt_sqs_queues(params["CONFIGURATION_ROLE_NAME"], params["DELEGATED_ADMIN_ACCOUNT_ID"], region, key_id) + + +def provision_security_lake(params: dict, regions: list) -> None: + """Enable Security Lake and configure Organization Configurations. + + Args: + params: parameters + regions: AWS regions + """ + all_data = [{"region": region, "key_arn": f'alias/{params["KEY_ALIAS"]}-{region}'} for region in regions] + sl_configurations = [{"encryptionConfiguration": {"kmsKeyId": data["key_arn"]}, "region": data["region"]} for data in all_data] + delegated_admin_session = common.assume_role( + params["CONFIGURATION_ROLE_NAME"], + "sra-create-data-lake", + params["DELEGATED_ADMIN_ACCOUNT_ID"], + ) + sl_client = delegated_admin_session.client("securitylake", HOME_REGION) + LOGGER.info(f"Creating Security Lake in {(', '.join(regions))}") + role_arn = f"arn:{PARTITION}:iam::{params['DELEGATED_ADMIN_ACCOUNT_ID']}:role/service-role/{params['META_STORE_MANAGER_ROLE_NAME']}" + security_lake.create_security_lake(sl_client, sl_configurations, role_arn) + status = security_lake.check_data_lake_create_status(sl_client, regions) + if status: + LOGGER.info("CreateDataLake status 'COMPLETED'") + process_org_configuration(sl_client, params["SET_ORG_CONFIGURATION"], params["ORG_CONFIGURATION_SOURCES"], regions, params["SOURCE_VERSION"]) + + +def update_security_lake(params: dict, regions: list) -> None: + """Update Security Lake and Organization Configurations. + + Args: + params: parameters + regions: AWS regions + """ + for region in regions: + delegated_admin_session = common.assume_role( + params["CONFIGURATION_ROLE_NAME"], + "sra-update-security-lake", + params["DELEGATED_ADMIN_ACCOUNT_ID"], + ) + sl_client = delegated_admin_session.client("securitylake", region) + LOGGER.info(f"Checking if Security Lake is enabled in {region} region...") + lake_exists = security_lake.check_data_lake_exists(sl_client, region) + if lake_exists: + LOGGER.info(f"Security Lake already enabled in {region} region.") + else: + LOGGER.info(f"Security Lake not found in {region} region. Enabling Security Lake...") + key_id = f'alias/{params["KEY_ALIAS"]}-{region}' + sl_configurations = [{"encryptionConfiguration": {"kmsKeyId": key_id}, "region": region}] + role_arn = f"arn:{PARTITION}:iam::{params['DELEGATED_ADMIN_ACCOUNT_ID']}:role/service-role/{params['META_STORE_MANAGER_ROLE_NAME']}" + security_lake.create_security_lake(sl_client, sl_configurations, role_arn) + lake_exists = security_lake.check_data_lake_exists(sl_client, region) + if lake_exists: + LOGGER.info(f"Security Lake is enabled in {region}.") + security_lake.encrypt_sqs_queues(params["CONFIGURATION_ROLE_NAME"], params["DELEGATED_ADMIN_ACCOUNT_ID"], region, key_id) + process_org_configuration(sl_client, params["SET_ORG_CONFIGURATION"], params["ORG_CONFIGURATION_SOURCES"], regions, params["SOURCE_VERSION"]) + + +def process_org_configuration( + sl_client: SecurityLakeClient, set_org_configuration: bool, org_configuration_sources: str, regions: list, source_version: str +) -> None: + """Set Security Lake organization configuration for new accounts. + + Args: + sl_client: boto3 client + set_org_configuration: enable organization configurations for new accounts + org_configuration_sources: list of aws log sources + regions: AWS regions + source_version: source version + """ + LOGGER.info(f"Checking if Organization Configuration enabled in {', '.join(regions)} region(s)") + org_configuration_exists, existing_org_configuration = security_lake.get_org_configuration(sl_client) + if set_org_configuration: + sources = [source.strip() for source in org_configuration_sources.split(",")] + if not org_configuration_exists: + LOGGER.info(f"Organization Configuration not enabled in {', '.join(regions)} region(s). Creating...") + security_lake.create_organization_configuration(sl_client, regions, sources, source_version) + LOGGER.info("Enabled Organization Configuration") + else: + security_lake.update_organization_configuration(sl_client, regions, sources, source_version, existing_org_configuration) + else: + if org_configuration_exists: + LOGGER.info(f"Deleting Organization Configuration in {r', '.join(regions)} region(s)...") + security_lake.delete_organization_configuration(sl_client, existing_org_configuration) + LOGGER.info("Deleted Organization Configuration") + + +def add_log_sources(params: dict, regions: list, org_accounts: dict) -> None: + """Configure aws log sources. + + Args: + params: Configuration parameters + regions: A list of AWS regions. + org_accounts: A list of AWS accounts. + """ + aws_log_sources = [] + org_accounts_ids = [account["AccountId"] for account in org_accounts] + delegated_admin_session = common.assume_role(params["CONFIGURATION_ROLE_NAME"], "sra-add-log-sources", params["DELEGATED_ADMIN_ACCOUNT_ID"]) + sl_client = delegated_admin_session.client("securitylake", HOME_REGION) + for log_source in AWS_LOG_SOURCES: + if params[log_source] != "": + accounts = params[log_source].split(",") if params[log_source] != "ALL" else org_accounts_ids + configurations = {"accounts": accounts, "regions": regions, "sourceName": log_source, "sourceVersion": params["SOURCE_VERSION"]} + aws_log_sources.append(configurations) + if aws_log_sources: + security_lake.add_aws_log_source(sl_client, aws_log_sources) + + +def update_log_sources(params: dict, regions: list, org_accounts: dict) -> None: + """Configure aws log sources. + + Args: + params: Configuration parameters + regions: A list of AWS regions. + org_accounts: A list of AWS accounts. + """ + org_accounts_ids = [account["AccountId"] for account in org_accounts] + delegated_admin_session = common.assume_role(params["CONFIGURATION_ROLE_NAME"], "sra-update-log-sources", params["DELEGATED_ADMIN_ACCOUNT_ID"]) + sl_client = delegated_admin_session.client("securitylake", HOME_REGION) + for log_source in AWS_LOG_SOURCES: + if params[log_source] != "": + accounts = params[log_source].split(",") if params[log_source] != "ALL" else org_accounts_ids + security_lake.update_aws_log_source(sl_client, regions, log_source, accounts, org_accounts_ids, params["SOURCE_VERSION"]) + elif params[log_source] == "": + result = security_lake.check_log_source_enabled(sl_client, [], org_accounts_ids, regions, log_source, params["SOURCE_VERSION"]) + accounts = list(result.accounts_to_disable) + if result.source_exists: + security_lake.delete_aws_log_source(sl_client, regions, log_source, accounts, params["SOURCE_VERSION"]) + else: + LOGGER.info(f"Error reading value for {log_source} parameter") + + +def update_audit_acct_data_subscriber(params: dict, regions: list) -> None: + """Configure Audit (Security Tooling) account as data access subscriber. + + Args: + params: parameters + regions: AWS regions + """ + s3_access = "S3" + sources = [source for source in AWS_LOG_SOURCES if params[source]] + if sources == []: + LOGGER.info("No log sources selected for data access subscriber. Skipping...") + else: + for region in regions: + subscriber_name = params["AUDIT_ACCT_DATA_SUBSCRIBER"] + "-" + region + delegated_admin_session = common.assume_role( + params["CONFIGURATION_ROLE_NAME"], "sra-process-audit-acct-subscriber", params["DELEGATED_ADMIN_ACCOUNT_ID"] + ) + sl_client = delegated_admin_session.client("securitylake", region, config=BOTO3_CONFIG) + subscriber_exists, subscriber_id, external_id = security_lake.check_subscriber_exists(sl_client, subscriber_name) + if subscriber_exists: + security_lake.update_subscriber( + sl_client, subscriber_id, sources, external_id, AUDIT_ACCT_ID, subscriber_name, params["SOURCE_VERSION"] + ) + else: + external_id = params["DATA_SUBSCRIBER_EXTERNAL_ID"] + LOGGER.info(f"Creating Audit account subscriber '{subscriber_name}' in {region} region...") + subscriber_id, _ = security_lake.create_subscribers( + sl_client, s3_access, sources, external_id, AUDIT_ACCT_ID, subscriber_name, params["SOURCE_VERSION"] + ) + + +def add_audit_acct_data_subscriber(sl_client: SecurityLakeClient, params: dict, region: str) -> None: + """Configure Audit (Security Tooling) account as data access subscriber. + + Args: + sl_client: boto3 client + params: configuration parameters + region: AWS region + """ + subscriber_name = params["AUDIT_ACCT_DATA_SUBSCRIBER"] + "-" + region + sources = [source for source in AWS_LOG_SOURCES if params[source]] + if sources == []: + LOGGER.info("No log sources selected for data access subscriber. Skipping...") + else: + subscriber_exists, subscriber_id, external_id = security_lake.check_subscriber_exists(sl_client, subscriber_name) + if subscriber_exists: + security_lake.update_subscriber(sl_client, subscriber_id, sources, external_id, AUDIT_ACCT_ID, subscriber_name, params["SOURCE_VERSION"]) + else: + external_id = params["DATA_SUBSCRIBER_EXTERNAL_ID"] + LOGGER.info(f"Creating Audit account subscriber '{subscriber_name}' in {region} region...") + subscriber_id, _ = security_lake.create_subscribers( + sl_client, "S3", sources, external_id, AUDIT_ACCT_ID, subscriber_name, params["SOURCE_VERSION"] + ) + + +def update_audit_acct_query_subscriber(params: dict, regions: list) -> None: + """Configure Audit (Security tooling) account as query access subscribe. + + Args: + params: parameters + regions: AWS regions + """ + lakeformation_access = "LAKEFORMATION" + sources = [source for source in AWS_LOG_SOURCES if params[source]] + if sources == []: + LOGGER.info("No log sources selected for query access subscriber. Skipping...") + else: + for region in regions: + subscriber_name = params["AUDIT_ACCT_QUERY_SUBSCRIBER"] + "-" + region + delegated_admin_session = common.assume_role( + params["CONFIGURATION_ROLE_NAME"], "sra-process-audit-acct-subscriber", params["DELEGATED_ADMIN_ACCOUNT_ID"] + ) + sl_client = delegated_admin_session.client("securitylake", region) + subscriber_exists, subscriber_id, external_id = security_lake.check_subscriber_exists(sl_client, subscriber_name) + if subscriber_exists: + LOGGER.info(f"Audit account subscriber '{subscriber_name}' exists in {region} region. Updating subscriber...") + resource_share_arn = security_lake.update_subscriber( + sl_client, subscriber_id, sources, external_id, AUDIT_ACCT_ID, subscriber_name, params["SOURCE_VERSION"] + ) + else: + external_id = params["QUERY_SUBSCRIBER_EXTERNAL_ID"] + LOGGER.info(f"Audit account subscriber '{subscriber_name}' does not exist in {region} region. Creating subscriber...") + subscriber_id, resource_share_arn = security_lake.create_subscribers( + sl_client, lakeformation_access, sources, external_id, AUDIT_ACCT_ID, subscriber_name, params["SOURCE_VERSION"] + ) + if params["CREATE_RESOURCE_LINK"]: + configure_query_subscriber_on_update( + params["SUBSCRIBER_ROLE_NAME"], + AUDIT_ACCT_ID, + subscriber_name, + params["DELEGATED_ADMIN_ACCOUNT_ID"], + region, + resource_share_arn, + params["SUBSCRIBER_ROLE_NAME"], + ) + + +def add_audit_acct_query_subscriber(sl_client: SecurityLakeClient, params: dict, region: str) -> None: + """Configure Audit (Security tooling) account as query access subscribe. + + Args: + sl_client: boto3 client + params: configuration parameters + region: AWS region + """ + subscriber_name = params["AUDIT_ACCT_QUERY_SUBSCRIBER"] + "-" + region + sources = [source for source in AWS_LOG_SOURCES if params[source]] + if sources == []: + LOGGER.info("No log sources selected for query access subscriber. Skipping...") + else: + external_id = params["QUERY_SUBSCRIBER_EXTERNAL_ID"] + LOGGER.info(f"Audit account subscriber '{subscriber_name}' does not exist in {region} region. Creating subscriber...") + security_lake.create_subscribers(sl_client, "LAKEFORMATION", sources, external_id, AUDIT_ACCT_ID, subscriber_name, params["SOURCE_VERSION"]) + + +def configure_audit_acct_for_query_access(params: dict, regions: list) -> None: + """Configure resources for query access in Audit account. + + Args: + params: configuration parameters + regions: AWS regions + """ + for region in regions: + subscriber_name = params["AUDIT_ACCT_QUERY_SUBSCRIBER"] + "-" + region + delegated_admin_session = common.assume_role( + params["CONFIGURATION_ROLE_NAME"], "sra-process-audit-acct-subscriber", params["DELEGATED_ADMIN_ACCOUNT_ID"] + ) + sl_client = delegated_admin_session.client("securitylake", region) + subscriber_created, resource_share_arn = security_lake.get_subscriber_resourceshare_arn(sl_client, subscriber_name) + if subscriber_created: + LOGGER.info(f"Configuring Audit (Security tooling) account subscriber '{subscriber_name}' ({region})") + if params["CREATE_RESOURCE_LINK"]: + configure_query_subscriber_on_update( + params["SUBSCRIBER_ROLE_NAME"], + AUDIT_ACCT_ID, + subscriber_name, + params["DELEGATED_ADMIN_ACCOUNT_ID"], + region, + resource_share_arn, + params["SUBSCRIBER_ROLE_NAME"], + ) + + +def configure_query_subscriber_on_update( + configuration_role_name: str, + subscriber_acct: str, + subscriber_name: str, + security_lake_acct: str, + region: str, + resource_share_arn: str, + subscriber_role: str, +) -> None: + """Configure query access subscriber. + + Args: + configuration_role_name: configuration role name + subscriber_acct: subscriber AWS account + subscriber_name: subscriber name + security_lake_acct: Security Lake delegated administrator account + region: AWS region + resource_share_arn: RAM resource share arn + subscriber_role: subscriber role name + """ + subscriber_session = common.assume_role(configuration_role_name, "sra-create-resource-share", subscriber_acct) + ram_client = subscriber_session.client("ram", region) + LOGGER.info(f"Configuring resource share link for subscriber '{subscriber_name}' ({region})") + security_lake.configure_resource_share_in_subscriber_acct(ram_client, resource_share_arn) + shared_db_name, shared_tables = security_lake.get_shared_resource_names(ram_client, resource_share_arn) + if shared_tables == "" or shared_db_name == "": + LOGGER.info(f"No shared resource names found for subscriber '{subscriber_name}' ({region})") + else: + subscriber_session = common.assume_role(configuration_role_name, "sra-create-resource-share-link", subscriber_acct) + glue_client = subscriber_session.client("glue", region) + LOGGER.info(f"Creating database '{shared_db_name}_subscriber' for subscriber '{subscriber_name}' ({region})") + security_lake.create_db_in_data_catalog(glue_client, subscriber_acct, shared_db_name, region, subscriber_role) + security_lake.create_table_in_data_catalog(glue_client, shared_db_name, shared_tables, security_lake_acct, region) + + +def disable_security_lake(params: dict, regions: list, accounts: dict) -> None: + """Disable Security Lake service. + + Args: + params: Configuration Parameters + regions: AWS regions + accounts: AWS accounts + """ + for region in regions: + delegated_admin_session = common.assume_role( + params["CONFIGURATION_ROLE_NAME"], "sra-delete-security-lake-subscribers", params["DELEGATED_ADMIN_ACCOUNT_ID"] + ) + sl_client = delegated_admin_session.client("securitylake", region) + if params["SET_AUDIT_ACCT_DATA_SUBSCRIBER"]: + subscriber_name = params["AUDIT_ACCT_DATA_SUBSCRIBER"] + "-" + region + security_lake.delete_subscriber(sl_client, subscriber_name, region) + if params["SET_AUDIT_ACCT_QUERY_SUBSCRIBER"]: + subscriber_name = params["AUDIT_ACCT_QUERY_SUBSCRIBER"] + "-" + region + security_lake.delete_subscriber(sl_client, subscriber_name, region) + + org_configuration_exists, existing_org_configuration = security_lake.get_org_configuration(sl_client) + if org_configuration_exists: + LOGGER.info(f"Deleting Organization Configuration in {region} region...") + security_lake.delete_organization_configuration(sl_client, existing_org_configuration) + + all_accounts = [account["AccountId"] for account in accounts] + for source in AWS_LOG_SOURCES: + security_lake.delete_aws_log_source(sl_client, regions, source, all_accounts, params["SOURCE_VERSION"]) + + +def orchestrator(event: dict[str, Any], context: Any) -> None: + """Orchestration. + + Args: + event: event data + context: runtime information + """ + if event.get("RequestType"): + LOGGER.info("...calling helper...") + helper(event, context) + else: + LOGGER.info("...else...just calling process_event...") + process_event(event) + + +def lambda_handler(event: dict[str, Any], context: Any) -> None: + """Lambda Handler. + + Args: + event: event data + context: runtime information + + Raises: + ValueError: Unexpected error executing Lambda function + """ + LOGGER.info("....Lambda Handler Started....") + boto3_version = boto3.__version__ + LOGGER.info(f"boto3 version: {boto3_version}") + try: + orchestrator(event, context) + except Exception: + LOGGER.exception(UNEXPECTED) + raise ValueError(f"Unexpected error executing Lambda function. Review CloudWatch logs ({context.log_group_name}) for details.") from None + + +@helper.create +@helper.update +@helper.delete +def process_event_cloudformation(event: CloudFormationCustomResourceEvent, context: Context) -> str: # noqa U100 + """Process Event from AWS CloudFormation. + + Args: + event: event data + context: runtime information + + Returns: + AWS CloudFormation physical resource id + """ + event_info = {"Event": event} + LOGGER.info(event_info) + params = get_validated_parameters({"RequestType": event["RequestType"]}) + accounts = common.get_active_organization_accounts() + regions = common.get_enabled_regions(params["ENABLED_REGIONS"], params["CONTROL_TOWER_REGIONS_ONLY"]) + if params["action"] == "Add": + process_add_event(params, regions, accounts) + elif params["action"] == "Update": + process_update_event(params, regions, accounts) + else: + LOGGER.info("...Disable Security Lake from (process_event_cloudformation)") + process_delete_event(params, regions, accounts) + + return f"sra-security-lake-org-{params['DELEGATED_ADMIN_ACCOUNT_ID']}" diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/common.py b/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/common.py new file mode 100644 index 00000000..30236a1e --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/common.py @@ -0,0 +1,169 @@ +# type: ignore +"""This script includes common functions. + +Version: 1.0 + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +from __future__ import annotations + +import logging +import os +from time import sleep +from typing import TYPE_CHECKING + +import boto3 +from botocore.exceptions import ClientError, EndpointConnectionError + +if TYPE_CHECKING: + from mypy_boto3_organizations import OrganizationsClient + from mypy_boto3_ssm.client import SSMClient + from mypy_boto3_sts.client import STSClient + +# Setup Default Logger +LOGGER = logging.getLogger("sra") +log_level = os.environ.get("LOG_LEVEL", logging.INFO) +LOGGER.setLevel(log_level) + +# Global variables +ORGANIZATIONS_PAGE_SIZE = 20 +ORGANIZATIONS_THROTTLE_PERIOD = 0.2 + +try: + MANAGEMENT_ACCOUNT_SESSION = boto3.Session() + ORG_CLIENT: OrganizationsClient = MANAGEMENT_ACCOUNT_SESSION.client("organizations") + SSM_CLIENT: SSMClient = MANAGEMENT_ACCOUNT_SESSION.client("ssm") +except Exception as error: + LOGGER.error({"Unexpected_Error": error}) + raise ValueError("Unexpected error executing Lambda function. Review CloudWatch logs for details.") from None + + +def assume_role( + role: str, + role_session_name: str, + account: str = None, + session: boto3.Session = None, +) -> boto3.Session: + """Assumes the provided role in the given account and returns a session. + + Args: + role: Role to assume in target account. + role_session_name: Identifier for the assumed role session. + account: AWS account number. Defaults to None. + session: Boto3 session. Defaults to None. + + Returns: + Session object for the specified AWS account + """ + if not session: + session = boto3.Session() + sts_client: STSClient = session.client("sts") + sts_arn = sts_client.get_caller_identity()["Arn"] + LOGGER.info(f"USER: {sts_arn}") + if not account: + account = sts_arn.split(":")[4] + partition = sts_arn.split(":")[1] + role_arn = f"arn:{partition}:iam::{account}:role/{role}" + + response = sts_client.assume_role(RoleArn=role_arn, RoleSessionName=role_session_name) + LOGGER.info(f"ASSUMED ROLE: {response['AssumedRoleUser']['Arn']}") + return boto3.Session( + aws_access_key_id=response["Credentials"]["AccessKeyId"], + aws_secret_access_key=response["Credentials"]["SecretAccessKey"], + aws_session_token=response["Credentials"]["SessionToken"], + ) + + +def get_active_organization_accounts(exclude_accounts: list = None) -> list: + """Get all the active AWS Organization accounts. + + Args: + exclude_accounts: list of account IDs to exclude + + Returns: + List of active account IDs + """ + if exclude_accounts is None: + exclude_accounts = ["00000000000"] + accounts: list[dict] = [] + paginator = ORG_CLIENT.get_paginator("list_accounts") + + for page in paginator.paginate(PaginationConfig={"PageSize": ORGANIZATIONS_PAGE_SIZE}): + for account in page["Accounts"]: + if account["Status"] == "ACTIVE" and account["Id"] not in exclude_accounts: + accounts.append({"AccountId": account["Id"], "Email": account["Email"]}) + sleep(ORGANIZATIONS_THROTTLE_PERIOD) + + return accounts + + +def get_control_tower_regions() -> list: # noqa: CCR001 + """Query SSM Parameter Store to identify customer regions. + + Returns: + Customer regions + """ + ssm_response = SSM_CLIENT.get_parameter(Name="/sra/regions/customer-control-tower-regions") + customer_regions = ssm_response["Parameter"]["Value"].split(",") + + return list(customer_regions) + + +def get_enabled_regions(customer_regions: str, control_tower_regions_only: bool = False) -> list: # noqa: CCR001, C901 + """Query STS to identify enabled regions. + + Args: + customer_regions: customer provided comma delimited string of regions + control_tower_regions_only: Use the Control Tower governed regions. Defaults to False. + + Returns: + Enabled regions + """ + if customer_regions.strip(): + LOGGER.info({"CUSTOMER PROVIDED REGIONS": customer_regions}) + region_list = [] + for region in customer_regions.split(","): + if region != "": + region_list.append(region.strip()) + elif control_tower_regions_only: + region_list = get_control_tower_regions() + else: + default_available_regions = [] + for region in boto3.client("account").list_regions(RegionOptStatusContains=["ENABLED", "ENABLED_BY_DEFAULT"])["Regions"]: + default_available_regions.append(region["RegionName"]) + + LOGGER.info({"Default_Available_Regions": default_available_regions}) + region_list = default_available_regions + + region_session = boto3.Session() + enabled_regions = [] + disabled_regions = [] + invalid_regions = [] + for region in region_list: + try: + sts_client = region_session.client( + "sts", + endpoint_url=f"https://sts.{region}.amazonaws.com", + region_name=region, + ) + sts_client.get_caller_identity() + enabled_regions.append(region) + except EndpointConnectionError: + invalid_regions.append(region) + LOGGER.error(f"Region: ({region}) is not valid") + except ClientError as error: + if error.response["Error"]["Code"] == "InvalidClientTokenId": + disabled_regions.append(region) + LOGGER.error(f"Error {error.response['Error']} occurred testing region {region}") + except Exception: + LOGGER.exception("Unexpected!") + + LOGGER.info( + { + "Enabled_Regions": enabled_regions, + "Disabled_Regions": disabled_regions, + "Invalid_Regions": invalid_regions, + } + ) + return enabled_regions diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/requirements.txt b/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/requirements.txt new file mode 100644 index 00000000..b9435de8 --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/requirements.txt @@ -0,0 +1,2 @@ +#install latest +crhelper \ No newline at end of file diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/security_lake.py b/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/security_lake.py new file mode 100644 index 00000000..74ff92e7 --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/security_lake.py @@ -0,0 +1,981 @@ +"""This script performs operations to enable, configure, and disable security lake. + +Version: 1.0 +'security_lake_org' solution in the repo, https://github.com/aws-samples/aws-security-reference-architecture-examples + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from __future__ import annotations + +import logging +import os +from time import sleep +from typing import TYPE_CHECKING, List, Literal, Sequence, Union + +import boto3 +import common +from botocore.config import Config +from botocore.exceptions import ClientError + +if TYPE_CHECKING: + from mypy_boto3_glue import GlueClient + from mypy_boto3_lakeformation import LakeFormationClient + from mypy_boto3_lakeformation.type_defs import ResourceTypeDef + from mypy_boto3_organizations import OrganizationsClient + from mypy_boto3_ram import RAMClient + from mypy_boto3_ram.type_defs import ResourceShareInvitationTypeDef + from mypy_boto3_securitylake import SecurityLakeClient + from mypy_boto3_securitylake.literals import AwsLogSourceNameType + from mypy_boto3_securitylake.paginator import ListLogSourcesPaginator + from mypy_boto3_securitylake.type_defs import ( + AwsLogSourceConfigurationTypeDef, + AwsLogSourceResourceTypeDef, + CreateDataLakeResponseTypeDef, + CreateSubscriberResponseTypeDef, + DataLakeAutoEnableNewAccountConfigurationTypeDef, + ListDataLakesResponseTypeDef, + LogSourceResourceTypeDef, + ) + +LOGGER = logging.getLogger("sra") +log_level = os.environ.get("LOG_LEVEL", logging.INFO) +LOGGER.setLevel(log_level) + +BOTO3_CONFIG = Config(retries={"max_attempts": 10, "mode": "standard"}) +UNEXPECTED = "Unexpected!" +EMPTY_STRING = "" +SECURITY_LAKE_THROTTLE_PERIOD = 0.2 +ENABLE_RETRY_ATTEMPTS = 10 +ENABLE_RETRY_SLEEP_INTERVAL = 10 +MAX_RETRY = 5 +SLEEP_SECONDS = 10 +KEY = "sra-solution" +VALUE = "sra-security-lake" + +try: + MANAGEMENT_ACCOUNT_SESSION = boto3.Session() + ORG_CLIENT: OrganizationsClient = MANAGEMENT_ACCOUNT_SESSION.client("organizations") +except Exception: + LOGGER.exception(UNEXPECTED) + raise ValueError("Unexpected error executing Lambda function. Review CloudWatch logs for details.") from None + + +def check_organization_admin_enabled(delegated_admin_account_id: str, service_principal: str) -> bool: + """Check if the delegated administrator account for the provided service principal exists. + + Args: + delegated_admin_account_id: Delegated Administrator Account ID + service_principal: AWS Service Principal + + Raises: + ValueError: If the delegated administrator other than Log Archive account already exists + + Returns: + bool: True if the delegated administrator account exists, False otherwise + """ + LOGGER.info(f"Checking if delegated administrator registered for '{service_principal}' service principal.") + try: + delegated_admins = ORG_CLIENT.list_delegated_administrators(ServicePrincipal=service_principal) + api_call_details = {"API_Call": "organizations:ListDelegatedAdministrators", "API_Response": delegated_admins} + LOGGER.info(api_call_details) + if not delegated_admins["DelegatedAdministrators"]: # noqa R505 + LOGGER.info(f"Delegated administrator not registered for '{service_principal}'") + return False + elif delegated_admins["DelegatedAdministrators"][0]["Id"] == delegated_admin_account_id: + LOGGER.info(f"Log Archive account ({delegated_admin_account_id}) already registered as delegated administrator for '{service_principal}'") + return True + else: + registered_admin = delegated_admins["DelegatedAdministrators"][0]["Id"] + LOGGER.info(f"Account {registered_admin} already registered as delegated administrator") + LOGGER.info("Important: removing the delegated Security Lake admin deletes your data lake and disables it for the accounts in your org") + raise ValueError(f"Deregister account {registered_admin} to delegate administration to Log Archive account") + except ClientError as e: + LOGGER.error(f"Delegated administrator check error occurred: {e}") + return False + + +def register_delegated_admin(admin_account_id: str, region: str, service_principal: str) -> None: + """Set the delegated admin account for the given region. + + Args: + admin_account_id: Admin account ID + region: AWS Region + service_principal: AWS Service Principal + """ + sl_client: SecurityLakeClient = MANAGEMENT_ACCOUNT_SESSION.client("securitylake", region, config=BOTO3_CONFIG) # type: ignore + if not check_organization_admin_enabled(admin_account_id, service_principal): + LOGGER.info(f"Registering delegated administrator ({admin_account_id})...") + sl_client.register_data_lake_delegated_administrator(accountId=admin_account_id) + LOGGER.info(f"Account {admin_account_id} registered as delegated administrator for '{service_principal}'") + + +def check_data_lake_exists(sl_client: SecurityLakeClient, region: str, max_retries: int = MAX_RETRY, initial_delay: int = 1) -> bool: + """Check if Security Lake enabled for the given region. + + Args: + sl_client: SecurityLakeClient + region: AWS region + max_retries: maximum number of retries + initial_delay: initial delay in seconds + + Raises: + ValueError: If the maximum number of retries is reached or if the Security Lake creation failed + + Returns: + bool: True if Security Lake enabled, False otherwise + """ + status: bool = False + retry_count: int = 0 + delay: float = initial_delay + max_delay: int = 30 + while not status: + try: + response: ListDataLakesResponseTypeDef = sl_client.list_data_lakes(regions=[region]) + if not response["dataLakes"]: + break + + elif response["dataLakes"][0]["createStatus"] == "INITIALIZED": + if retry_count < max_retries: + delay = min(delay * (2**retry_count), max_delay) + LOGGER.info(f"Security Lake create status ({region}): 'INITIALIZED'. Retrying ({retry_count + 1}/{max_retries}) in {delay}...") + sleep(delay) + retry_count += 1 + elif response["dataLakes"][0]["createStatus"] == "COMPLETED": + status = True + break + elif response["dataLakes"][0]["createStatus"] == "FAILED": + raise ValueError("Security Lake creation failed") + except ClientError as e: + LOGGER.error(f"Error calling 'securitylake:ListDataLakes' ({region}): {e}...") + raise + + if not status: + LOGGER.info(f"Security Lake is not enabled ({region})") + return status + + +def check_data_lake_create_status(sl_client: SecurityLakeClient, regions: list, retries: int = 0) -> bool: + """Check Security Lake creation status for given regions. + + Args: + sl_client: boto3 client + regions: list of AWS regions + retries: Number of retries. Defaults to 0. + + Raises: + ValueError: If the maximum number of retries is reached + + Returns: + bool: True if creation completed, False otherwise + """ + all_completed: bool = False + max_retries: int = 20 + regions_status_list: list = [] + while retries < max_retries: + response: ListDataLakesResponseTypeDef = sl_client.list_data_lakes(regions=regions) + for data_lake in response["dataLakes"]: + create_status = data_lake["createStatus"] + regions_status_list.append(create_status) + if set(regions_status_list) == {"COMPLETED"}: + all_completed = True + break + if "INITIALIZED" in regions_status_list: + LOGGER.info(f"Security Lake creation status: 'INITIALIZED'. Retrying ({retries+1}/{max_retries}) in 5 seconds...") + sleep(5) + retries += 1 + status = check_data_lake_create_status(sl_client, regions, retries) + if status: + all_completed = True + break + if "FAILED" in regions_status_list: + raise ValueError("Security Lake creation failed") + + if retries >= max_retries: + raise ValueError("Security Lake status not 'COMPLETED'") + + return all_completed + + +def create_security_lake(sl_client: SecurityLakeClient, sl_configurations: list, role_arn: str) -> None: + """Create Security Lake for the given region(s). + + Args: + sl_client: boto3 client + sl_configurations: Security Lake configurations + role_arn: role arn + + Raises: + ValueError: Error creating Security Lake + """ + base_delay = 10 + max_delay = 20 + data_lake_created = False + + for attempt in range(MAX_RETRY): + try: + security_lake_response: CreateDataLakeResponseTypeDef = sl_client.create_data_lake( + configurations=sl_configurations, + metaStoreManagerRoleArn=role_arn, + tags=[ + {"key": KEY, "value": VALUE}, + ], + ) + api_call_details = {"API_Call": "securitylake:CreateDataLake", "API_Response": security_lake_response} + LOGGER.info(api_call_details) + sleep(20) + data_lake_created = True + break + + except ClientError as e: + error_code = e.response["Error"]["Code"] + if error_code in ["BadRequestException", "ConflictException"]: + error_message = str(e) + if "The CreateDataLake operation can't be used to update the settings for an existing data lake" in error_message: + raise ValueError("Security lake already exists.") from None + else: + delay = min(base_delay * (1.0**attempt), max_delay) + LOGGER.info(f"'{error_code}' occurred: {e}. Retrying ({attempt + 1}/{MAX_RETRY}) in {delay} seconds...") + sleep(delay) + else: + LOGGER.error(f"Error calling CreateDataLake: {e}") + raise + attempt += 1 + if attempt >= MAX_RETRY: + LOGGER.error("Error calling CreateDataLake") + break + if not data_lake_created: + raise ValueError("Error creating security lake") + + +def encrypt_sqs_queues(configuration_role_name: str, account: str, region: str, key_id: str) -> None: + """Encrypt Security Lake SQS queues with KMS key. + + Args: + configuration_role_name: configuration role name + account: AWS Account id + region: AWS region + key_id: KMS key id + """ + sqs_queues = [ + f"https://sqs.{region}.amazonaws.com/{account}/AmazonSecurityLakeManager-{region}-Dlq", + f"https://sqs.{region}.amazonaws.com/{account}/AmazonSecurityLakeManager-{region}-Queue", + ] + session = common.assume_role(configuration_role_name, "sra-configure-security-lake", account) + sqs_client = session.client("sqs", region) + for queue_url in sqs_queues: + try: + response = sqs_client.set_queue_attributes(QueueUrl=queue_url, Attributes={"KmsMasterKeyId": key_id}) + api_call_details = {"API_Call": "sqs:SetQueueAttributes", "API_Response": response} + LOGGER.info(api_call_details) + except ClientError as e: + LOGGER.error(e) + + +class CheckLogSourceResult: + """Log source check result.""" + + def __init__(self, source_exists: bool, accounts_to_enable: list, accounts_to_disable: list, regions_to_enable: list): + """Set result attributes. + + Args: + source_exists: source exists + accounts_to_enable: accounts to enable + accounts_to_disable: accounts to disable + regions_to_enable: regions to enable + """ + self.source_exists = source_exists + self.accounts_to_enable = accounts_to_enable + self.accounts_to_disable = accounts_to_disable + self.regions_to_enable = regions_to_enable + + +def check_log_source_enabled( + sl_client: SecurityLakeClient, + requested_accounts: list, + org_accounts: list, + requested_regions: list, + log_source_name: AwsLogSourceNameType, + log_source_version: str, +) -> CheckLogSourceResult: + """Check if AWS log and event source enabled. + + Args: + sl_client: SecurityLakeClient + requested_accounts: requested accounts + org_accounts: organization accounts + requested_regions: requested regions + log_source_name: log source name + log_source_version: log source version + + Returns: + CheckLogSourceResult + """ + accounts_to_enable: list = [] + accounts_to_disable_log_source: list = [] + regions_with_source_enabled: list = [] + list_log_sources_paginator: ListLogSourcesPaginator = sl_client.get_paginator("list_log_sources") + for page in list_log_sources_paginator.paginate( + accounts=org_accounts, + regions=requested_regions, + sources=[{"awsLogSource": {"sourceName": log_source_name, "sourceVersion": log_source_version}}], + ): + if not page["sources"]: # noqa R505 + return CheckLogSourceResult(False, requested_accounts, accounts_to_disable_log_source, requested_regions) + else: + enabled_accounts = {s["account"] for s in page["sources"] if s["account"] in org_accounts} + regions_with_source_enabled = list({s["region"] for s in page["sources"]}) + accounts_to_enable = [account for account in requested_accounts if account not in enabled_accounts] + accounts_to_disable_log_source = [account for account in enabled_accounts if account not in requested_accounts] + regions_to_enable = [region for region in requested_regions if region not in regions_with_source_enabled] + + if accounts_to_enable: + LOGGER.info(f"AWS log and event source {log_source_name} will be enabled in {', '.join(accounts_to_enable)} account(s)") + if accounts_to_disable_log_source: + LOGGER.info(f"AWS log and event source {log_source_name} will be deleted in {', '.join(accounts_to_disable_log_source)} account(s)") + if regions_to_enable: + LOGGER.info(f"AWS log and event source {log_source_name} will be enabled in {', '.join(regions_to_enable)} region(s)") + + return CheckLogSourceResult(True, accounts_to_enable, accounts_to_disable_log_source, regions_to_enable) + + +def add_aws_log_source(sl_client: SecurityLakeClient, aws_log_sources: list) -> None: + """Create AWS log and event sources. + + Args: + sl_client: boto3 client + aws_log_sources: list of AWS log and event sources + + Raises: + ClientError: Error calling CreateAwsLogSource + ValueError: Error creating log and event source + """ + create_log_source_retries = 10 + base_delay = 1 + max_delay = 30 + log_source_created = False + for attempt in range(create_log_source_retries): + try: + LOGGER.info("Configuring requested AWS log and events sources") + sl_client.create_aws_log_source(sources=aws_log_sources) + log_source_created = True + LOGGER.info("Enabled requested AWS log and event sources") + break + except ClientError as e: + error_code = e.response["Error"]["Code"] + if error_code == "ConflictException": + delay = min(base_delay * (2**attempt), max_delay) + LOGGER.info(f"'ConflictException' occurred {e}. Retrying ({attempt + 1}/{create_log_source_retries}) in {delay} seconds...") + sleep(delay) + else: + LOGGER.error(f"Error calling CreateAwsLogSource: {e}.") + raise + attempt += 1 + if log_source_created or attempt >= create_log_source_retries: + break + + if not log_source_created: + raise ValueError("Failed to create log events sources") + + +def update_aws_log_source( + sl_client: SecurityLakeClient, + requested_regions: list, + source: AwsLogSourceNameType, + requested_accounts: list, + org_accounts: list, + source_version: str, +) -> None: + """Create AWS log and event sources. + + Args: + sl_client: boto3 client + requested_regions: list of AWS regions + source: AWS log and event source name + requested_accounts: list of AWS accounts + org_accounts: list of all AWS accounts in organization + source_version: log source version + """ + result = check_log_source_enabled(sl_client, requested_accounts, org_accounts, requested_regions, source, source_version) + accounts = list(result.accounts_to_enable) + accounts_to_delete = list(result.accounts_to_disable) + regions_to_enable = list(result.regions_to_enable) + + configurations: AwsLogSourceConfigurationTypeDef = { + "accounts": requested_accounts, + "regions": requested_regions, + "sourceName": source, + "sourceVersion": source_version, + } + if result.source_exists and accounts: + configurations.update({"accounts": accounts}) + + if result.source_exists and not accounts and not regions_to_enable: + LOGGER.info("Log and event source already configured. No changes to apply") + + else: + add_aws_log_source(sl_client, [configurations]) + + if accounts_to_delete: + delete_aws_log_source(sl_client, requested_regions, source, accounts_to_delete, source_version) + + +def get_org_configuration(sl_client: SecurityLakeClient) -> tuple: + """Get Security Lake organization configuration. + + Args: + sl_client: boto3 client + + Raises: + ClientError: If there is an issue interacting with the AWS API + + Returns: + tuple: (bool, dict) + """ + try: + org_configurations = sl_client.get_data_lake_organization_configuration() + if org_configurations["autoEnableNewAccount"]: # noqa R505 + return True, org_configurations["autoEnableNewAccount"] + else: + return False, org_configurations + except ClientError as e: + error_code = e.response["Error"]["Code"] + if error_code == "ResourceNotFoundException": + return False, "ResourceNotFoundException" + else: + LOGGER.error(f"Error calling GetDataLakeConfiguration: {e}.") + raise + + +def create_organization_configuration(sl_client: SecurityLakeClient, regions: list, org_sources: list, source_version: str, retry: int = 0) -> None: + """Create Security Lake organization configuration. + + Args: + sl_client: boto3 client + regions: list of AWS regions + org_sources: list of AWS log and event sources + source_version: version of log source + retry: retry counter. Defaults to 0 + """ + sources: List[AwsLogSourceResourceTypeDef] = [{"sourceName": source, "sourceVersion": source_version} for source in org_sources] + auto_enable_config: List[DataLakeAutoEnableNewAccountConfigurationTypeDef] = [] + for region in regions: + region_config: DataLakeAutoEnableNewAccountConfigurationTypeDef = {"region": region, "sources": sources} + auto_enable_config.append(region_config) + if retry < MAX_RETRY: + try: + sl_client.create_data_lake_organization_configuration(autoEnableNewAccount=auto_enable_config) + except sl_client.exceptions.ConflictException: + LOGGER.info("'ConflictException' occurred. Retrying...") + sleep(SLEEP_SECONDS) + create_organization_configuration(sl_client, regions, org_sources, source_version, retry + 1) + + +def set_sources_to_disable(org_configurations: list, region: str) -> list: + """Update Security Lake. + + Args: + org_configurations: list of configurations + region: AWS region + + Returns: + list: list of sources to disable + """ + sources_to_disable = [] + for configuration in org_configurations: + if configuration["region"] == region: + for source in configuration["sources"]: + sources_to_disable.append(source) + + return sources_to_disable + + +def update_organization_configuration( + sl_client: SecurityLakeClient, regions: list, org_sources: list, source_version: str, existing_org_configuration: list +) -> None: + """Update Security Lake organization configuration. + + Args: + sl_client: boto3 client + regions: list of AWS regions + org_sources: list of AWS log and event sources + source_version: version of log source + existing_org_configuration: list of existing configurations + """ + delete_organization_configuration(sl_client, existing_org_configuration) + sources: List[AwsLogSourceResourceTypeDef] = [{"sourceName": source, "sourceVersion": source_version} for source in org_sources] + auto_enable_config: List[DataLakeAutoEnableNewAccountConfigurationTypeDef] = [] + for region in regions: + region_config: DataLakeAutoEnableNewAccountConfigurationTypeDef = {"region": region, "sources": sources} + auto_enable_config.append(region_config) + response = sl_client.create_data_lake_organization_configuration(autoEnableNewAccount=auto_enable_config) + api_call_details = {"API_Call": "securitylake:CreateDataLakeOrganizationConfiguration", "API_Response": response} + LOGGER.info(api_call_details) + + +def delete_organization_configuration(sl_client: SecurityLakeClient, existing_org_configuration: list) -> None: + """Delete Security Lake organization configuration. + + Args: + sl_client: boto3 client + existing_org_configuration: list of existing configurations + """ + sources_to_disable = existing_org_configuration + if sources_to_disable: + delete_response = sl_client.delete_data_lake_organization_configuration(autoEnableNewAccount=existing_org_configuration) + api_call_details = {"API_Call": "securitylake:DeleteDataLakeOrganizationConfiguration", "API_Response": delete_response} + LOGGER.info(api_call_details) + + +def check_subscriber_exists(sl_client: SecurityLakeClient, subscriber_name: str, next_token: str = EMPTY_STRING) -> tuple: # noqa: CFQ004 + """List Security Lake subscribers. + + Args: + sl_client: boto3 client + subscriber_name: subscriber name + next_token: next token. Defaults to EMPTY_STRING. + + Raises: + ClientError: If there is an issue listing subscribers + + Returns: + tuple: (bool, str, str) + """ + subscriber_exists = False + subscriber_id = "" + external_id = "" + try: + if next_token != EMPTY_STRING: + response = sl_client.list_subscribers(maxResults=10, nextToken=next_token) + else: + response = sl_client.list_subscribers(maxResults=10) + if response["subscribers"]: # noqa R505 + subscriber = next((subscriber for subscriber in response["subscribers"] if subscriber_name == subscriber["subscriberName"]), None) + if subscriber: + subscriber_id = subscriber["subscriberId"] + external_id = subscriber["subscriberIdentity"]["externalId"] + subscriber_exists = True + return subscriber_exists, subscriber_id, external_id + + if "nextToken" in response: + subscriber_exists, subscriber_id, external_id = check_subscriber_exists(sl_client, subscriber_name, response["nextToken"]) + return subscriber_exists, subscriber_id, external_id + else: + return subscriber_exists, subscriber_id, external_id + + except ClientError as e: + error_code = e.response["Error"]["Code"] + if error_code == "ResourceNotFoundException": # noqa: R505 + LOGGER.info(f"Error calling ListSubscribers: {e}. Skipping...") + return subscriber_exists, subscriber_id, external_id + else: + LOGGER.error(f"Error calling ListSubscribers: {e}.") + raise + + +def get_subscriber_resourceshare_arn(sl_client: SecurityLakeClient, subscriber_name: str, next_token: str = EMPTY_STRING) -> tuple: # noqa S107 + """List Security Lake subscribers. + + Args: + sl_client: boto3 client + subscriber_name: subscriber name + next_token: next token. Defaults to EMPTY_STRING. + + Returns: + tuple: (bool, str, str) + """ + resource_share_arn = "" + subscriber_exists = False + if next_token != EMPTY_STRING: + response = sl_client.list_subscribers(maxResults=10, nextToken=next_token) + else: + response = sl_client.list_subscribers(maxResults=10) + if response["subscribers"]: # noqa R505 + for subscriber in response["subscribers"]: + if subscriber_name == subscriber["subscriberName"]: + resource_share_arn = subscriber.get("resourceShareArn", "") + subscriber_exists = True + return subscriber_exists, resource_share_arn + if "nextToken" in response: + subscriber_exists, resource_share_arn = get_subscriber_resourceshare_arn(sl_client, subscriber_name, response["nextToken"]) + return subscriber_exists, resource_share_arn + else: + return subscriber_exists, resource_share_arn + + +def create_subscribers( + sl_client: SecurityLakeClient, + data_access: Literal["LAKEFORMATION", "S3"], + source_types: list, + external_id: str, + principal: str, + subscriber_name: str, + source_version: str, +) -> tuple: + """Create Security Lake subscriber. + + Args: + sl_client: boto3 client + data_access: data access type + source_types: list of source types + external_id: external id + principal: AWS account id + subscriber_name: subscriber name + source_version: source version + + Returns: + tuple: subscriber id, resource share ARN + """ + subscriber_sources: Sequence[LogSourceResourceTypeDef] = [ + {"awsLogSource": {"sourceName": source, "sourceVersion": source_version}} for source in source_types + ] + resource_share_arn = "" + subscriber_id = "" + base_delay = 1 + max_delay = 10 + done = False + for attempt in range(ENABLE_RETRY_ATTEMPTS): + try: + response: CreateSubscriberResponseTypeDef = sl_client.create_subscriber( + accessTypes=[data_access], + sources=subscriber_sources, + subscriberIdentity={"externalId": external_id, "principal": principal}, + subscriberName=subscriber_name, + tags=[ + {"key": KEY, "value": VALUE}, + ], + ) + api_call_details = {"API_Call": "securitylake:CreateSubscriber", "API_Response": response} + LOGGER.info(api_call_details) + subscriber_id = response["subscriber"]["subscriberId"] + if data_access == "LAKEFORMATION": # noqa R505 + resource_share_arn = response["subscriber"]["resourceShareArn"] + done = True + return subscriber_id, resource_share_arn + else: + return subscriber_id, "s3_data_access" + except sl_client.exceptions.BadRequestException as e: + delay = min(base_delay * (2**attempt), max_delay) + LOGGER.info(f"'Error occurred calling CreateSubscriber: {e}. Retrying ({attempt + 1}/{ENABLE_RETRY_ATTEMPTS}) in {delay}") + sleep(delay) + + attempt += 1 + if done or attempt >= ENABLE_RETRY_ATTEMPTS: + break + + return subscriber_id, resource_share_arn + + +def update_subscriber( + sl_client: SecurityLakeClient, subscriber_id: str, source_types: list, external_id: str, principal: str, subscriber_name: str, source_version: str +) -> str: + """Update Security Lake subscriber. + + Args: + sl_client: boto3 client + subscriber_id: subscriber id + source_types: list of source types + external_id: external id + principal: AWS account id + subscriber_name: subscriber name + source_version: source version + + Returns: + str: Resource share ARN + + Raises: + ValueError: if subscriber not created + """ + subscriber_sources: Sequence[LogSourceResourceTypeDef] = [ + {"awsLogSource": {"sourceName": source, "sourceVersion": source_version}} for source in source_types + ] + base_delay = 1 + max_delay = 3 + done = False + for attempt in range(ENABLE_RETRY_ATTEMPTS): + try: + response = sl_client.update_subscriber( + sources=subscriber_sources, + subscriberId=subscriber_id, + subscriberIdentity={"externalId": external_id, "principal": principal}, + subscriberName=subscriber_name, + ) + api_call_details = {"API_Call": "securitylake:UpdateSubscriber", "API_Response": response} + LOGGER.info(api_call_details) + LOGGER.info(f"Subscriber '{subscriber_name}' updated") + if response["subscriber"]["accessTypes"] == ["LAKEFORMATION"]: + resource_share_arn = response["subscriber"]["resourceShareArn"] + sleep(SLEEP_SECONDS) + done = True + return resource_share_arn + return "s3_data_access" + except sl_client.exceptions.BadRequestException: + delay = min(base_delay * (2**attempt), max_delay) + LOGGER.info(f"'BadRequestException' occurred calling UpdateSubscriber. Retrying ({attempt + 1}/{ENABLE_RETRY_ATTEMPTS}) in {delay}") + sleep(delay) + + attempt += 1 + if done or attempt >= ENABLE_RETRY_ATTEMPTS: + break + if not done: + raise ValueError("Subscriber not updated") + + return resource_share_arn + + +def configure_resource_share_in_subscriber_acct(ram_client: RAMClient, resource_share_arn: str) -> None: + """Accept resource share invitation in subscriber account. + + Args: + ram_client: boto3 client + resource_share_arn: resource share arn + + Raises: + ValueError: If there is an issue interacting with the AWS API + """ + base_delay = 0.5 + max_delay = 5 + invitation_accepted = False + for attempt in range(MAX_RETRY): + paginator = ram_client.get_paginator("get_resource_share_invitations") + invitation = next( + ( + inv + for page in paginator.paginate(PaginationConfig={"PageSize": 20}) + for inv in page["resourceShareInvitations"] + if resource_share_arn == inv["resourceShareArn"] + ), + None, + ) # noqa: E501, B950 + + if invitation: + if invitation["status"] == "PENDING": + accept_resource_share_invitation(ram_client, invitation) + delay = min(base_delay * (2**attempt), max_delay) + sleep(delay) + if invitation["status"] == "ACCEPTED": + invitation_accepted = True + break + else: + if check_shared_resource_exists(ram_client, resource_share_arn): + invitation_accepted = True + break + attempt += 1 + if invitation_accepted or attempt >= MAX_RETRY: + break + if not invitation_accepted: + raise ValueError("Error accepting resource share invitation") from None + + +def accept_resource_share_invitation(ram_client: RAMClient, invitation: ResourceShareInvitationTypeDef) -> None: + """Accept the resource share invitation. + + Args: + ram_client: The AWS RAM client to interact with the service. + invitation: The invitation to accept. + """ + ram_client.accept_resource_share_invitation( + resourceShareInvitationArn=invitation["resourceShareInvitationArn"], + ) + LOGGER.info(f"Accepted resource share invitation: {invitation['resourceShareInvitationArn']}") + + +def check_shared_resource_exists(ram_client: RAMClient, resource_share_arn: str) -> bool: + """Check if a shared resource exists in the organization that has AWS RAM access enabled. + + Args: + ram_client: The AWS RAM client to interact with the service. + resource_share_arn: The ARN (Amazon Resource Name) of the shared resource. + + Returns: + bool: True or False. + """ + response = ram_client.list_resources(resourceOwner="OTHER-ACCOUNTS", resourceShareArns=[resource_share_arn]) + if response["resources"]: + return True + return False + + +def get_shared_resource_names(ram_client: RAMClient, resource_share_arn: str) -> tuple: + """Get resource names from resource share arn. + + Args: + ram_client: boto3 client + resource_share_arn: resource share arn + + Returns: + tuple: database name and table names + """ + db_name = "" + table_names = [] + retry = 0 + resources_created = False + LOGGER.info("Getting shared resources") + while retry < MAX_RETRY: + response = ram_client.list_resources(resourceOwner="OTHER-ACCOUNTS", resourceShareArns=[resource_share_arn]) + if response["resources"]: + db_name = next((resource["arn"].split("/")[-1] for resource in response["resources"] if resource["type"] == "glue:Database"), "") + table_names = [resource["arn"].split("/")[-1] for resource in response["resources"] if resource["type"] == "glue:Table"] + resources_created = True + break + else: + LOGGER.info(f"No shared resources found. Retrying {retry+1}") + retry += 1 + sleep(SLEEP_SECONDS) + if not resources_created: + LOGGER.error("Max retries reached. Unable to retrieve resource names.") + return db_name, table_names + + +def create_db_in_data_catalog(glue_client: GlueClient, subscriber_acct: str, shared_db_name: str, region: str, role_name: str) -> None: + """Create database in data catalog. + + Args: + glue_client: boto3 client + subscriber_acct: Security Lake query access subscriber AWS account id + shared_db_name: name of shared database + role_name: subscriber configuration role name + region: AWS region + + Raises: + ClientError: If there is an issue interacting with the AWS API + """ + try: + response = glue_client.create_database( + CatalogId=subscriber_acct, DatabaseInput={"Name": shared_db_name + "_subscriber", "CreateTableDefaultPermissions": []} + ) + api_call_details = {"API_Call": "glue:CreateDatabase", "API_Response": response} + LOGGER.info(api_call_details) + except ClientError as e: + error_code = e.response["Error"]["Code"] + if error_code == "AlreadyExistsException": + LOGGER.info(f"Database '{shared_db_name}_subscriber' already exists") + else: + LOGGER.error(f"Error calling CreateDatabase: {e}") + raise + subscriber_session = common.assume_role(role_name, "sra-configure-resource-link", subscriber_acct) + lf_client = subscriber_session.client("lakeformation", region) + set_lake_formation_permissions(lf_client, subscriber_acct, shared_db_name) + + +def create_table_in_data_catalog(glue_client: GlueClient, shared_db_name: str, shared_table_names: str, security_lake_acct: str, region: str) -> None: + """Create table in data catalog. + + Args: + glue_client: boto3 client + shared_db_name: name of shared database + shared_table_names: name of shared tables + security_lake_acct: Security Lake delegated administrator AWS account id + region: AWS region + + Raises: + ValueError: If there is an creating Glue table + """ + for table in shared_table_names: + table_name = "rl_" + table + try: + response = glue_client.create_table( + DatabaseName=shared_db_name + "_subscriber", + TableInput={ + "Name": table_name, + "TargetTable": {"CatalogId": security_lake_acct, "DatabaseName": shared_db_name, "Name": table}, + }, + ) + api_call_details = {"API_Call": "glue:CreateTable", "API_Response": response} + LOGGER.info(api_call_details) + except ClientError as e: + error_code = e.response["Error"]["Code"] + if error_code == "AlreadyExistsException": + LOGGER.info(f"Table '{table_name}' already exists in {region} region.") + continue + if error_code == "AccessDeniedException": # noqa R505 + LOGGER.info("'AccessDeniedException' error occurred. Review and update Lake Formation permission(s)") + LOGGER.info("Skipping...") + continue + else: + raise ValueError(f"Error calling glue:CreateTable {e}") from None + + +def set_lake_formation_permissions(lf_client: LakeFormationClient, account: str, db_name: str) -> None: + """Set Lake Formation permissions. + + Args: + lf_client: boto3 client + account: AWS account + db_name: database name + + Raises: + ClientError: If there is an issue interacting with the AWS API + + """ + LOGGER.info("Setting lakeformation permissions for db") + try: + resource: Union[ResourceTypeDef] = { + "Database": {"CatalogId": account, "Name": db_name + "_subscriber"}, + "Table": {"CatalogId": account, "DatabaseName": db_name + "_subscriber", "Name": "rl_*"}, + } + lf_client.grant_permissions( + CatalogId=account, + Principal={"DataLakePrincipalIdentifier": f"arn:aws:iam::{account}:role/sra-security-lake-query-subscriber"}, + Resource=resource, + Permissions=["ALL"], + PermissionsWithGrantOption=["ALL"], + ) + except ClientError as e: + LOGGER.error(f"Error calling GrantPermissions {e}.") + raise + + +def delete_subscriber(sl_client: SecurityLakeClient, subscriber_name: str, region: str) -> None: + """Delete Security Lake subscriber. + + Args: + sl_client: boto3 client + subscriber_name: subscriber name + region: AWS region + """ + subscriber_exists, subscriber_id, _ = check_subscriber_exists(sl_client, subscriber_name) + LOGGER.info(f"Subscriber exists: {subscriber_exists}. Subscriber name {subscriber_name} sub id {subscriber_id}") + if subscriber_exists: + + try: + response = sl_client.delete_subscriber(subscriberId=subscriber_id) + api_call_details = {"API_Call": "securitylake:DeleteSubscriber", "API_Response": response} + LOGGER.info(api_call_details) + except sl_client.exceptions.ResourceNotFoundException as e: + LOGGER.info(f"Subscriber not found in {region} region. {e}") + pass + else: + LOGGER.info(f"Subscriber not found in {region} region. Skipping delete subscriber...") + + +def delete_aws_log_source(sl_client: SecurityLakeClient, regions: list, source: AwsLogSourceNameType, accounts: list, source_version: str) -> None: + """Delete AWS log and event source. + + Args: + sl_client: boto3 client + regions: list of AWS regions + source: AWS log source name + accounts: list of AWS accounts + source_version: AWS log source version + + Raises: + ClientError: If there is an issue interacting with the AWS API. + """ + configurations: AwsLogSourceConfigurationTypeDef = { + "accounts": accounts, + "regions": regions, + "sourceName": source, + "sourceVersion": source_version, + } + try: + sl_client.delete_aws_log_source(sources=[configurations]) + LOGGER.info(f"Deleted AWS log source {source} in {', '.join(accounts)} account(s) {', '.join(regions)} region(s)...") + except ClientError as e: + error_code = e.response["Error"]["Code"] + if error_code == "UnauthorizedException": + LOGGER.info("'UnauthorizedException' occurred....") + else: + LOGGER.error(f"Error calling DeleteAwsLogSource {e}.") + raise diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/sra_ssm_params.py b/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/sra_ssm_params.py new file mode 100644 index 00000000..62411a46 --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/lambda/src/sra_ssm_params.py @@ -0,0 +1,65 @@ +"""Custom Resource to gather data and create SSM paramters in the management account. + +Version: 1.0 + +'common_prerequisites' solution in the repo, https://github.com/aws-samples/aws-security-reference-architecture-examples + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import boto3 +from botocore.config import Config + +if TYPE_CHECKING: + from mypy_boto3_ssm.client import SSMClient + + +class SraSsmParams: + """SRA SSM parameter values.""" + + def __init__(self, logger: Any) -> None: + """Get SSM parameter values. + + Args: + logger: logger + + Raises: + ValueError: Unexpected error executing Lambda function. Review CloudWatch logs for details. + """ + self.LOGGER = logger + + # Global Variables + self.UNEXPECTED = "Unexpected!" + self.BOTO3_CONFIG = Config(retries={"max_attempts": 10, "mode": "standard"}) + + try: + management_account_session = boto3.Session() + self.SSM_CLIENT: SSMClient = management_account_session.client("ssm") + except Exception: + self.LOGGER.exception(self.UNEXPECTED) + raise ValueError("Unexpected error executing Lambda function. Review CloudWatch logs for details.") from None + + def get_security_acct(self) -> str: + """Query SSM Parameter Store to identify security tooling account id. + + Returns: + Security tooling account id + """ + self.LOGGER.info("Getting security tooling (audit) account id") + ssm_response = self.SSM_CLIENT.get_parameter(Name="/sra/control-tower/audit-account-id") + return ssm_response["Parameter"]["Value"] + + def get_home_region(self) -> str: + """Query SSM Parameter Store to identify home region. + + Returns: + Home region + """ + ssm_response = self.SSM_CLIENT.get_parameter( + Name="/sra/control-tower/home-region", + ) + return ssm_response["Parameter"]["Value"] diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/layer/boto3/package.txt b/aws_sra_examples/solutions/security_lake/security_lake_org/layer/boto3/package.txt new file mode 100644 index 00000000..1db657b6 --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/layer/boto3/package.txt @@ -0,0 +1 @@ +boto3 \ No newline at end of file diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-lakeformation-slr.yaml b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-lakeformation-slr.yaml new file mode 100644 index 00000000..fb1b3c31 --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-lakeformation-slr.yaml @@ -0,0 +1,19 @@ +######################################################################## +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +######################################################################## +AWSTemplateFormatVersion: 2010-09-09 +Description: + This template creates an IAM role to configure the delegated administrator account - - 'security_lake_org' solution in the repo, + https://github.com/aws-samples/aws-security-reference-architecture-examples (sra-1u3sd7f8p) + +Metadata: + SRA: + Version: 1.0 + Order: 2 + +Resources: + rLakeFormationServiceLinkedRole: + Type: AWS::IAM::ServiceLinkedRole + Properties: + AWSServiceName: lakeformation.amazonaws.com diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-meta-store-manager-role.yaml b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-meta-store-manager-role.yaml new file mode 100644 index 00000000..19e4a9d4 --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-meta-store-manager-role.yaml @@ -0,0 +1,76 @@ +######################################################################## +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +######################################################################## +AWSTemplateFormatVersion: 2010-09-09 +Description: + This template creates an IAM role to configure the delegated administrator account - - 'security_lake_org' solution in the repo, + https://github.com/aws-samples/aws-security-reference-architecture-examples (sra-1u3sd7f8p) + +Metadata: + SRA: + Version: 1.0 + Order: 2 + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: General Properties + Parameters: + - pSRASolutionName + + - Label: + default: Role Properties + Parameters: + - pSRASecurityLakeMetaStoreManagerRoleName + + ParameterLabels: + pSRASecurityLakeMetaStoreManagerRoleName: + default: SecurityLakeMetaStoreManager Role Name + +Parameters: + pSRASecurityLakeMetaStoreManagerRoleName: + AllowedPattern: '^[\w+=,.@-]{1,64}$' + ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [+, =, ., @, -] + Default: AmazonSecurityLakeMetaStoreManagerV2 + Description: SecurityLakeMetaStoreManagerRole + Type: String + pSRASolutionName: + AllowedValues: [sra-security-lake-org] + Default: sra-security-lake-org + Description: The SRA solution name. The default value is the folder name of the solution + Type: String + +Resources: + rSecurityLakeMetaStoreManagerRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Ref pSRASecurityLakeMetaStoreManagerRoleName + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + Path: '/service-role/' + ManagedPolicyArns: + - !Sub arn:${AWS::Partition}:iam::${AWS::Partition}:policy/service-role/AmazonSecurityLakeMetaStoreManager + Policies: + - PolicyName: sra-security-lake-org-kms-policy + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AllowKmsDecrypt + Effect: Allow + Action: + - kms:Decrypt + - kms:RetireGrant + Resource: "*" + Condition: + ForAllValues:StringEquals: + kms:RequestAlias: + - alias/sra-security-lake-org-* + - alias/aws/lambda + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-configuration-role.yaml b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-configuration-role.yaml new file mode 100644 index 00000000..51886474 --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-configuration-role.yaml @@ -0,0 +1,187 @@ +######################################################################## +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +######################################################################## +AWSTemplateFormatVersion: 2010-09-09 +Description: + This template creates an IAM role to configure the delegated administrator account - - 'security_lake_org' solution in the repo, + https://github.com/aws-samples/aws-security-reference-architecture-examples (sra-1u3sd7f8p) + +Metadata: + SRA: + Version: 1.0 + Order: 2 + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: General Properties + Parameters: + - pSRASolutionName + + - Label: + default: Role Properties + Parameters: + - pSecurityLakeConfigurationRoleName + - pSecurityLakeOrgLambdaRoleName + - pManagementAccountId + - pAuditAccountQuerySubscriberExternalId + + ParameterLabels: + pManagementAccountId: + default: Organization Management Account ID + pSecurityLakeOrgLambdaRoleName: + default: Lambda Role Name + pSecurityLakeConfigurationRoleName: + default: Security Lake Configuration Role Name + pSRASolutionName: + default: SRA Solution Name + pAuditAccountQuerySubscriberExternalId: + default: Audit Account Query Subscriber External ID + +Parameters: + pManagementAccountId: + AllowedPattern: '^\d{12}$' + ConstraintDescription: Must be 12 digits + Description: Organization Management Account ID + Type: String + pSecurityLakeOrgLambdaRoleName: + AllowedPattern: '^[\w+=,.@-]{1,64}$' + ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [+, =, ., @, -] + Default: sra-security-lake-org-lambda + Description: Lambda Role Name + Type: String + pSecurityLakeConfigurationRoleName: + AllowedPattern: '^[\w+=,.@-]{1,64}$' + ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [+, =, ., @, -] + Default: sra-security-lake-org-configuration + Description: Security Lake Configuration IAM Role Name + Type: String + pSRASolutionName: + AllowedValues: [sra-security-lake-org] + Default: sra-security-lake-org + Description: The SRA solution name. The default value is the folder name of the solution + Type: String + pAuditAccountQuerySubscriberExternalId: + AllowedPattern: ^(?:[a-zA-Z0-9]{0,64})?$ + ConstraintDescription: All characters allowed except '&<>\%|' + Default: '' + Description: (Optional) External ID for Security Lake Audit (Security Tooling) query access subscriber. If 'Register Audit (Security Tooling) account as a Subscriber with Query Access' parameter is set to 'true', then this parameter becomes required. + Type: String + + +Resources: + rConfigurationRole: + Type: AWS::IAM::Role + Metadata: + cfn_nag: + rules_to_suppress: + - id: W11 + reason: Actions require * in resource + - id: W28 + reason: Explicit role name provided + Properties: + RoleName: !Ref pSecurityLakeConfigurationRoleName + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: sts:AssumeRole + Condition: + StringEquals: + aws:PrincipalArn: + - !Sub arn:${AWS::Partition}:iam::${pManagementAccountId}:role/${pSecurityLakeOrgLambdaRoleName} + Principal: + AWS: + - !Sub arn:${AWS::Partition}:iam::${pManagementAccountId}:root + Path: '/' + ManagedPolicyArns: + - !Sub arn:${AWS::Partition}:iam::${AWS::Partition}:policy/AmazonSecurityLakeAdministrator + Policies: + - PolicyName: sra-security-lake-org-policy-lakeformation + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AllowPutLakeFormationSettings + Effect: Allow + Action: lakeformation:PutDataLakeSettings + Resource: "*" + Condition: + ForAnyValue:StringEquals: + aws:CalledVia: securitylake.amazonaws.com + - Sid: AllowActions + Effect: Allow + Action: + - lakeformation:RevokePermissions + Resource: "*" + - PolicyName: sra-security-lake-org-policy-cloudformation + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AllowCloudformationAction + Effect: Allow + Action: + - cloudformation:DescribeStacks + - cloudformation:ListStacks + Resource: "*" + - PolicyName: sra-security-lake-org-policy-sqs + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AllowSqsActions + Effect: Allow + Action: + - sqs:SetQueueAttributes + Condition: + StringLike: + aws:ResourceAccount: "${aws:PrincipalAccount}" + Resource: !Sub arn:${AWS::Partition}:sqs:*:${AWS::AccountId}:AmazonSecurityLake* + - PolicyName: sra-security-lake-org-policy-lambda + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AllowLambdaFunctionConfigurationActions + Effect: Allow + Action: + - lambda:GetFunctionConfiguration + - lambda:UpdateFunctionConfiguration + Resource: "arn:aws:lambda:*:*:function:AmazonSecurityLake*" + - Sid: AllowLambdaListEventSourceMappings + Effect: Allow + Action: + - lambda:ListEventSourceMappings + Resource: "*" + - PolicyName: sra-security-lake-org-policy-glue + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AllowGluePolicyActions + Effect: Allow + Action: + - glue:PutResourcePolicy + - glue:DeleteResourcePolicy + Resource: + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:catalog + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:table/amazon_security_lake_glue_db_*/* + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:database/amazon_security_lake_glue_db_* + - PolicyName: sra-security-lake-org-policy-ram + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AllowRamActions + Effect: Allow + Action: + - ram:GetResourceShares + Resource: !Sub arn:${AWS::Partition}:ram:*:${AWS::AccountId}:resource-share/* + + - Sid: AllowResourceShareActions + Effect: Allow + Action: + - ram:UpdateResourceShare + - ram:DisassociateResourceShare + Resource: !Sub arn:${AWS::Partition}:ram:*:${AWS::AccountId}:resource-share/* + Condition: + StringLike: + ram:ResourceShareName: !Sub "*-${pAuditAccountQuerySubscriberExternalId}" + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-configuration.yaml b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-configuration.yaml new file mode 100644 index 00000000..0d62d870 --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-configuration.yaml @@ -0,0 +1,807 @@ +######################################################################## +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +######################################################################## +AWSTemplateFormatVersion: 2010-09-09 +Description: + This template creates a custom resource Lambda to delegate administration and configure Security Lake within an AWS Organization - 'security_lake_org' + solution in the repo, https://github.com/aws-samples/aws-security-reference-architecture-examples (sra-1u3sd7f8p) + +Metadata: + SRA: + Version: 1.0 + Order: 3 + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: General Properties + Parameters: + - pSRASolutionName + - pSRAStagingS3BucketName + - pSRAAlarmEmail + - pOrganizationId + + - Label: + default: Lambda Function Properties + Parameters: + - pSecurityLakeOrgLambdaRoleName + - pSecurityLakeOrgLambdaFunctionName + + - Label: + default: Custom Resource Properties + Parameters: + - pControlTowerRegionsOnly + - pSecurityLakeConfigurationRoleName + - pSecurityLakeSubscriberRoleName + - pDelegatedAdminAccountId + - pEnabledRegions + - pCreateOrganizationConfiguration + - pOrgConfigurationSources + - pSourceVersion + - pCloudTrailManagementEvents + - pCloudTrailLambdaDataEvents + - pCloudTrailS3DataEvents + - pSecurityHubFindings + - pVpcFlowLogs + - pWafLogs + - pRoute53Logs + - pEksAuditLogs + - pRegisterAuditAccountDataSubscriber + - pRegisterAuditAccountQuerySubscriber + - pAuditAccountDataSubscriberPrefix + - pAuditAccountDataSubscriberExternalId + - pAuditAccountQuerySubscriberPrefix + - pAuditAccountQuerySubscriberExternalId + - pDisableSecurityLake + - pSRASecurityLakeMetaStoreManagerRoleName + - pCreateResourceLink + - pSecurityLakeOrgKeyAlias + + - Label: + default: General Lambda Function Properties + Parameters: + - pCreateLambdaLogGroup + - pLambdaLogGroupRetention + - pLambdaLogGroupKmsKey + - pLambdaLogLevel + + - Label: + default: EventBridge Rule Properties + Parameters: + - pComplianceFrequency + - pControlTowerLifeCycleRuleName + + ParameterLabels: + pCreateResourceLink: + default: Create Resource Link + pSecurityLakeOrgKeyAlias: + default: Security Lake KMS Key Alias + pSRASecurityLakeMetaStoreManagerRoleName: + default: SecurityLakeMetaStoreManagerRole + pCloudTrailManagementEvents: + default: CloudTrail - Management events + pSourceVersion: + default: Log Source Version + pCloudTrailLambdaDataEvents: + default: CloudTrail - Lambda Data events + pCloudTrailS3DataEvents: + default: CloudTrail - S3 Data events + pSecurityHubFindings: + default: SecurityHub Findings + pVpcFlowLogs: + default: VPC Flow Logs + pWafLogs: + default: WAFv2 Logs + pRoute53Logs: + default: Amazon Route 53 resolver query logs + pEksAuditLogs: + default: Amazon EKS Audit Logs + pControlTowerRegionsOnly: + default: Governed Regions Only + pSecurityLakeConfigurationRoleName: + default: Security Lake Configuration Role Name + pSecurityLakeSubscriberRoleName: + default: Security Lake Query Subscriber Role Name + pComplianceFrequency: + default: Frequency to Check for Organizational Compliance + pControlTowerLifeCycleRuleName: + default: Control Tower Lifecycle Rule Name + pCreateLambdaLogGroup: + default: Create Lambda Log Group + pDelegatedAdminAccountId: + default: Delegated Admin Account ID + pEnabledRegions: + default: (Optional) Enabled Regions + pLambdaLogGroupKmsKey: + default: (Optional) Lambda Logs KMS Key + pLambdaLogGroupRetention: + default: Lambda Log Group Retention + pLambdaLogLevel: + default: Lambda Log Level + pSRAAlarmEmail: + default: (Optional) SRA Alarm Email + pSRASolutionName: + default: SRA Solution Name + pSRAStagingS3BucketName: + default: SRA Staging S3 Bucket Name + pSecurityLakeOrgLambdaFunctionName: + default: Lambda Function Name + pSecurityLakeOrgLambdaRoleName: + default: Lambda Role Name + pRegisterAuditAccountDataSubscriber: + default: Register Audit Account as a Subscriber with Data Access + pAuditAccountDataSubscriberPrefix: + default: Audit (Security Tooling) account data access subscriber name + pAuditAccountDataSubscriberExternalId: + default: Audit (Security Tooling) account data access subscriber external id + pRegisterAuditAccountQuerySubscriber: + default: Register Audit (Security Tooling) account as a subscriber with query access + pAuditAccountQuerySubscriberPrefix: + default: Audit (Security Tooling) account query access subscriber name + pAuditAccountQuerySubscriberExternalId: + default: Audit (Security Tooling) account query access subscriber external id + pOrganizationId: + default: Organization ID + pDisableSecurityLake: + default: Disable Security Lake log sources and organization configuration + +Parameters: + pCreateResourceLink: + AllowedValues: ['true', 'false'] + Default: 'true' + Description: Indicates whether to create a resource link for shared resources in Audit (Security Tooling) account + Type: String + pSecurityLakeOrgKeyAlias: + AllowedPattern: '^[a-zA-Z0-9/_-]+$' + ConstraintDescription: + The alias must be string of 1-256 characters. It can contain only alphanumeric characters, forward slashes (/), underscores (_), and dashes (-). + Default: sra-security-lake-org-key + Description: Security Lake KMS Key Alias + Type: String + pSRASecurityLakeMetaStoreManagerRoleName: + AllowedPattern: '^[\w+=,.@-]{1,64}$' + ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [+, =, ., @, -] + Default: AmazonSecurityLakeMetaStoreManagerV2 + Description: SecurityLakeMetaStoreManagerRole + Type: String + pSourceVersion: + AllowedValues: ['2.0'] + ConstraintDescription: Must be a valid version number. Currently supported version is 2.0 + Description: 'Chose the version of data source from which you want to ingest log and event sources' + Default: '2.0' + Type: String + pCloudTrailManagementEvents: + AllowedPattern: '^($|ALL|(\d{12})(,\s*\d{12})*)$' + ConstraintDescription: 'Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation' + Description: + Accounts to ingest CloudTrail - Management events from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma + separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: 'ALL' + pCloudTrailLambdaDataEvents: + AllowedPattern: '^($|ALL|(\d{12})(,\s*\d{12})*)$' + ConstraintDescription: 'Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation' + Description: + Accounts to ingest CloudTrail - Lambda Data events from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma + separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: 'ALL' + pCloudTrailS3DataEvents: + AllowedPattern: '^($|ALL|(\d{12})(,\s*\d{12})*)$' + ConstraintDescription: 'Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation' + Description: + Accounts to ingest CloudTrail - S3 Data events from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma + separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: '' + pSecurityHubFindings: + AllowedPattern: '^($|ALL|(\d{12})(,\s*\d{12})*)$' + ConstraintDescription: 'Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation' + Description: + Accounts to ingest SecurityHub Findings from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma + separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: 'ALL' + pVpcFlowLogs: + AllowedPattern: '^($|ALL|(\d{12})(,\s*\d{12})*)$' + ConstraintDescription: 'Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation' + Description: + Accounts to ingest VPC Flow Logs from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma + separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: 'ALL' + pWafLogs: + AllowedPattern: '^($|ALL|(\d{12})(,\s*\d{12})*)$' + ConstraintDescription: 'Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation' + Description: + Accounts to ingest WAFv2 Logs from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma + separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: '' + pRoute53Logs: + AllowedPattern: '^($|ALL|(\d{12})(,\s*\d{12})*)$' + ConstraintDescription: 'Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation' + Description: + Accounts to ingest Amazon Route 53 resolver query logs from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma + separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: 'ALL' + pEksAuditLogs: + AllowedPattern: '^($|ALL|(\d{12})(,\s*\d{12})*)$' + ConstraintDescription: 'Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation' + Description: + Accounts to ingest Amazon EKS Audit Logs from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma + separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: 'ALL' + pControlTowerRegionsOnly: + AllowedValues: ['true', 'false'] + Default: 'false' + Description: Only enable in the customer governed regions specified in Control Tower or Common Prerequisites solution + Type: String + pComplianceFrequency: + ConstraintDescription: Compliance Frequency must be a number between 1 and 30, inclusive. + Default: 7 + Description: Frequency (in days between 1 and 30, default is 7) to check organizational compliance + MinValue: 1 + MaxValue: 30 + Type: Number + pControlTowerLifeCycleRuleName: + AllowedPattern: '^[\w.-]{1,64}$' + ConstraintDescription: Max 64 alphanumeric and underscore characters. Also special characters supported [., -] + Default: sra-security-lake-org-trigger + Description: The name of the AWS Control Tower Life Cycle Rule. + Type: String + pCreateLambdaLogGroup: + AllowedValues: ['true', 'false'] + Default: 'false' + Description: + Indicates whether a CloudWatch Log Group should be explicitly created for the Lambda function, to allow for setting a Log Retention and/or KMS + Key for encryption. + Type: String + pDelegatedAdminAccountId: + AllowedPattern: '^\d{12}$' + ConstraintDescription: Must be 12 digits + Description: Delegated administrator account ID - Log Archive account + Type: String + pEnabledRegions: + AllowedPattern: '^$|^([a-z0-9-]{1,64})$|^(([a-z0-9-]{1,64},)*[a-z0-9-]{1,64})$' + ConstraintDescription: + Only lowercase letters, numbers, and hyphens ('-') allowed. (e.g. us-east-1) Additional AWS regions can be provided, separated by commas. (e.g. + us-east-1,ap-southeast-2) + Description: (Optional) Enabled regions (AWS regions, separated by commas). Leave blank to enable all regions. + Type: String + pSecurityLakeOrgLambdaFunctionName: + AllowedPattern: '^[\w-]{0,64}$' + ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [_, -] + Default: sra-security-lake-org + Description: Lambda function name + Type: String + pSecurityLakeOrgLambdaRoleName: + AllowedPattern: '^[\w+=,.@-]{1,64}$' + ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [+, =, ., @, -] + Default: sra-security-lake-org-lambda + Description: Security Lake configuration Lambda role name + Type: String + pSecurityLakeConfigurationRoleName: + AllowedPattern: '^[\w+=,.@-]{1,64}$' + ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [+, =, ., @, -] + Default: sra-security-lake-org-configuration + Description: Security Lake Configuration role to assume in the delegated administrator account + Type: String + pSecurityLakeSubscriberRoleName: + AllowedPattern: '^[\w+=,.@-]{1,64}$' + ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [+, =, ., @, -] + Default: sra-security-lake-query-subscriber + Description: Security Lake Configuration role to assume in the delegated administrator account + Type: String + pLambdaLogGroupKmsKey: + AllowedPattern: '^$|^arn:(aws[a-zA-Z-]*){1}:kms:[a-z0-9-]+:\d{12}:key\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$' + ConstraintDescription: 'Key ARN example: arn:aws:kms:::key/1234abcd-12ab-34cd-56ef-1234567890ab' + Description: + (Optional) KMS Key ARN to use for encrypting the Lambda logs data. If empty, encryption is enabled with CloudWatch Logs managing the server-side + encryption keys. + Type: String + pLambdaLogGroupRetention: + AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653] + Default: 14 + Description: Specifies the number of days you want to retain log events + Type: String + pLambdaLogLevel: + AllowedValues: [INFO, ERROR, DEBUG] + Default: INFO + Description: Lambda Function Logging Level + Type: String + pSRAAlarmEmail: + AllowedPattern: '^$|^([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)$' + ConstraintDescription: Must be a valid email address. + Description: (Optional) Email address for receiving DLQ alarms + Type: String + pSRASolutionName: + AllowedValues: [sra-security-lake-org] + Default: sra-security-lake-org + Description: The SRA solution name. The default value is the folder name of the solution + Type: String + pSRAStagingS3BucketName: + AllowedPattern: '^(?=^.{3,63}$)(?!.*[.-]{2})(?!.*[--]{2})(?!^(?:(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(?!$)|$)){4}$)(^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])$)' + ConstraintDescription: + SRA Staging S3 bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). + Description: + SRA Staging S3 bucket name for the artifacts relevant to solution. (e.g., lambda zips, CloudFormation templates) S3 bucket name can include + numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). + Type: String + pRegisterAuditAccountDataSubscriber: + AllowedValues: ['true', 'false'] + Default: 'true' + Description: Identifies whether to registerAudit (Security Tooling) account as a Subscriber with Data Access + Type: String + pAuditAccountDataSubscriberPrefix: + AllowedValues: [sra-audit-account-data-subscriber] + Default: sra-audit-account-data-subscriber + Description: The name of the Audit (Security Tooling) account data access subscriber + Type: String + pAuditAccountDataSubscriberExternalId: + AllowedPattern: ^(?:[a-zA-Z0-9]{0,64})?$ + ConstraintDescription: All characters allowed except '&<>\%|' + Default: '' + Description: + (Optional) External ID for Security Lake Audit (Security Tooling) data access subscriber. If 'Register Audit (Security Tooling) account as a Subscriber with Data Access' parameter is set to 'true', then this parameter becomes + required. + Type: String + pRegisterAuditAccountQuerySubscriber: + AllowedValues: ['true', 'false'] + Default: 'true' + Description: Identifies whether to register Audit (Security Tooling) account as a Subscriber with Query Access + Type: String + pAuditAccountQuerySubscriberPrefix: + AllowedValues: [sra-audit-account-query-subscriber] + Default: sra-audit-account-query-subscriber + Description: The name of the Audit (Security Tooling) account query access subscriber + Type: String + pAuditAccountQuerySubscriberExternalId: + AllowedPattern: ^(?:[a-zA-Z0-9]{0,64})?$ + ConstraintDescription: All characters allowed except '&<>\%|' + Default: '' + Description: + (Optional) External ID for Security Lake Audit (Security Tooling) query access subscriber. If 'Register Audit (Security Tooling) account as a Subscriber with Query Access' parameter is set to 'true', then this parameter becomes + required. + Type: String + + pOrgConfigurationSources: + AllowedValues: ['', ROUTE53, VPC_FLOW, SH_FINDINGS, CLOUD_TRAIL_MGMT, LAMBDA_EXECUTION, S3_DATA, EKS_AUDIT, WAF] + Default: ROUTE53, VPC_FLOW, SH_FINDINGS, CLOUD_TRAIL_MGMT, LAMBDA_EXECUTION, S3_DATA, EKS_AUDIT, WAF + Description: (Optional) AWS log sources to enable for new member accounts in your organization. If 'Create Organization Configuration' parameter is set to 'true', then this parameter becomes required. + Type: CommaDelimitedList + pCreateOrganizationConfiguration: + AllowedValues: ['true', 'false'] + Default: 'true' + Description: Select whether to automatically enable Amazon Security Lake for new member accounts in your organization + Type: String + pOrganizationId: + AllowedPattern: '^o-[a-z0-9]{10,32}$' + ConstraintDescription: Must start with 'o-' followed by from 10 to 32 lowercase letters or digits. (e.g. o-abc1234567) + Description: AWS Organizations ID + Type: String + pDisableSecurityLake: + AllowedValues: ['true', 'false'] + Default: 'false' + Description: Update to 'true' to disable Security Lake log sources and organization configuration before deleting the stack. + Type: String + + +Conditions: + cComplianceFrequencySingleDay: !Equals [!Ref pComplianceFrequency, 1] + cCreateDLQAlarm: !Not [!Equals [!Ref pSRAAlarmEmail, '']] + cCreateLambdaLogGroup: !Equals [!Ref pCreateLambdaLogGroup, 'true'] + cUseGraviton: !Or + - !Equals [!Ref 'AWS::Region', ap-northeast-1] + - !Equals [!Ref 'AWS::Region', ap-south-1] + - !Equals [!Ref 'AWS::Region', ap-southeast-1] + - !Equals [!Ref 'AWS::Region', ap-southeast-2] + - !Equals [!Ref 'AWS::Region', eu-central-1] + - !Equals [!Ref 'AWS::Region', eu-west-1] + - !Equals [!Ref 'AWS::Region', eu-west-2] + - !Equals [!Ref 'AWS::Region', us-east-1] + - !Equals [!Ref 'AWS::Region', us-east-2] + - !Equals [!Ref 'AWS::Region', us-west-2] + cUseKmsKey: !Not [!Equals [!Ref pLambdaLogGroupKmsKey, '']] + +Resources: + rSecurityLakeOrgLambdaLogGroup: + Type: AWS::Logs::LogGroup + Condition: cCreateLambdaLogGroup + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + LogGroupName: !Sub /aws/lambda/${pSecurityLakeOrgLambdaFunctionName} + KmsKeyId: !If + - cUseKmsKey + - !Ref pLambdaLogGroupKmsKey + - !Ref AWS::NoValue + RetentionInDays: !Ref pLambdaLogGroupRetention + + rSecurityLakeOrgLambdaRole: + Type: AWS::IAM::Role + Metadata: + cfn_nag: + rules_to_suppress: + - id: W11 + reason: Actions require wildcard in resource + - id: W28 + reason: The role name is defined + checkov: + skip: + - id: CKV_AWS_109 + comment: Actions require wildcard in resource or condition provides constraints. + - id: CKV_AWS_111 + comment: IAM write actions require wildcard in resource + Properties: + RoleName: !Ref pSecurityLakeOrgLambdaRoleName + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Path: '/' + Policies: + - PolicyName: sra-security-lake-org-policy-cloudformation + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: CloudFormation + Effect: Allow + Action: cloudformation:ListStackInstances + Resource: + - !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stackset/AWSControlTowerBP-* + - PolicyName: sra-security-lake-org-policy-securitylake + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: SecurityLakeDesignateAdministratorAccess + Effect: Allow + Action: + - securitylake:RegisterDataLakeDelegatedAdministrator + - organizations:DescribeOrganization + - organizations:EnableAWSServiceAccess + - organizations:ListDelegatedAdministrators + - organizations:ListDelegatedServicesForAccount + - organizations:RegisterDelegatedAdministrator + Resource: "*" + - Sid: AllowCreateServiceLinkedRole + Effect: Allow + Action: iam:CreateServiceLinkedRole + Condition: + StringLike: + iam:AWSServiceName: securitylake.amazonaws.com + Resource: "*" + - Sid: SecurityLakeRemoveAdministratorAccess + Effect: Allow + Action: + - organizations:DeregisterDelegatedAdministrator + Resource: "*" + - PolicyName: sra-account-alternate-contacts-policy-organizations + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: OrganizationsReadAccess + Effect: Allow + Action: + - organizations:ListAccounts + Resource: '*' + + - PolicyName: "ssm-access" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Action: + - ssm:GetParameter + - ssm:GetParameters + Resource: + - !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/sra*" + + - PolicyName: sra-security-lake-org-policy-logs + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: CreateLogGroupAndEvents + Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${pSecurityLakeOrgLambdaFunctionName}:log-stream:* + + - PolicyName: sra-security-lake-org-policy-sqs + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: SQSSendMessage + Effect: Allow + Action: sqs:SendMessage + Resource: !GetAtt rSecurityLakeOrgDLQ.Arn + + - PolicyName: sra-security-lake-org-policy-acct + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AcctListRegions + Effect: Allow + Action: + - account:ListRegions + Resource: '*' + + - PolicyName: sra-security-lake-org-policy-iam + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AssumeRole + Effect: Allow + Action: sts:AssumeRole + Condition: + StringEquals: + aws:PrincipalOrgId: !Ref pOrganizationId + Resource: + - !Sub arn:${AWS::Partition}:iam::*:role/${pSecurityLakeConfigurationRoleName} + - !Sub arn:${AWS::Partition}:iam::*:role/${pSecurityLakeSubscriberRoleName} + - Sid: AllowReadIamActions + Effect: Allow + Action: iam:GetRole + Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/* + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName + + rSecurityLakeOrgLambdaFunction: + Type: AWS::Lambda::Function + Metadata: + cfn_nag: + rules_to_suppress: + - id: W58 + reason: CloudWatch access provided by the attached IAM role + - id: W89 + reason: Lambda is not deployed within a VPC + - id: W92 + reason: Lambda does not need reserved concurrent executions. + checkov: + skip: + - id: CKV_AWS_115 + comment: Lambda does not need reserved concurrent executions. + - id: CKV_AWS_117 + comment: Lambda does not need to communicate with VPC resources. + - id: CKV_AWS_173 + comment: Environment variables are not sensitive. + Properties: + FunctionName: !Ref pSecurityLakeOrgLambdaFunctionName + Description: configure Security Lake for the Organization + Architectures: !If + - cUseGraviton + - [arm64] + - !Ref AWS::NoValue + Handler: app.lambda_handler + Role: !GetAtt rSecurityLakeOrgLambdaRole.Arn + MemorySize: 512 + Runtime: python3.9 + Timeout: 900 + Code: + S3Bucket: !Ref pSRAStagingS3BucketName + S3Key: !Sub ${pSRASolutionName}/lambda_code/${pSRASolutionName}.zip + Layers: + - !Ref rSecurityLakeOrgLambdaLayer + DeadLetterConfig: + TargetArn: !GetAtt rSecurityLakeOrgDLQ.Arn + Environment: + Variables: + LOG_LEVEL: !Ref pLambdaLogLevel + AWS_PARTITION: !Ref AWS::Partition + CONFIGURATION_ROLE_NAME: !Ref pSecurityLakeConfigurationRoleName + SUBSCRIBER_ROLE_NAME: !Ref pSecurityLakeSubscriberRoleName + CONTROL_TOWER_REGIONS_ONLY: !Ref pControlTowerRegionsOnly + DELEGATED_ADMIN_ACCOUNT_ID: !Ref pDelegatedAdminAccountId + ENABLED_REGIONS: !Ref pEnabledRegions + MANAGEMENT_ACCOUNT_ID: !Ref AWS::AccountId + SOURCE_VERSION: !Ref pSourceVersion + CLOUD_TRAIL_MGMT: !Join + - ',' + - !Ref pCloudTrailManagementEvents + LAMBDA_EXECUTION: !Join + - ',' + - !Ref pCloudTrailLambdaDataEvents + S3_DATA: !Join + - ',' + - !Ref pCloudTrailS3DataEvents + ROUTE53: !Join + - ',' + - !Ref pRoute53Logs + VPC_FLOW: !Join + - ',' + - !Ref pVpcFlowLogs + SH_FINDINGS: !Join + - ',' + - !Ref pSecurityHubFindings + EKS_AUDIT: !Join + - ',' + - !Ref pEksAuditLogs + WAF: !Join + - ',' + - !Ref pWafLogs + SET_AUDIT_ACCT_QUERY_SUBSCRIBER: !Ref pRegisterAuditAccountQuerySubscriber + SET_AUDIT_ACCT_DATA_SUBSCRIBER: !Ref pRegisterAuditAccountDataSubscriber + AUDIT_ACCT_DATA_SUBSCRIBER: !Ref pAuditAccountDataSubscriberPrefix + DATA_SUBSCRIBER_EXTERNAL_ID: !Ref pAuditAccountDataSubscriberExternalId + AUDIT_ACCT_QUERY_SUBSCRIBER: !Ref pAuditAccountQuerySubscriberPrefix + QUERY_SUBSCRIBER_EXTERNAL_ID: !Ref pAuditAccountQuerySubscriberExternalId + SET_ORG_CONFIGURATION: !Ref pCreateOrganizationConfiguration + ORG_CONFIGURATION_SOURCES: !Join + - ',' + - !Ref pOrgConfigurationSources + DISABLE_SECURITY_LAKE: !Ref pDisableSecurityLake + META_STORE_MANAGER_ROLE_NAME: !Ref pSRASecurityLakeMetaStoreManagerRoleName + CREATE_RESOURCE_LINK: !Ref pCreateResourceLink + KEY_ALIAS: !Ref pSecurityLakeOrgKeyAlias + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName + + rSecurityLakeOrgLambdaLayer: + Type: AWS::Lambda::LayerVersion + Properties: + Content: + S3Bucket: !Ref pSRAStagingS3BucketName + S3Key: !Sub ${pSRASolutionName}/layer_code/${pSRASolutionName}-layer.zip + Description: Boto3 version 1.35.10 layer to enable newer API of Security Lake + LayerName: !Sub ${pSecurityLakeOrgLambdaFunctionName}-updated-boto3-layer + + rSecurityLakeOrgLambdaCustomResource: + Type: Custom::LambdaCustomResource + Version: '1.0' + Properties: + ServiceToken: !GetAtt rSecurityLakeOrgLambdaFunction.Arn + LOG_LEVEL: !Ref pLambdaLogLevel + CONFIGURATION_ROLE_NAME: !Ref pSecurityLakeConfigurationRoleName + SUBSCRIBER_ROLE_NAME: !Ref pSecurityLakeSubscriberRoleName + CONTROL_TOWER_REGIONS_ONLY: !Ref pControlTowerRegionsOnly + DELEGATED_ADMIN_ACCOUNT_ID: !Ref pDelegatedAdminAccountId + ENABLED_REGIONS: !Ref pEnabledRegions + MANAGEMENT_ACCOUNT_ID: !Ref AWS::AccountId + AWS_PARTITION: !Ref AWS::Partition + SOURCE_VERSION: !Ref pSourceVersion + CLOUD_TRAIL_MGMT: !Join + - ',' + - !Ref pCloudTrailManagementEvents + LAMBDA_EXECUTION: !Join + - ',' + - !Ref pCloudTrailLambdaDataEvents + S3_DATA: !Join + - ',' + - !Ref pCloudTrailS3DataEvents + ROUTE53: !Join + - ',' + - !Ref pRoute53Logs + VPC_FLOW: !Join + - ',' + - !Ref pVpcFlowLogs + SH_FINDINGS: !Join + - ',' + - !Ref pSecurityHubFindings + EKS_AUDIT: !Join + - ',' + - !Ref pEksAuditLogs + WAF: !Join + - ',' + - !Ref pWafLogs + SET_AUDIT_ACCT_DATA_SUBSCRIBER: !Ref pRegisterAuditAccountDataSubscriber + SET_AUDIT_ACCT_QUERY_SUBSCRIBER: !Ref pRegisterAuditAccountQuerySubscriber + AUDIT_ACCT_DATA_SUBSCRIBER: !Ref pAuditAccountDataSubscriberPrefix + DATA_SUBSCRIBER_EXTERNAL_ID: !Ref pAuditAccountDataSubscriberExternalId + AUDIT_ACCT_QUERY_SUBSCRIBER: !Ref pAuditAccountQuerySubscriberPrefix + QUERY_SUBSCRIBER_EXTERNAL_ID: !Ref pAuditAccountQuerySubscriberExternalId + SET_ORG_CONFIGURATION: !Ref pCreateOrganizationConfiguration + ORG_CONFIGURATION_SOURCES: !Join + - ',' + - !Ref pOrgConfigurationSources + DISABLE_SECURITY_LAKE: !Ref pDisableSecurityLake + META_STORE_MANAGER_ROLE_NAME: !Ref pSRASecurityLakeMetaStoreManagerRoleName + CREATE_RESOURCE_LINK: !Ref pCreateResourceLink + KEY_ALIAS: !Ref pSecurityLakeOrgKeyAlias + + rSecurityLakeOrgDLQ: + Type: AWS::SQS::Queue + Properties: + KmsMasterKeyId: alias/aws/sqs + QueueName: !Sub ${pSRASolutionName}-dlq + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName + MessageRetentionPeriod: 345600 + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + + rSecurityLakeOrgDLQPolicy: + Type: AWS::SQS::QueuePolicy + Properties: + Queues: + - !Ref rSecurityLakeOrgDLQ + PolicyDocument: + Statement: + - Action: SQS:SendMessage + Condition: + ArnEquals: + aws:SourceArn: + - !GetAtt rSecurityLakeOrgLambdaFunction.Arn + Effect: Allow + Principal: + Service: events.amazonaws.com + Resource: + - !GetAtt rSecurityLakeOrgDLQ.Arn + + rSecurityLakeOrgDLQAlarmTopic: + Condition: cCreateDLQAlarm + Type: AWS::SNS::Topic + Properties: + DisplayName: !Sub ${pSRASolutionName}-dlq-alarm + KmsMasterKeyId: !Sub arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/sns + TopicName: !Sub ${pSRASolutionName}-dlq-alarm + Subscription: + - Endpoint: !Ref pSRAAlarmEmail + Protocol: email + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName + + rSecurityLakeOrgDLQAlarm: + Condition: cCreateDLQAlarm + Type: AWS::CloudWatch::Alarm + Properties: + AlarmDescription: SRA DLQ alarm if the queue depth is 1 + Namespace: AWS/SQS + MetricName: ApproximateNumberOfMessagesVisible + Dimensions: + - Name: QueueName + Value: !GetAtt rSecurityLakeOrgDLQ.QueueName + Statistic: Sum + Period: 300 + EvaluationPeriods: 1 + Threshold: 1 + ComparisonOperator: GreaterThanThreshold + AlarmActions: + - !Ref rSecurityLakeOrgDLQAlarmTopic + InsufficientDataActions: + - !Ref rSecurityLakeOrgDLQAlarmTopic + + rPermissionForScheduledComplianceRuleToInvokeLambda: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !GetAtt rSecurityLakeOrgLambdaFunction.Arn + Action: lambda:InvokeFunction + Principal: events.amazonaws.com + SourceArn: !GetAtt rScheduledComplianceRule.Arn + + rScheduledComplianceRule: + Type: AWS::Events::Rule + Properties: + Name: !Sub ${pControlTowerLifeCycleRuleName}-organization-compliance + Description: SRA Security Lake Trigger for scheduled organization compliance + ScheduleExpression: !If + - cComplianceFrequencySingleDay + - !Sub rate(${pComplianceFrequency} day) + - !Sub rate(${pComplianceFrequency} days) + State: ENABLED + Targets: + - Arn: !GetAtt rSecurityLakeOrgLambdaFunction.Arn + Id: !Ref pSecurityLakeOrgLambdaFunctionName + +Outputs: + oSecurityLakeOrgLambdaFunctionArn: + Description: SRA Security Lake Lambda Function ARN + Value: !GetAtt rSecurityLakeOrgLambdaFunction.Arn + oSecurityLakeOrgLambdaLogGroupArn: + Condition: cCreateLambdaLogGroup + Description: SRA Security Lake Lambda Log Group ARN + Value: !GetAtt rSecurityLakeOrgLambdaLogGroup.Arn + oSecurityLakeOrgLambdaRoleArn: + Description: SRA Security Lake Lambda Role ARN + Value: !GetAtt rSecurityLakeOrgLambdaRole.Arn diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-kms-key.yaml b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-kms-key.yaml new file mode 100644 index 00000000..6b8018b6 --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-kms-key.yaml @@ -0,0 +1,138 @@ +######################################################################## +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +######################################################################## +AWSTemplateFormatVersion: 2010-09-09 +Description: This template creates KMS key for Security Lake configurations - 'security_lake_org' solution in the repo, https://github.com/aws-samples/aws-security-reference-architecture-examples (sra-1u3sd7f8p) +Metadata: + SRA: + Version: 1 + Order: 4 + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: General Properties + Parameters: + - pSRASolutionName + - Label: + default: KMS Key Properties + Parameters: + - pSecurityLakeOrgKeyAlias + - pAuditAccountId + - pManagementAccountId + - pRegisterAuditAccountQuerySubscriber + ParameterLabels: + pAuditAccountId: + default: Audit Account ID + pSecurityLakeOrgKeyAlias: + default: Security Lake KMS Key Alias + pManagementAccountId: + default: Organization Management Account ID + pSRASolutionName: + default: SRA Solution Name + pRegisterAuditAccountQuerySubscriber: + default: Register Audit Account as Query Subscriber +Parameters: + pAuditAccountId: + AllowedPattern: '^([\w.-]{1,900})$|^(\/[\w.-]{1,900})*[\w.-]{1,900}$' + ConstraintDescription: + Must be alphanumeric or special characters [., _, -]. In addition, the slash character ( / ) used to delineate hierarchies in parameter names. + Description: AWS Account ID of the Audit (Security Tooling) account. + Type: String + pManagementAccountId: + AllowedPattern: ^\d{12}$ + ConstraintDescription: Must be 12 digits + Description: Management Account ID + Type: String + pSecurityLakeOrgKeyAlias: + Default: sra-security-lake-org-key + Description: Security Lake KMS Key Alias + Type: String + pSRASolutionName: + AllowedValues: [sra-security-lake-org] + Default: sra-security-lake-org + Description: The SRA solution name. The default value is the folder name of the solution + Type: String + pRegisterAuditAccountQuerySubscriber: + AllowedValues: ['true', 'false'] + Default: 'true' + Description: Register Audit Account as Query Subscriber + Type: String +Conditions: + cCreateQuerySubscriber: !Equals + - !Ref pRegisterAuditAccountQuerySubscriber + - 'true' +Resources: + rSecurityLakeKey: + Type: AWS::KMS::Key + DeletionPolicy: Delete + UpdateReplacePolicy: Retain + Properties: + Description: SRA Security Lake Key + EnableKeyRotation: true + KeyPolicy: + Version: 2012-10-17 + Id: !Ref pSecurityLakeOrgKeyAlias + Statement: + - Sid: Enable IAM User Permissions + Effect: Allow + Action: kms:* + Resource: '*' + Principal: + AWS: + - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root + - !Sub arn:${AWS::Partition}:iam::${pAuditAccountId}:root + - !Sub arn:${AWS::Partition}:iam::${pManagementAccountId}:root + - Sid: Enable Security Lake Role Permissions + Effect: Allow + Action: + - kms:GenerateDataKey + - kms:Decrypt + Resource: '*' + Principal: + AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/sra-security-lake-org-configuration + - Sid: Allow alias creation during setup + Effect: Allow + Action: kms:CreateAlias + Condition: + StringEquals: + kms:CallerAccount: !Sub ${AWS::AccountId} + kms:ViaService: !Sub cloudformation.${AWS::Region}.amazonaws.com + Resource: '*' + Principal: + AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root + - Sid: Allow s3 service to encrypt its events + Effect: Allow + Principal: + Service: s3.amazonaws.com + Action: + - kms:GenerateDataKey* + - kms:Decrypt + Resource: '*' + - !If + - cCreateQuerySubscriber + - Sid: Allow use of the key + Effect: Allow + Principal: + AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/aws-service-role/lakeformation.amazonaws.com/AWSServiceRoleForLakeFormationDataAccess + Action: + - kms:CreateGrant + - kms:DescribeKey + - kms:GenerateDataKey + - kms:Decrypt + Resource: '*' + - !Ref AWS::NoValue + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName + rSecurityLakeKeyAlias: + Type: AWS::KMS::Alias + Properties: + AliasName: !Sub alias/${pSecurityLakeOrgKeyAlias}-${AWS::Region} + TargetKeyId: !Ref rSecurityLakeKey +Outputs: + oSecurityLakeKeyArn: + Description: Security Lake KMS Key ARN + Value: !GetAtt rSecurityLakeKey.Arn + Export: + Name: eSecurityLakeKeyArn \ No newline at end of file diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-main-ssm.yaml b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-main-ssm.yaml new file mode 100644 index 00000000..899bdeae --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-org-main-ssm.yaml @@ -0,0 +1,709 @@ +######################################################################## +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +######################################################################## +AWSTemplateFormatVersion: 2010-09-09 +Description: This template creates a custom resource Lambda to delegate administration and configure Security Lake within an AWS Organization - 'security_lake_org' solution in the repo, https://github.com/aws-samples/aws-security-reference-architecture-examples (sra-1u3sd7f8p) +Metadata: + SRA: + Version: 1 + Entry: Parameters for deploying the solution resolving SSM parameters + Order: 1 + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: General Properties + Parameters: + - pSRASolutionName + - pSRASolutionVersion + - pSRAStagingS3BucketName + - pSRAAlarmEmail + - pAuditAccountId + - pLogArchiveAccountId + - pStackSetAdminRole + - pStackExecutionRole + - pOrganizationId + - pCustomerControlTowerRegions + - pSecurityLakeConfigurationRoleName + - Label: + default: Security Lake Configuration - Properties + Parameters: + - pDisableSecurityLake + - pControlTowerRegionsOnly + - pEnabledRegions + - pSecurityLakeOrgKeyAlias + - pSecurityLakeWarning + - pSRASecurityLakeMetaStoreManagerRoleName + - Label: + default: Security Lake Configuration - Sources to Ingest + Parameters: + - pSourceVersion + - pCloudTrailManagementEvents + - pCloudTrailLambdaDataEvents + - pCloudTrailS3DataEvents + - pSecurityHubFindings + - pVpcFlowLogs + - pWafLogs + - pRoute53Logs + - pEksAuditLogs + - Label: + default: Security Lake Configuration - Organization Configurations + Parameters: + - pCreateOrganizationConfiguration + - pOrgConfigurationSources + - Label: + default: Security Lake Configuration - Audit (Security Tooling) account Data Access Subscriber + Parameters: + - pRegisterAuditAccountDataSubscriber + - pAuditAccountDataSubscriberPrefix + - pAuditAccountDataSubscriberExternalId + - Label: + default: Security Lake Configuration - Audit (Security Tooling) account Query Access Subscriber + Parameters: + - pRegisterAuditAccountQuerySubscriber + - pCreateLakeFormationSlr + - pCreateResourceLink + - pAuditAccountQuerySubscriberPrefix + - pAuditAccountQuerySubscriberExternalId + - Label: + default: General Lambda Function Properties + Parameters: + - pCreateLambdaLogGroup + - pLambdaLogGroupRetention + - pLambdaLogGroupKmsKey + - pLambdaLogLevel + - Label: + default: EventBridge Rule Properties + Parameters: + - pControlTowerLifeCycleRuleName + - pComplianceFrequency + + ParameterLabels: + pCreateResourceLink: + default: Create resource link for shared resources + pCreateLakeFormationSlr: + default: Create AWS Lake Formation service-linked role + pSRASecurityLakeMetaStoreManagerRoleName: + default: SecurityLakeMetaStoreManagerRole Name + pCloudTrailManagementEvents: + default: CloudTrail - Management Events (recommended)) + pLogArchiveAccountId: + default: Log Archive Account ID + pCloudTrailLambdaDataEvents: + default: CloudTrail - Lambda Data Events (recommended) + pCloudTrailS3DataEvents: + default: CloudTrail - S3 Data Events (high volume data) + pCustomerControlTowerRegions: + default: Customer Regions + pSecurityHubFindings: + default: SecurityHub Findings (recommended) + pVpcFlowLogs: + default: VPC Flow Logs (recommended) + pWafLogs: + default: WAFv2 Logs (high volume data) + pRoute53Logs: + default: Amazon Route 53 Resolver Query Logs (recommended) + pEksAuditLogs: + default: Amazon EKS Audit Logs (recommended) + pOrgConfigurationSources: + default: Sources for Organization Configuration + pCreateOrganizationConfiguration: + default: Create Organization Configuration + pSourceVersion: + default: Log Source Version + pSecurityLakeConfigurationRoleName: + default: Security Lake Configuration Role Name + pSecurityLakeOrgKeyAlias: + default: Security Lake KMS Key Alias + pAuditAccountId: + default: Audit (Security Tooling) account ID + pComplianceFrequency: + default: Frequency to Check for Organizational Compliance + pControlTowerLifeCycleRuleName: + default: Control Tower Lifecycle Rule Name + pControlTowerRegionsOnly: + default: Governed Regions Only + pCreateLambdaLogGroup: + default: Create Lambda Log Group + pEnabledRegions: + default: (Optional) Enabled Regions + pLambdaLogGroupKmsKey: + default: (Optional) Lambda Logs KMS Key + pLambdaLogGroupRetention: + default: Lambda Log Group Retention + pLambdaLogLevel: + default: Lambda Log Level + pSRAAlarmEmail: + default: (Optional) SRA Alarm Email + pSRASolutionName: + default: SRA Solution Name + pSRASolutionVersion: + default: SRA Solution Version + pSRAStagingS3BucketName: + default: SRA Staging S3 Bucket Name + pRegisterAuditAccountDataSubscriber: + default: Register Audit (Security Tooling) account as a Subscriber with Data Access + pAuditAccountDataSubscriberPrefix: + default: Audit (Security Tooling) account data access subscriber name + pAuditAccountDataSubscriberExternalId: + default: Audit (Security Tooling) account data access subscriber external id + pRegisterAuditAccountQuerySubscriber: + default: Register Audit (Security Tooling) account as a subscriber with query access + pAuditAccountQuerySubscriberPrefix: + default: Audit (Security Tooling) account query access subscriber name + pAuditAccountQuerySubscriberExternalId: + default: Audit (Security Tooling) account query access subscriber external id + pStackSetAdminRole: + default: Stack Set Role + pStackExecutionRole: + default: Stack execution role + pOrganizationId: + default: Organization ID + pSecurityLakeWarning: + default: Security Lake Warning + pDisableSecurityLake: + default: Disable Security Lake log sources and organization configuration + pSecurityLakeOrgLambdaRoleName: + default: Lambda Role Name + +Parameters: + pSecurityLakeOrgLambdaRoleName: + AllowedPattern: '^[\w+=,.@-]{1,64}$' + ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [+, =, ., @, -] + Default: sra-security-lake-org-lambda + Description: Security Lake configuration Lambda role name + Type: String + pCreateResourceLink: + AllowedValues: ['true', 'false'] + Default: 'false' + Description: Indicates whether to create a resource link for shared resources in Audit (Security Tooling) account + Type: String + pCreateLakeFormationSlr: + AllowedValues: ['true', 'false'] + Default: 'true' + Description: Indicates whether a Lake Formation service-linked role named AWSServiceRoleForLakeFormationDataAccess should be created + Type: String + pSRASecurityLakeMetaStoreManagerRoleName: + AllowedValues: ['AmazonSecurityLakeMetaStoreManagerV2', 'AmazonSecurityLakeMetaStoreManager'] + Default: AmazonSecurityLakeMetaStoreManagerV2 + Description: IAM role used by Security Lake to create data lake or query data from Security Lake + Type: String + pSourceVersion: + AllowedValues: ['2.0'] + ConstraintDescription: Must be a valid version number. Currently supported version is 2.0 + Description: Chose the version of data source from which you want to ingest log and event sources + Default: '2.0' + Type: String + pCloudTrailManagementEvents: + AllowedPattern: ^($|ALL|(\d{12})(,\s*\d{12})*)$ + ConstraintDescription: Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation + Description: Accounts to ingest CloudTrail - Management events from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: ALL + pCloudTrailLambdaDataEvents: + AllowedPattern: ^($|ALL|(\d{12})(,\s*\d{12})*)$ + ConstraintDescription: Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation + Description: Accounts to ingest CloudTrail - Lambda Data events from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: ALL + pCloudTrailS3DataEvents: + AllowedPattern: ^($|ALL|(\d{12})(,\s*\d{12})*)$ + ConstraintDescription: Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation + Description: Accounts to ingest CloudTrail - S3 Data events from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: '' + pCustomerControlTowerRegions: + AllowedPattern: ^([\w.-]{1,900})$|^(\/[\w.-]{1,900})*[\w.-]{1,900}$ + ConstraintDescription: Must be alphanumeric or special characters [., _, -]. In addition, the slash character ( / ) used to delineate hierarchies in parameter names. + Default: /sra/regions/customer-control-tower-regions + Description: SSM Parameter for Customer regions + Type: AWS::SSM::Parameter::Value> + pSecurityHubFindings: + AllowedPattern: ^($|ALL|(\d{12})(,\s*\d{12})*)$ + ConstraintDescription: Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation + Description: Accounts to ingest SecurityHub Findings from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: ALL + pVpcFlowLogs: + AllowedPattern: ^($|ALL|(\d{12})(,\s*\d{12})*)$ + ConstraintDescription: Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation + Description: Accounts to ingest VPC Flow Logs from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: ALL + pWafLogs: + AllowedPattern: ^($|ALL|(\d{12})(,\s*\d{12})*)$ + ConstraintDescription: Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation + Description: Accounts to ingest WAFv2 Logs from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: '' + pRoute53Logs: + AllowedPattern: ^($|ALL|(\d{12})(,\s*\d{12})*)$ + ConstraintDescription: Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation + Description: Accounts to ingest Amazon Route 53 resolver query logs from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: ALL + pEksAuditLogs: + AllowedPattern: ^($|ALL|(\d{12})(,\s*\d{12})*)$ + ConstraintDescription: Enter "ALL" or a comma-separated list of AWS account numbers without spaces, e.g., "123456789012,234567890123" to create log source. Leave empty to skip log source creation + Description: Accounts to ingest Amazon EKS Audit Logs from. Choose ALL to enable for all accounts in your AWS Organization. To choose the accounts enter a comma separated list of the AWS Account numbers. Leave empty to skip log source creation. + Type: CommaDelimitedList + Default: ALL + pLogArchiveAccountId: + AllowedPattern: ^([\w.-]{1,900})$|^(\/[\w.-]{1,900})*[\w.-]{1,900}$ + ConstraintDescription: Must be alphanumeric or special characters [., _, -]. In addition, the slash character ( / ) used to delineate hierarchies in parameter names. + Default: /sra/control-tower/log-archive-account-id + Description: SSM Parameter for AWS Account ID of the Log Archive account. + Type: AWS::SSM::Parameter::Value + pOrgConfigurationSources: + AllowedValues: ['', ROUTE53, VPC_FLOW, SH_FINDINGS, CLOUD_TRAIL_MGMT, LAMBDA_EXECUTION, S3_DATA, EKS_AUDIT, WAF] + Default: ROUTE53,VPC_FLOW,SH_FINDINGS,CLOUD_TRAIL_MGMT,LAMBDA_EXECUTION,EKS_AUDIT + Description: (Optional) Comma separated list of AWS log sources to enable for new member accounts in your organization (ROUTE53,VPC_FLOW,SH_FINDINGS,CLOUD_TRAIL_MGMT,LAMBDA_EXECUTION,S3_DATA,EKS_AUDIT,WAF). If 'Create Organization Configuration' parameter is set to 'true', then this parameter becomes required. + Type: CommaDelimitedList + pCreateOrganizationConfiguration: + AllowedValues: ['true', 'false'] + Default: 'true' + Description: Select whether to automatically enable Amazon Security Lake for new member accounts in your organization + Type: String + pSecurityLakeOrgKeyAlias: + AllowedPattern: '^[a-zA-Z0-9/_-]+$' + ConstraintDescription: + The alias must be string of 1-256 characters. It can contain only alphanumeric characters, forward slashes (/), underscores (_), and dashes (-). + Default: sra-security-lake-org-key + Description: Security Lake KMS Key Alias + Type: String + pAuditAccountId: + AllowedPattern: ^([\w.-]{1,900})$|^(\/[\w.-]{1,900})*[\w.-]{1,900}$ + ConstraintDescription: Must be alphanumeric or special characters [., _, -]. In addition, the slash character ( / ) used to delineate hierarchies in parameter names. + Default: /sra/control-tower/audit-account-id + Description: SSM Parameter for AWS Account ID of the Control Tower account to delegate administration. + Type: AWS::SSM::Parameter::Value + pComplianceFrequency: + ConstraintDescription: Compliance Frequency must be a number between 1 and 30, inclusive. + Default: 7 + Description: Frequency (in days between 1 and 30, default is 7) to check organizational compliance by invoking the Lambda Function. + MinValue: 1 + MaxValue: 30 + Type: Number + pControlTowerLifeCycleRuleName: + AllowedPattern: ^[\w.-]{1,64}$ + ConstraintDescription: Max 64 alphanumeric and underscore characters. Also special characters supported [., -] + Default: sra-security-lake-org-trigger + Description: The name of the AWS Control Tower Life Cycle Rule. + Type: String + pControlTowerRegionsOnly: + AllowedValues: ['true', 'false'] + Default: 'true' + Description: Only enable in the customer governed regions specified in Control Tower or Common Prerequisites solution + Type: String + pCreateLambdaLogGroup: + AllowedValues: ['true', 'false'] + Default: 'false' + Description: Indicates whether a CloudWatch Log Group should be explicitly created for the Lambda function, to allow for setting a Log Retention and/or KMS Key for encryption. + Type: String + pEnabledRegions: + AllowedPattern: ^$|^([a-z0-9-]{1,64})$|^(([a-z0-9-]{1,64},)*[a-z0-9-]{1,64})$ + ConstraintDescription: Only lowercase letters, numbers, and hyphens ('-') allowed. (e.g. us-east-1) Additional AWS regions can be provided, separated by commas. (e.g. us-east-1,ap-southeast-2) + Default: '' + Description: (Optional) Enabled regions (AWS regions, separated by commas). Leave blank to enable all supported regions (recommended). + Type: String + pLambdaLogGroupKmsKey: + AllowedPattern: ^$|^arn:(aws[a-zA-Z-]*){1}:kms:[a-z0-9-]+:\d{12}:key\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$ + ConstraintDescription: 'Key ARN example: arn:aws:kms:::key/1234abcd-12ab-34cd-56ef-1234567890ab' + Default: '' + Description: (Optional) KMS Key ARN to use for encrypting the Lambda logs data. If empty, encryption is enabled with CloudWatch Logs managing the server-side encryption keys. + Type: String + pLambdaLogGroupRetention: + AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653] + Default: 14 + Description: Specifies the number of days you want to retain log events + Type: String + pLambdaLogLevel: + AllowedValues: [INFO, ERROR, DEBUG] + Default: INFO + Description: Lambda Function Logging Level + Type: String + pSRAAlarmEmail: + AllowedPattern: ^$|^([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)$ + ConstraintDescription: Must be a valid email address. + Default: '' + Description: (Optional) Email address for receiving SRA alarms + Type: String + pSRASolutionName: + AllowedValues: [sra-security-lake-org] + Default: sra-security-lake-org + Description: The SRA solution name. The default value is the folder name of the solution + Type: String + pSRAStagingS3BucketName: + AllowedPattern: ^([\w.-]{1,900})$|^(\/[\w.-]{1,900})*[\w.-]{1,900}$ + ConstraintDescription: Must be alphanumeric or special characters [., _, -]. In addition, the slash character ( / ) used to delineate hierarchies in parameter names. + Default: /sra/staging-s3-bucket-name + Description: SSM Parameter for SRA Staging S3 bucket name for the artifacts relevant to solution. (e.g., lambda zips, CloudFormation templates) S3 bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-). + Type: AWS::SSM::Parameter::Value + pSRASolutionVersion: + AllowedValues: [v1.0] + Default: v1.0 + Description: The SRA solution version. Used to trigger updates on the nested StackSets. + Type: String + pRegisterAuditAccountDataSubscriber: + AllowedValues: ['true', 'false'] + Default: 'false' + Description: Identifies whether to registerAudit (Security Tooling) account as a Subscriber with Data Access + Type: String + pAuditAccountDataSubscriberPrefix: + AllowedValues: [sra-audit-account-data-subscriber] + Default: sra-audit-account-data-subscriber + Description: The name of the Audit (Security Tooling) account data access subscriber + Type: String + pAuditAccountDataSubscriberExternalId: + AllowedPattern: ^(?:[a-zA-Z0-9]{0,64})?$ + ConstraintDescription: All characters allowed except '&<>\%|' + Default: '' + Description: (Optional) External ID for Security Lake Audit (Security Tooling) data access subscriber. If 'Register Audit (Security Tooling) account as a Subscriber with Data Access' parameter is set to 'true', then this parameter becomes required. + Type: String + pAuditAccountQuerySubscriberPrefix: + AllowedValues: [sra-audit-account-query-subscriber] + Default: sra-audit-account-query-subscriber + Description: The name of the Audit (Security Tooling) account query access subscriber + Type: String + pAuditAccountQuerySubscriberExternalId: + AllowedPattern: ^(?:[a-zA-Z0-9]{0,64})?$ + ConstraintDescription: All characters allowed except '&<>\%|' + Default: '' + Description: (Optional) External ID for Security Lake Audit (Security Tooling) query access subscriber. If 'Register Audit (Security Tooling) account as a Subscriber with Query Access' parameter is set to 'true', then this parameter becomes required. + Type: String + pRegisterAuditAccountQuerySubscriber: + AllowedValues: ['true', 'false'] + Default: 'false' + Description: Identifies whether to register Audit (Security Tooling) account as a Subscriber with Query Access + Type: String + pStackSetAdminRole: + AllowedValues: [sra-stackset] + Default: sra-stackset + Description: The administration role name that is used in the stackset. + Type: String + pStackExecutionRole: + AllowedValues: [sra-execution] + Default: sra-execution + Description: The execution role name that is used in the stack. + Type: String + pOrganizationId: + AllowedPattern: ^([\w.-]{1,900})$|^(\/[\w.-]{1,900})*[\w.-]{1,900}$ + ConstraintDescription: Must be alphanumeric or special characters [., _, -]. In addition, the slash character ( / ) used to delineate hierarchies in parameter names. + Default: /sra/control-tower/organization-id + Description: SSM Parameter for AWS Organizations ID + Type: AWS::SSM::Parameter::Value + pSecurityLakeWarning: + AllowedValues: ['Accept', 'Reject'] + Default: Reject + Description: (Disclaimer) Resources created using this CloudFormation template may incur costs. The pricing for the individual AWS services and resources used in this template can be found on the respective service pricing pages. Please refer to https://aws.amazon.com/pricing/ + Type: String + pDisableSecurityLake: + AllowedValues: ['true', 'false'] + Default: 'false' + Description: Update to 'true' to disable Security Lake log sources and organization configuration before deleting the stack. + Type: String + pSecurityLakeConfigurationRoleName: + AllowedPattern: '^[\w+=,.@-]{1,64}$' + ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [+, =, ., @, -] + Default: sra-security-lake-org-configuration + Description: Security Lake Configuration IAM Role Name + Type: String + +Conditions: + cRegisterAuditAccountQuerySubscriber: !Equals + - !Ref pRegisterAuditAccountQuerySubscriber + - 'true' + cControlTowerRegions: !Equals + - !Ref pControlTowerRegionsOnly + - 'true' + cCreateLakeFormationSlr: !Equals + - !Ref pCreateLakeFormationSlr + - 'true' + +Rules: + VerifySecurityLakeDisclaimer: + RuleCondition: !Equals + - !Ref pSecurityLakeWarning + - Reject + Assertions: + - Assert: !Not + - !Equals + - !Ref pSecurityLakeWarning + - Reject + AssertDescription: Please Acknowledge Security Lake pricing disclaimer + ProvideDataAccessExternalId: + RuleCondition: !Equals + - !Ref pRegisterAuditAccountDataSubscriber + - 'true' + Assertions: + - Assert: !Not [!Equals [!Ref pAuditAccountDataSubscriberExternalId, '']] + AssertDescription: Please provide External ID for Security Lake Audit (Security Tooling) data access subscriber + ProvideQueryAccessExternalId: + RuleCondition: !Equals + - !Ref pRegisterAuditAccountQuerySubscriber + - 'true' + Assertions: + - Assert: !Not [!Equals [!Ref pAuditAccountQuerySubscriberExternalId, '']] + AssertDescription: Please provide External ID for Security Lake Audit (Security Tooling) query access subscriber + VerifyEnabledRegions: + RuleCondition: !Equals + - !Ref pControlTowerRegionsOnly + - 'false' + Assertions: + - Assert: !Not [!Equals [!Ref pEnabledRegions, '']] + AssertDescription: Please provide Enabled Regions + ProvideUniqueExternalIds: + RuleCondition: !And + - !Not [!Equals [!Ref pAuditAccountDataSubscriberExternalId, '']] + - !Not [!Equals [!Ref pAuditAccountQuerySubscriberExternalId, '']] + - !Equals [!Ref pAuditAccountDataSubscriberExternalId, !Ref pAuditAccountQuerySubscriberExternalId] + Assertions: + - Assert: !Not [!Equals [!Ref pAuditAccountDataSubscriberExternalId, !Ref pAuditAccountQuerySubscriberExternalId]] + AssertDescription: The external ID for Security Lake Audit (Security Tooling) data access and query access subscribers must be different from one another. + +Resources: + rSecurityLakeQuerySubscriberIAMRoleStackSet: + Type: AWS::CloudFormation::StackSet + Condition: cRegisterAuditAccountQuerySubscriber + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + StackSetName: sra-security-lake-query-subscriber-role + AdministrationRoleARN: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${pStackSetAdminRole} + CallAs: SELF + Capabilities: + - CAPABILITY_NAMED_IAM + Description: !Sub ${pSRASolutionVersion} - Deploys an IAM role via ${pSRASolutionName} for configuring Security Lake Subscriber account + ExecutionRoleName: !Ref pStackExecutionRole + ManagedExecution: + Active: true + OperationPreferences: + FailureTolerancePercentage: 0 + MaxConcurrentPercentage: 100 + RegionConcurrencyType: PARALLEL + PermissionModel: SELF_MANAGED + StackInstancesGroup: + - DeploymentTargets: + Accounts: + - !Ref pAuditAccountId + Regions: + - !Ref AWS::Region + TemplateURL: !Sub https://${pSRAStagingS3BucketName}.s3.${AWS::Region}.${AWS::URLSuffix}/${pSRASolutionName}/templates/sra-security-lake-query-subscriber-role.yaml + Parameters: + - ParameterKey: pManagementAccountId + ParameterValue: !Ref AWS::AccountId + - ParameterKey: pLogArchiveAccountId + ParameterValue: !Ref pLogArchiveAccountId + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName + + rSecurityLakeConfigurationIAMRoleStackSet: + Type: AWS::CloudFormation::StackSet + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + StackSetName: sra-security-lake-org-configuration-role + AdministrationRoleARN: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${pStackSetAdminRole} + CallAs: SELF + Capabilities: + - CAPABILITY_NAMED_IAM + Description: !Sub ${pSRASolutionVersion} - Deploys an IAM role via ${pSRASolutionName} for configuring SecurityLake + ExecutionRoleName: !Ref pStackExecutionRole + ManagedExecution: + Active: true + OperationPreferences: + FailureTolerancePercentage: 0 + MaxConcurrentPercentage: 100 + RegionConcurrencyType: PARALLEL + PermissionModel: SELF_MANAGED + StackInstancesGroup: + - DeploymentTargets: + Accounts: + - !Ref pLogArchiveAccountId + Regions: + - !Ref AWS::Region + TemplateURL: !Sub https://${pSRAStagingS3BucketName}.s3.${AWS::Region}.${AWS::URLSuffix}/${pSRASolutionName}/templates/sra-security-lake-org-configuration-role.yaml + Parameters: + - ParameterKey: pManagementAccountId + ParameterValue: !Ref AWS::AccountId + - ParameterKey: pAuditAccountQuerySubscriberExternalId + ParameterValue: !Ref pAuditAccountQuerySubscriberExternalId + - ParameterKey: pSecurityLakeOrgLambdaRoleName + ParameterValue: !Ref pSecurityLakeOrgLambdaRoleName + - ParameterKey: pSecurityLakeConfigurationRoleName + ParameterValue: !Ref pSecurityLakeConfigurationRoleName + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName + + rSecurityLakeConfigurationStack: + Type: AWS::CloudFormation::Stack + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + TemplateURL: !Sub https://${pSRAStagingS3BucketName}.s3.${AWS::Region}.${AWS::URLSuffix}/${pSRASolutionName}/templates/sra-security-lake-org-configuration.yaml + Parameters: + pComplianceFrequency: !Ref pComplianceFrequency + pControlTowerLifeCycleRuleName: !Ref pControlTowerLifeCycleRuleName + pControlTowerRegionsOnly: !Ref pControlTowerRegionsOnly + pCreateLambdaLogGroup: !Ref pCreateLambdaLogGroup + pDelegatedAdminAccountId: !Ref pLogArchiveAccountId + pEnabledRegions: !Ref pEnabledRegions + pLambdaLogGroupKmsKey: !Ref pLambdaLogGroupKmsKey + pLambdaLogGroupRetention: !Ref pLambdaLogGroupRetention + pLambdaLogLevel: !Ref pLambdaLogLevel + pSRAAlarmEmail: !Ref pSRAAlarmEmail + pSRAStagingS3BucketName: !Ref pSRAStagingS3BucketName + pCreateOrganizationConfiguration: !Ref pCreateOrganizationConfiguration + pOrgConfigurationSources: !Join + - ',' + - !Ref pOrgConfigurationSources + pCloudTrailManagementEvents: !Join + - ',' + - !Ref pCloudTrailManagementEvents + pCloudTrailLambdaDataEvents: !Join + - ',' + - !Ref pCloudTrailLambdaDataEvents + pCloudTrailS3DataEvents: !Join + - ',' + - !Ref pCloudTrailS3DataEvents + pSecurityHubFindings: !Join + - ',' + - !Ref pSecurityHubFindings + pVpcFlowLogs: !Join + - ',' + - !Ref pVpcFlowLogs + pWafLogs: !Join + - ',' + - !Ref pWafLogs + pRoute53Logs: !Join + - ',' + - !Ref pRoute53Logs + pEksAuditLogs: !Join + - ',' + - !Ref pEksAuditLogs + pSourceVersion: !Ref pSourceVersion + pRegisterAuditAccountDataSubscriber: !Ref pRegisterAuditAccountDataSubscriber + pAuditAccountDataSubscriberPrefix: !Ref pAuditAccountDataSubscriberPrefix + pAuditAccountDataSubscriberExternalId: !Ref pAuditAccountDataSubscriberExternalId + pRegisterAuditAccountQuerySubscriber: !Ref pRegisterAuditAccountQuerySubscriber + pAuditAccountQuerySubscriberPrefix: !Ref pAuditAccountQuerySubscriberPrefix + pAuditAccountQuerySubscriberExternalId: !Ref pAuditAccountQuerySubscriberExternalId + pDisableSecurityLake: !Ref pDisableSecurityLake + pOrganizationId: !Ref pOrganizationId + pCreateResourceLink: !Ref pCreateResourceLink + pSecurityLakeOrgKeyAlias: !Ref pSecurityLakeOrgKeyAlias + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName + + rSecurityLakeKMSKeyStackSet: + Type: AWS::CloudFormation::StackSet + DependsOn: rSecurityLakeConfigurationIAMRoleStackSet + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + StackSetName: sra-security-lake-org-kms-key + AdministrationRoleARN: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${pStackSetAdminRole} + CallAs: SELF + Description: !Sub ${pSRASolutionVersion} - Deploys a KMS Key via ${pSRASolutionName} for encrypting Security Lake + ExecutionRoleName: !Ref pStackExecutionRole + ManagedExecution: + Active: true + OperationPreferences: + FailureTolerancePercentage: 0 + MaxConcurrentPercentage: 100 + RegionConcurrencyType: PARALLEL + PermissionModel: SELF_MANAGED + StackInstancesGroup: + - DeploymentTargets: + Accounts: + - !Ref pLogArchiveAccountId + Regions: !If + - cControlTowerRegions + - !Ref pCustomerControlTowerRegions + - !Split + - ',' + - !Ref pEnabledRegions + TemplateURL: !Sub https://${pSRAStagingS3BucketName}.s3.${AWS::Region}.${AWS::URLSuffix}/${pSRASolutionName}/templates/sra-security-lake-org-kms-key.yaml + Parameters: + - ParameterKey: pSecurityLakeOrgKeyAlias + ParameterValue: !Ref pSecurityLakeOrgKeyAlias + - ParameterKey: pManagementAccountId + ParameterValue: !Ref AWS::AccountId + - ParameterKey: pSRASolutionName + ParameterValue: !Ref pSRASolutionName + - ParameterKey: pRegisterAuditAccountQuerySubscriber + ParameterValue: !Ref pRegisterAuditAccountQuerySubscriber + - ParameterKey: pAuditAccountId + ParameterValue: !Ref pAuditAccountId + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName + + rSecurityLakeMetaStoreManagerIAMRoleStackSet: + Type: AWS::CloudFormation::StackSet + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + StackSetName: sra-security-lake-meta-store-manager-role + AdministrationRoleARN: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${pStackSetAdminRole} + CallAs: SELF + Capabilities: + - CAPABILITY_NAMED_IAM + Description: !Sub ${pSRASolutionVersion} - Deploys an IAM role via ${pSRASolutionName} for configuring Security Lake + ExecutionRoleName: !Ref pStackExecutionRole + ManagedExecution: + Active: true + OperationPreferences: + FailureTolerancePercentage: 0 + MaxConcurrentPercentage: 100 + RegionConcurrencyType: PARALLEL + PermissionModel: SELF_MANAGED + StackInstancesGroup: + - DeploymentTargets: + Accounts: + - !Ref pLogArchiveAccountId + Regions: + - !Ref AWS::Region + TemplateURL: !Sub https://${pSRAStagingS3BucketName}.s3.${AWS::Region}.${AWS::URLSuffix}/${pSRASolutionName}/templates/sra-security-lake-meta-store-manager-role.yaml + Parameters: + - ParameterKey: pSRASolutionName + ParameterValue: !Ref pSRASolutionName + - ParameterKey: pSRASecurityLakeMetaStoreManagerRoleName + ParameterValue: !Ref pSRASecurityLakeMetaStoreManagerRoleName + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName + + rSecurityLakeLakeFormationSlrStackSet: + Type: AWS::CloudFormation::StackSet + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Condition: cCreateLakeFormationSlr + Properties: + StackSetName: sra-security-lake-lakeformation-slr + AdministrationRoleARN: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${pStackSetAdminRole} + CallAs: SELF + Capabilities: + - CAPABILITY_NAMED_IAM + Description: !Sub ${pSRASolutionVersion} - Deploys AWS Lake Formation service-linked role via ${pSRASolutionName} + ExecutionRoleName: !Ref pStackExecutionRole + ManagedExecution: + Active: true + OperationPreferences: + FailureTolerancePercentage: 0 + MaxConcurrentPercentage: 100 + RegionConcurrencyType: PARALLEL + PermissionModel: SELF_MANAGED + StackInstancesGroup: + - DeploymentTargets: + Accounts: + - !Ref pLogArchiveAccountId + Regions: + - !Ref AWS::Region + TemplateURL: !Sub https://${pSRAStagingS3BucketName}.s3.${AWS::Region}.${AWS::URLSuffix}/${pSRASolutionName}/templates/sra-security-lake-lakeformation-slr.yaml + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName \ No newline at end of file diff --git a/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-query-subscriber-role.yaml b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-query-subscriber-role.yaml new file mode 100644 index 00000000..e6ee17f0 --- /dev/null +++ b/aws_sra_examples/solutions/security_lake/security_lake_org/templates/sra-security-lake-query-subscriber-role.yaml @@ -0,0 +1,168 @@ +######################################################################## +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +######################################################################## +AWSTemplateFormatVersion: 2010-09-09 +Description: + This template creates an IAM role to configure the delegated administrator account - - 'security_lake_org' solution in the repo, + https://github.com/aws-samples/aws-security-reference-architecture-examples (sra-1u3sd7f8p) + +Metadata: + SRA: + Version: 1.0 + Order: 2 + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: General Properties + Parameters: + - pSRASolutionName + + - Label: + default: Role Properties + Parameters: + - pSecurityLakeSubscriberRoleName + - pSecurityLakeOrgLambdaRoleName + - pManagementAccountId + - pLogArchiveAccountId + + ParameterLabels: + pManagementAccountId: + default: Organization Management Account ID + pSecurityLakeOrgLambdaRoleName: + default: Lambda Role Name + pSecurityLakeSubscriberRoleName: + default: Security Lake Query Subscriber Role Name + pSRASolutionName: + default: SRA Solution Name + pLogArchiveAccountId: + default: Log Archive Account ID + +Parameters: + pManagementAccountId: + AllowedPattern: '^\d{12}$' + ConstraintDescription: Must be 12 digits + Description: Organization Management Account ID + Type: String + pSecurityLakeOrgLambdaRoleName: + AllowedPattern: '^[\w+=,.@-]{1,64}$' + ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [+, =, ., @, -] + Default: sra-security-lake-org-lambda + Description: Lambda Role Name + Type: String + pSecurityLakeSubscriberRoleName: + AllowedPattern: '^[\w+=,.@-]{1,64}$' + ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [+, =, ., @, -] + Default: sra-security-lake-query-subscriber + Description: Security Lake Configuration IAM Role Name + Type: String + pSRASolutionName: + AllowedValues: [sra-security-lake-org] + Default: sra-security-lake-org + Description: The SRA solution name. The default value is the folder name of the solution + Type: String + pLogArchiveAccountId: + AllowedPattern: '^\d{12}$' + ConstraintDescription: Must be 12 digits + Description: Log Archive Account ID + Type: String + +Resources: + rQuerySubscriberRole: + Type: AWS::IAM::Role + Metadata: + cfn_nag: + rules_to_suppress: + - id: W11 + reason: Actions require * in resource + - id: W28 + reason: Explicit role name provided + Properties: + RoleName: !Ref pSecurityLakeSubscriberRoleName + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: sts:AssumeRole + Condition: + StringEquals: + aws:PrincipalArn: + - !Sub arn:${AWS::Partition}:iam::${pManagementAccountId}:role/${pSecurityLakeOrgLambdaRoleName} + Principal: + AWS: + - !Sub arn:${AWS::Partition}:iam::${pManagementAccountId}:root + Path: '/' + Policies: + + - PolicyName: sra-security-lake-org-subscriber-policy-ram + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AllowRamReadActions + Effect: Allow + Action: + - ram:ListResources + - ram:GetResourceShareInvitations + Resource: '*' + + - Sid: AllowAcceptResourceShareInvitation + Effect: Allow + Action: + - ram:AcceptResourceShareInvitation + Resource: !Sub arn:${AWS::Partition}:ram:*:${pLogArchiveAccountId}:resource-share-invitation/* + Condition: + StringEquals: + ram:ShareOwnerAccountId: !Sub ${pLogArchiveAccountId} + + - PolicyName: sra-security-lake-org-subscriber-policy-glue + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AllowGlueDatabaseActions + Effect: Allow + Action: + - glue:CreateDatabase + - glue:GetDatabase + - glue:GetDatabases + Resource: + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:catalog + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:database/amazon_security_lake_glue_db_*_subscriber + - Sid: AllowGlueTableActions + Effect: Allow + Action: + - glue:CreateTable + - glue:GetPartitions + - glue:GetTable + Resource: + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:catalog + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:table/amazon_security_lake_glue_db_*_subscriber/rl_amazon_security_lake_table_*_cloud_trail_mgmt_2_0 + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:database/amazon_security_lake_glue_db_*_subscriber + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:table/amazon_security_lake_glue_db_*_subscriber/rl_amazon_security_lake_table_*_sh_findings_2_0 + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:table/amazon_security_lake_glue_db_*_subscriber/rl_amazon_security_lake_table_*_lambda_execution_2_0 + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:table/amazon_security_lake_glue_db_*_subscriber/rl_amazon_security_lake_table_*_s3_data_2_0 + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:table/amazon_security_lake_glue_db_*_subscriber/rl_amazon_security_lake_table_*_route53_2_0 + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:table/amazon_security_lake_glue_db_*_subscriber/rl_amazon_security_lake_table_*_vpc_flow_2_0 + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:table/amazon_security_lake_glue_db_*_subscriber/rl_amazon_security_lake_table_*_eks_audit_2_0 + - !Sub arn:${AWS::Partition}:glue:*:${AWS::AccountId}:table/amazon_security_lake_glue_db_*_subscriber/rl_amazon_security_lake_table_*_waf_2_0 + + - PolicyName: sra-security-lake-org-policy-iam + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AllowReadIamActions + Effect: Allow + Action: iam:GetRole + Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/* + + - PolicyName: sra-security-lake-org-policy-lakeformation + PolicyDocument: + Version: 2012-10-17 + Statement: + - Sid: AllowGrantPermissions + Effect: Allow + Action: lakeformation:GrantPermissions + Resource: "*" + + Tags: + - Key: sra-solution + Value: !Ref pSRASolutionName