From b85cce82070263aa7b4fc277897db5f376559c3a Mon Sep 17 00:00:00 2001 From: Gabriel Costa Date: Wed, 28 Feb 2024 19:54:58 -0800 Subject: [PATCH 1/7] Fixes to allow Non Control Tower environment deployment --- .../functional_tests/entrypoint.sh | 21 ++- .../functional_tests/scoutsuite/scoutsuite.sh | 2 +- .taskcat.yml | 30 ++++- scripts/cleanup_config.json | 24 ++-- scripts/cleanup_config.py | 123 +++++++++++------- .../cfn-abi-lacework-polygraph.template.yaml | 98 +++++++++++++- 6 files changed, 223 insertions(+), 75 deletions(-) diff --git a/.project_automation/functional_tests/entrypoint.sh b/.project_automation/functional_tests/entrypoint.sh index ab372eb..91ca67b 100755 --- a/.project_automation/functional_tests/entrypoint.sh +++ b/.project_automation/functional_tests/entrypoint.sh @@ -5,11 +5,12 @@ # managed and local tasks always use these variables for the project and project type path PROJECT_PATH=${BASE_PATH}/project PROJECT_TYPE_PATH=${BASE_PATH}/projecttype - +export REGION=$(grep -A1 regions: .taskcat.yml | awk '/ - / {print $NF}' |sort | uniq -c |sort -k1| head -1 |awk '{print $NF}') cd ${PROJECT_PATH} - +NON_CT_ENV="211125739641" # Retrieve the AWS account ID and store it in a variable AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text) +echo "Account ID: $AWS_ACCOUNT_ID" cleanup_region() { echo "Cleanup running in region: $1" @@ -18,7 +19,7 @@ cleanup_region() { } cleanup_all_regions() { - export AWS_DEFAULT_REGION=us-east-1 + export AWS_DEFAULT_REGION=$REGION regions=($(aws ec2 describe-regions --query "Regions[*].RegionName" --output text)) for region in ${regions[@]} do @@ -33,10 +34,18 @@ run_test() { unset AWS_DEFAULT_REGION echo $AWS_DEFAULT_REGION taskcat test run -n -t $1 - .project_automation/functional_tests/scoutsuite/scoutsuite.sh + #.project_automation/functional_tests/scoutsuite/scoutsuite.sh } -# Run taskcat e2e test -run_test "cfn-abi-lacework-polygraph-multi-org-multi-sub-mapping" + +# if account id is xxxx do this +if [ "$AWS_ACCOUNT_ID" == ${NON_CT_ENV} ]; then + # Run taskcat e2e test for Non-Control Tower environment + run_test "cfn-abi-lacework-polygraph-non-controltower" +else + # Run taskcat e2e test for Control Tower environment + echo "Account ID: $AWS_ACCOUNT_ID" + run_test "cfn-abi-lacework-polygraph-multi-org-multi-sub-mapping" +fi ## Executing ash tool diff --git a/.project_automation/functional_tests/scoutsuite/scoutsuite.sh b/.project_automation/functional_tests/scoutsuite/scoutsuite.sh index 1cad0e2..9a1b0eb 100755 --- a/.project_automation/functional_tests/scoutsuite/scoutsuite.sh +++ b/.project_automation/functional_tests/scoutsuite/scoutsuite.sh @@ -19,7 +19,7 @@ run_scoutsuite() { # Upload Scoutsuite security scan results to S3 bucket named scoutsuite-results-aws-AWS-ACCOUNT-ID python3 .project_automation/functional_tests/scoutsuite/process-scoutsuite-report.py # Delete taskcat e2e test resources - taskcat test clean ALL + taskcat test clean ALL -w -r $REGION process_scoutsuite_report } diff --git a/.taskcat.yml b/.taskcat.yml index 70db568..0288fb5 100644 --- a/.taskcat.yml +++ b/.taskcat.yml @@ -8,7 +8,7 @@ project: tests: cfn-abi-lacework-polygraph-multi-org-multi-sub-mapping: regions: - - us-east-1 # Control Tower Home region for Pilot + - us-east-1 template: templates/cfn-abi-lacework-polygraph.template.yaml parameters: pSRAStagingS3KeyPrefix: $[taskcat_project_name] @@ -17,4 +17,30 @@ tests: LaceworkURL: laceworkalliances.lacework.net LaceworkAccessKeyID: $[taskcat_ssm_/lacework/LaceworkAccessKeyID] LaceworkSecretKey: $[taskcat_ssm_/lacework/LaceworkSecretKey] - pDisableGuardDuty: 'Yes' \ No newline at end of file + pDisableGuardDuty: 'Yes' + KMSKeyIdentifierARN: $[taskcat_ssm_/lacework/CloudTrailKMSARN] + cfn-abi-lacework-polygraph-non-controltower: + regions: + - us-east-1 + template: templates/cfn-abi-lacework-polygraph.template.yaml + parameters: + pSRAStagingS3KeyPrefix: $[taskcat_project_name] + pSRASourceS3BucketName: $[taskcat_autobucket] + pSRAS3BucketRegion: $[taskcat_current_region] + LaceworkURL: laceworkalliances.lacework.net + LaceworkAccessKeyID: $[taskcat_ssm_/lacework/LaceworkAccessKeyID] + LaceworkSecretKey: $[taskcat_ssm_/lacework/LaceworkSecretKey] + pDisableGuardDuty: 'Yes' + pControlTower: 'false' + pLogArchiveAccountId: $[taskcat_ssm_/nonct/log-archive-account-id] + pSecurityAccountId: $[taskcat_ssm_/nonct/audit-account-id] + pGovernedRegions: 'us-east-1' + pAdminRoleName: 'AWSCloudFormationStackSetAdministrationRole' + pExecRoleName: 'AWSCloudFormationStackSetExecutionRole' + LaceworkAccount: 'laceworkalliances' + OrganizationID: 'o-5py8cuszlp' + OrganizationalUnit: 'ou-aty9-5t69fbhr' + ResourceNamePrefix: 'lw-stackset' + ExistingCloudTrail: 'organization-trail' + KMSKeyIdentifierARN: $[taskcat_ssm_/lacework/CloudTrailKMSARN] + LogAccountName: 'Log Archive' \ No newline at end of file diff --git a/scripts/cleanup_config.json b/scripts/cleanup_config.json index fda7798..bf869b4 100644 --- a/scripts/cleanup_config.json +++ b/scripts/cleanup_config.json @@ -65,12 +65,7 @@ }, { "Type" : "SSM_PARAMETER", - "Filter" : "/sra/" - }, - { - "Type" : "S3_BUCKET", - "Filter" : "sra-guardduty-org-delivery-", - "Account" : "log_account" + "Filter" : "/sra/staging-s3-bucket-name" }, { "Type" : "S3_BUCKET", @@ -89,11 +84,6 @@ "Filter": "sra-org-trail-logs-", "Account": "log_account" }, - { - "Type" : "S3_BUCKET", - "Filter" : "sra-guardduty-org-delivery-", - "Account" : "log_account" - }, { "Type" : "S3_BUCKET", "Filter" : "cfn-abi-amazon-guardduty-" @@ -174,6 +164,18 @@ "Type" : "STACK_SET", "Filter" : "sra-stackset-execution-role" }, + { + "Type" : "STACK_SET", + "Filter" : "sra-staging-s3-bucket-management-account-regions" + }, + { + "Type" : "STACK_SET", + "Filter" : "sra-guardduty-org-delivery-s3-bucket" + }, + { + "Type" : "STACK_SET", + "Filter" : "sra-guardduty-org-delivery-kms-key" + }, { "Type" : "STACK", "Filter" : "sra-common-prerequisites-staging-s3-bucket" diff --git a/scripts/cleanup_config.py b/scripts/cleanup_config.py index 29becdb..990ffc3 100644 --- a/scripts/cleanup_config.py +++ b/scripts/cleanup_config.py @@ -24,26 +24,26 @@ STACKSTATUS = [ 'ROLLBACK_FAILED', 'ROLLBACK_COMPLETE', 'DELETE_FAILED', 'DELETE_COMPLETE'] -def list_stacksets(): +def list_stacksets(context=CF): '''List all stacksets in the account''' - response = CF.list_stack_sets() + response = context.list_stack_sets() stacksets = response['Summaries'] while response.get('NextToken'): - response = CF.list_stack_sets(NextToken=response['NextToken']) + response = context.list_stack_sets(NextToken=response['NextToken']) stacksets.extend(response['Summaries']) return stacksets -def list_active_stackset_names(): +def list_active_stackset_names(context=CF): '''List all stackset names in the account''' cf_names = [] - for cfn in list_stacksets(): + for cfn in list_stacksets(context): if cfn['Status'] != 'DELETED': cf_names += [cfn['StackSetName']] return cf_names -def list_stackset_names(filters=None): +def list_stackset_names(context=CF, filters=None): '''List all stackset names in the account''' - cf_info = list_stacksets() + cf_info = list_stacksets(context) cf_names = [] for cfn in cf_info: if cfn['Status'] != 'DELETED': @@ -56,19 +56,19 @@ def list_stackset_names(filters=None): return cf_names -def list_stackset_instances(stackset_name): +def list_stackset_instances(context=CF, ss_name=None): '''List all stackset instances in the account''' - response = CF.list_stack_instances(StackSetName=stackset_name) + response = context.list_stack_instances(StackSetName=ss_name) stackinstances = response['Summaries'] while response.get('NextToken'): - response = CF.list_stack_instances(StackSetName=stackset_name, + response = context.list_stack_instances(StackSetName=ss_name, NextToken=response['NextToken']) stackinstances.extend(response['Summaries']) return stackinstances def delete_stack_instances(stackset_name, retain_stacks=False): '''Delete all stackset instances in the account''' - stackinstances = list_stackset_instances(stackset_name) + stackinstances = list_stackset_instances(ss_name=stackset_name) for stackinstance in stackinstances: CF.delete_stack_instances(StackSetName=stackset_name, Regions=[stackinstance['Region']], @@ -76,7 +76,7 @@ def delete_stack_instances(stackset_name, retain_stacks=False): def si_account_list(stackset_name): '''List all stackset instance accounts''' - stackinstances = list_stackset_instances(stackset_name) + stackinstances = list_stackset_instances(ss_name=stackset_name) stackinstance_names = [] for stackinstance in stackinstances: stackinstance_names += [stackinstance['Account']] @@ -84,7 +84,7 @@ def si_account_list(stackset_name): def si_region_list(stackset_name): '''List all stackset instance regions''' - stackinstances = list_stackset_instances(stackset_name) + stackinstances = list_stackset_instances(ss_name=stackset_name) stackinstance_regions = [] for stackinstance in stackinstances: stackinstance_regions += [stackinstance['Region']] @@ -121,7 +121,7 @@ def delete_all_stackinstances(stackset_name): RetainStacks=False) loop = 1 - while len(list_stackset_instances(stackset_name)) > 0 and loop < 30: + while len(list_stackset_instances(ss_name=stackset_name)) > 0 and loop < 30: sleep(10) loop += 1 @@ -129,7 +129,7 @@ def delete_all_stackinstances(stackset_name): def delete_stacksets(filters): '''Delete all stacksets created by CfCT solution in the account''' - cf_names = list_stackset_names(filters) + cf_names = list_stackset_names(filters=filters) for cf_name in cf_names: op_info = delete_all_stackinstances(cf_name) op_id = op_info['OperationId'] @@ -177,11 +177,11 @@ def delete_stack(filters='tCaT-'): stack_name = stack['StackName'] stack_status = stack['StackStatus'] if stack_name.startswith(filters) and stack_status != 'DELETE_COMPLETE': - print('Deleting stack: %s', stack_name) + print(f"Deleting stack: {stack_name}") CF.delete_stack(StackName=stack_name) wait = 1 while list_stack_status_by_name(stack_name) not in STACKSTATUS and wait < 60: - print('Wait: %s, Stack: %s', stack_name, wait) + print(f"Wait: {wait}, Stack: {stack_name}") sleep(10) wait += 1 @@ -228,28 +228,33 @@ def delete_s3_buckets(item): else: raise exe -def list_all_parameters(ssm_session=SSM): +def list_all_parameters(context): ''''List all parameters in the account''' - response = ssm_session.describe_parameters() + response = context.describe_parameters() parameters = response['Parameters'] while response.get('NextToken'): - response = ssm_session.describe_parameters(NextToken=response['NextToken']) + response = context.describe_parameters(NextToken=response['NextToken']) parameters.extend(response['Parameters']) return parameters def delete_parameters(item): '''Delete all parameters created in the account''' + print(f"Recieved item: {item}") filters = item['Filter'] (ssm_session, account, target) = get_client_session(item, 'ssm') - print(f"SSM action on {target} with filters: {filters}") + print(f"SSM action on {account}/{target} with filters: {filters}") - parameters = list_all_parameters(ssm_session) + parameters = list_all_parameters(context=ssm_session) for parameter in parameters: param_name = parameter['Name'] + print(f"param_name: {param_name}") if param_name.startswith(filters): print(f"..Deleting parameter {param_name}.") + res = ssm_session.get_parameter(Name=param_name)['Parameter']['ARN'] + print(f"res: {res}") ssm_session.delete_parameter(Name=param_name) + print(f"..Deleted parameter {param_name}.") def get_temp_credentials(aws_account, role_name='AWSControlTowerExecution'): ''' @@ -288,6 +293,7 @@ def establish_remote_session(account): aws_secret_access_key=sts_creds['SecretAccessKey'], aws_session_token=sts_creds['SessionToken'] ) + print(f"Established session for {account} with {role}") break return result @@ -342,7 +348,7 @@ def delete_cw_logs(item): filters = item['Filter'] (cw_session, account, target) = get_client_session(item, 'logs') - print(f"LOG GROUP action on {target} with filters: {filters}") + print(f"LOG GROUP action on {account}/{target} with filters: {filters}") log_groups = list_cw_lognames(context=cw_session) for log_group_name in log_groups: @@ -390,7 +396,6 @@ def delete_detector(): print('Deleting GuardDuty Detector in %s', account['Id']) gd_client.delete_detector(DetectorId=det_id) - def list_cb_projects(): ''' List all CodeBuild projects @@ -405,9 +410,7 @@ def list_cb_projects(): return projects def delete_build_projects(filters='sra-codebuild-'): - ''' - Delete the CodeBuild projects in all accounts in the organization in the current region - ''' + ''' Delete the CodeBuild projects in all accounts in the organization in the current region ''' projects = list_cb_projects() for project in projects: if project.startswith(filters): @@ -415,35 +418,57 @@ def delete_build_projects(filters='sra-codebuild-'): cb_session = SESSION.client('codebuild') cb_session.delete_project(name=project) -def get_account_info(ss_name='AWSControlTowerLoggingResources'): +def get_log_account_info(context, ss_name='AWSControlTowerLoggingResources'): + ''' List first stack instances in a stackset ''' + + cf_client = context.client('cloudformation') + if ss_name in list_active_stackset_names(cf_client): + instance = cf_client.list_stack_instances(StackSetName=ss_name) + account_id = instance['Summaries'][0]['Account'] + for account in get_list_of_accounts(): + if account['Id'] == account_id: + account_name = account['Name'] + else: + account_id = get_account_id('Log Archive') + account_name = 'LogArchive' + + return {'AccountName': account_name, 'AccountID': account_id} + +def get_audit_account_info(context, ss_name='AWSControlTowerLoggingResources'): ''' List first stack instances in a stackset ''' - result = None - if ss_name in list_active_stackset_names(): - instance = CF.list_stack_instances(StackSetName=ss_name) + cf_client = context.client('cloudformation') + if ss_name in list_active_stackset_names(cf_client): + instance = cf_client.list_stack_instances(StackSetName=ss_name) account_id = instance['Summaries'][0]['Account'] for account in get_list_of_accounts(): if account['Id'] == account_id: account_name = account['Name'] - result = {'AccountName': account_name, 'AccountID': account_id} - return result + else: + account_id = get_account_id('Audit') + account_name = 'Audit' + + return {'AccountName': account_name, 'AccountID': account_id} def get_client_session(item, client_name): - ''' - Return a session for parent or child - ''' + ''' Return a session for parent or child ''' account = None if 'Account' in item: if item['Account'] in ACCOUNTS: + print(f"Using account {ACCOUNTS[item['Account']]} for {client_name}") account = get_account_id(ACCOUNTS[item['Account']]) + print(f"Got account ID: {account}") + if account: + print(f"Establishing session to : {account}") session = establish_remote_session(account) client_session = session.client(client_name) target = account else: + print(f"Using local session for {client_name}") client_session = boto3.client(client_name) target = STS.get_caller_identity()['Account'] @@ -456,17 +481,13 @@ def delete_iam_role(item): role_name = item['Filter'] (iam_session, account, target) = get_client_session(item, 'iam') - print(f"IAM action on {target} with role_name: {role_name}") + print(f"IAM action on {account}/{target} with role_name: {role_name}") try: policies = iam_session.list_attached_role_policies(RoleName=role_name) for policy in policies['AttachedPolicies']: print(f"..Detaching policy {policy['PolicyArn']} from role {role_name}.") iam_session.detach_role_policy(RoleName=role_name, PolicyArn=policy['PolicyArn']) - policies = iam_session.list_role_policies(RoleName=role_name) - for policy in policies['PolicyNames']: - print(f"..Deleting inline policy {policy} from role {role_name}.") - iam_session.delete_role_policy(RoleName=role_name, PolicyName=policy) print(f"....Deleting role {role_name}.") iam_session.delete_role(RoleName=role_name) except Exception as exe: @@ -506,23 +527,25 @@ def run_cleanup(config): description='Clear the configuration.') PARSER.add_argument("-C", "--config", default='cleanup_config.json', help="Clear content from config") + PARSER.add_argument("-r", "--home_region", default='us-east-1', + help="Control Tower home region") ARGS = PARSER.parse_args() + CT_HOME = ARGS.home_region + + ACC_SESSION = boto3.session.Session(region_name=CT_HOME) + LOG_ACCT_NAME = 'Log Archive' + AUDIT_ACCT_NAME = 'Audit' + + LOG_ACCT_INFO = get_log_account_info(context=ACC_SESSION) + AUDIT_ACCT_INFO = get_audit_account_info(ACC_SESSION) - LOG_ACCT_INFO = get_account_info('AWSControlTowerLoggingResources') - AUDIT_ACCT_INFO = get_account_info('AWSControlTowerSecurityResources') if LOG_ACCT_INFO: LOG_ACCT_NAME = LOG_ACCT_INFO['AccountName'] - else: - LOG_ACCT_NAME = 'Log Archive' - if AUDIT_ACCT_INFO: AUDIT_ACCT_NAME = AUDIT_ACCT_INFO['AccountName'] - else: - AUDIT_ACCT_NAME = 'Audit' ACCOUNTS = {"log_account": LOG_ACCT_NAME, "audit": AUDIT_ACCT_NAME} - print('Recieved Account Info: %s', ACCOUNTS) CLEAR_CFG = ARGS.config if isfile(CLEAR_CFG): @@ -530,4 +553,4 @@ def run_cleanup(config): CONFIG = json.load(json_file) run_cleanup(CONFIG) else: - print('Config file not found: %s', CLEAR_CFG) + print('Config file not found: %s', CLEAR_CFG) \ No newline at end of file diff --git a/templates/cfn-abi-lacework-polygraph.template.yaml b/templates/cfn-abi-lacework-polygraph.template.yaml index e327cfb..9651f31 100644 --- a/templates/cfn-abi-lacework-polygraph.template.yaml +++ b/templates/cfn-abi-lacework-polygraph.template.yaml @@ -60,11 +60,20 @@ Metadata: - pLambdaLogGroupRetention - pLambdaLogGroupKmsKey - pLambdaLogLevel - - Label: default: EventBridge Rule Properties Parameters: - pComplianceFrequency + - Label: + default: Advanced Configuration Properties + Parameters: + - pControlTower + - pGovernedRegions + - pSecurityAccountId + - pLogArchiveAccountId + - pCreateAWSControlTowerExecutionRole + - pAdminRoleName + - pExecRoleName ParameterLabels: # General Properties @@ -147,6 +156,15 @@ Metadata: # EventBridge Rule Properties pComplianceFrequency: default: Frequency to Check for Organizational Compliance + # Advanced Configuration Properties + pControlTower: + default: pControlTower + pGovernedRegions: + default: pGovernedRegions + pSecurityAccountId: + default: pSecurityAccountId + pLogArchiveAccountId: + default: pLogArchiveAccountId Parameters: # General Properties pSRASourceS3BucketName: @@ -226,7 +244,7 @@ Parameters: MinLength: '1' AllowedPattern: '^[-a-zA-Z0-9_]*$' ConstraintDescription: "Invalid CloudTrail name." - Description: "Enter your existing AWS Control Tower CloudTrail name." + Description: "Enter your existing AWS CloudTrail trail name if not using AWS Control Tower." Default: 'aws-controltower-BaselineCloudTrail' LogAccountName: Type: String @@ -360,6 +378,49 @@ Parameters: MinValue: 1 MaxValue: 30 Type: Number + # Advanced Configuration Properties + pControlTower: + AllowedValues: ['true', 'false'] + Default: 'true' + Description: + Indicates whether AWS Control Tower is deployed and being used for this AWS environment. + Type: String + pSecurityAccountId: + AllowedPattern: '^\d{12}$' + Default: 111111111111 + ConstraintDescription: Must be 12 digits. + Description: AWS Account ID of the Security Tooling account (ignored for AWS Control Tower environments). + Type: String + pLogArchiveAccountId: + AllowedPattern: '^\d{12}$' + Default: 222222222222 + ConstraintDescription: Must be 12 digits. + Description: AWS Account ID of the Log Archive account (ignored for AWS Control Tower environments). + Type: String + pGovernedRegions: + AllowedPattern: '^(ct-regions)|((\b(? Date: Wed, 28 Feb 2024 19:57:59 -0800 Subject: [PATCH 2/7] Fix entrypoint --- .project_automation/functional_tests/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.project_automation/functional_tests/entrypoint.sh b/.project_automation/functional_tests/entrypoint.sh index 91ca67b..a45bff6 100755 --- a/.project_automation/functional_tests/entrypoint.sh +++ b/.project_automation/functional_tests/entrypoint.sh @@ -34,7 +34,7 @@ run_test() { unset AWS_DEFAULT_REGION echo $AWS_DEFAULT_REGION taskcat test run -n -t $1 - #.project_automation/functional_tests/scoutsuite/scoutsuite.sh + .project_automation/functional_tests/scoutsuite/scoutsuite.sh } # if account id is xxxx do this From 83a9c03d5f085354873158d1ae203cef0132b55a Mon Sep 17 00:00:00 2001 From: Gabriel Costa Date: Thu, 29 Feb 2024 10:32:22 -0800 Subject: [PATCH 3/7] Updating Cleanup script --- scripts/cleanup_config.py | 46 +++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/scripts/cleanup_config.py b/scripts/cleanup_config.py index 990ffc3..9235446 100644 --- a/scripts/cleanup_config.py +++ b/scripts/cleanup_config.py @@ -23,6 +23,13 @@ GD = SESSION.client('guardduty') STACKSTATUS = [ 'ROLLBACK_FAILED', 'ROLLBACK_COMPLETE', 'DELETE_FAILED', 'DELETE_COMPLETE'] +VALID_STATUS = ['CREATE_IN_PROGRESS', 'CREATE_FAILED', 'CREATE_COMPLETE', + 'ROLLBACK_IN_PROGRESS', 'ROLLBACK_FAILED', 'ROLLBACK_COMPLETE', + 'DELETE_IN_PROGRESS', 'DELETE_FAILED', + 'UPDATE_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS', + 'UPDATE_COMPLETE', 'UPDATE_ROLLBACK_IN_PROGRESS', + 'UPDATE_ROLLBACK_FAILED', 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', + 'UPDATE_ROLLBACK_COMPLETE', 'REVIEW_IN_PROGRESS'] def list_stacksets(context=CF): '''List all stacksets in the account''' @@ -145,11 +152,11 @@ def delete_stacksets(filters): CF.delete_stack_set(StackSetName=cf_name) def list_all_stacks(): - '''List all stacks in the account''' - response = CF.list_stacks() + '''List all stacks in the account with status other than DELETE_COMPLETE''' + response = CF.list_stacks(StackStatusFilter=VALID_STATUS) stacks = response['StackSummaries'] while response.get('NextToken'): - response = CF.list_stacks(NextToken=response['NextToken']) + response = CF.list_stacks(StackStatusFilter=VALID_STATUS, NextToken=response['NextToken']) stacks.extend(response['StackSummaries']) return stacks @@ -170,20 +177,31 @@ def is_nested_stack(stack_name): result = True return result +def list_stacks_by_prefix(stack_prefix): + '''List stacks by prefix''' + stacks = list_all_stacks() + output = [] + for stack in stacks: + if stack['StackName'].startswith(stack_prefix): + output.append(stack['StackName']) + return sorted(output, key=len) + def delete_stack(filters='tCaT-'): '''Delete all stacks created by CfCT solution in the account''' - stacks = list_all_stacks() + stacks = list_stacks_by_prefix(filters) for stack in stacks: - stack_name = stack['StackName'] - stack_status = stack['StackStatus'] - if stack_name.startswith(filters) and stack_status != 'DELETE_COMPLETE': - print(f"Deleting stack: {stack_name}") - CF.delete_stack(StackName=stack_name) + status = list_stack_status_by_name(stack) + if status: + print(f"Deleting stack: {stack}") + CF.delete_stack(StackName=stack) wait = 1 - while list_stack_status_by_name(stack_name) not in STACKSTATUS and wait < 60: - print(f"Wait: {wait}, Stack: {stack_name}") - sleep(10) + stack_status = list_stack_status_by_name(stack) + while stack_status and stack_status not in STACKSTATUS and wait < 60: + sleep_time = 15-wait/6 + print(f"Wait: {stack}, {wait}, {sleep_time}, {stack_status}") + sleep(sleep_time) wait += 1 + stack_status = list_stack_status_by_name(stack) def delete_all_objects_from_s3_bucket(bucket_name, account=None): '''Delete all objects from an S3 bucket''' @@ -488,6 +506,10 @@ def delete_iam_role(item): for policy in policies['AttachedPolicies']: print(f"..Detaching policy {policy['PolicyArn']} from role {role_name}.") iam_session.detach_role_policy(RoleName=role_name, PolicyArn=policy['PolicyArn']) + policies = iam_session.list_role_policies(RoleName=role_name) + for policy in policies['PolicyNames']: + print(f"..Deleting inline policy {policy} from role {role_name}.") + iam_session.delete_role_policy(RoleName=role_name, PolicyName=policy) print(f"....Deleting role {role_name}.") iam_session.delete_role(RoleName=role_name) except Exception as exe: From cd70124d4d2c42c2457ebf2f6ef8c7be34d487b8 Mon Sep 17 00:00:00 2001 From: Gabriel Costa Date: Tue, 5 Mar 2024 22:14:40 -0800 Subject: [PATCH 4/7] Improvements on Kishore's comments --- .taskcat.yml | 14 +++++--------- templates/cfn-abi-lacework-polygraph.template.yaml | 10 +++------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/.taskcat.yml b/.taskcat.yml index 0288fb5..0654b40 100644 --- a/.taskcat.yml +++ b/.taskcat.yml @@ -35,12 +35,8 @@ tests: pLogArchiveAccountId: $[taskcat_ssm_/nonct/log-archive-account-id] pSecurityAccountId: $[taskcat_ssm_/nonct/audit-account-id] pGovernedRegions: 'us-east-1' - pAdminRoleName: 'AWSCloudFormationStackSetAdministrationRole' - pExecRoleName: 'AWSCloudFormationStackSetExecutionRole' - LaceworkAccount: 'laceworkalliances' - OrganizationID: 'o-5py8cuszlp' - OrganizationalUnit: 'ou-aty9-5t69fbhr' - ResourceNamePrefix: 'lw-stackset' - ExistingCloudTrail: 'organization-trail' - KMSKeyIdentifierARN: $[taskcat_ssm_/lacework/CloudTrailKMSARN] - LogAccountName: 'Log Archive' \ No newline at end of file + LaceworkAccount: $[taskcat_ssm_/lacework/LaceworkAccount] + OrganizationID: $[taskcat_ssm_/lacework/OrganizationID] + OrganizationalUnit: $[taskcat_ssm_/lacework/OrgOrganizationalUnitanizationID] + ExistingCloudTrail: $[taskcat_ssm_/lacework/ExistingCloudTrail] + KMSKeyIdentifierARN: $[taskcat_ssm_/lacework/CloudTrailKMSARN] \ No newline at end of file diff --git a/templates/cfn-abi-lacework-polygraph.template.yaml b/templates/cfn-abi-lacework-polygraph.template.yaml index 9651f31..4545332 100644 --- a/templates/cfn-abi-lacework-polygraph.template.yaml +++ b/templates/cfn-abi-lacework-polygraph.template.yaml @@ -765,8 +765,8 @@ Resources: AdministrationRoleARN: !If - cControlTowerEnabled - - !Join [':', ['arn:aws:iam:', !Ref 'AWS::AccountId', 'role/service-role/AWSControlTowerStackSetRole']] - - !Join [':', ['arn:aws:iam:', !Ref 'AWS::AccountId', !Sub 'role/${pAdminRoleName}']] + - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/service-role/AWSControlTowerStackSetRole + - !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${pAdminRoleName} ExecutionRoleName: !If - cControlTowerEnabled @@ -833,11 +833,7 @@ Resources: - CfnAbiAwsSecurityHub - CfnAbiAmazonGuardDuty Properties: - TemplateURL: - !If - - cControlTowerEnabled - - !Sub "https://${pSRASourceS3BucketName}.s3.${pSRAS3BucketRegion}.${AWS::URLSuffix}/${pSRAStagingS3KeyPrefix}/templates/cfn-abi-lacework-sec-hub-ingest.template.yaml" - - !Sub "https://${pSRASourceS3BucketName}.s3.${pSRAS3BucketRegion}.${AWS::URLSuffix}/${pSRAStagingS3KeyPrefix}/templates/cfn-abi-lacework-sec-hub-ingest.template.yaml" + TemplateURL: !Sub "https://${pSRASourceS3BucketName}.s3.${pSRAS3BucketRegion}.${AWS::URLSuffix}/${pSRAStagingS3KeyPrefix}/templates/cfn-abi-lacework-sec-hub-ingest.template.yaml" Parameters: LaceworkAccount: !Select [ 0, !Split [ '.', !Ref LaceworkURL ] ] ExternalID: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref AWS::StackId ] ] ] ] From 92e2478eedfd1d1b8fccfd18b43334dba8dbf217 Mon Sep 17 00:00:00 2001 From: Gabriel Costa Date: Fri, 8 Nov 2024 09:20:40 -0800 Subject: [PATCH 5/7] Update cfn-abi-lacework-polygraph.template.yaml Fix linting error on cfn-abi-lacework-polygraph.template.yaml --- templates/cfn-abi-lacework-polygraph.template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cfn-abi-lacework-polygraph.template.yaml b/templates/cfn-abi-lacework-polygraph.template.yaml index 4545332..d4e33d4 100644 --- a/templates/cfn-abi-lacework-polygraph.template.yaml +++ b/templates/cfn-abi-lacework-polygraph.template.yaml @@ -5,7 +5,7 @@ Metadata: ParameterGroups: - Label: default: General Properties - Parameters: + Parameters: - pSRASourceS3BucketName - pSRAStagingS3KeyPrefix - pSRAS3BucketRegion From 0d0b949c60b15fc5b1add4fb46a05efd92b9abfb Mon Sep 17 00:00:00 2001 From: Gabriel Costa Date: Fri, 8 Nov 2024 09:29:07 -0800 Subject: [PATCH 6/7] Update cfn-abi-lacework-polygraph.template.yaml Update Lambda python runtime to 3.11 --- templates/cfn-abi-lacework-polygraph.template.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/cfn-abi-lacework-polygraph.template.yaml b/templates/cfn-abi-lacework-polygraph.template.yaml index d4e33d4..4d27275 100644 --- a/templates/cfn-abi-lacework-polygraph.template.yaml +++ b/templates/cfn-abi-lacework-polygraph.template.yaml @@ -454,7 +454,7 @@ Resources: Description: Get AWS Organization ID Handler: index.lambda_handler Role: !GetAtt rOrgIdGdLambdaRole.Arn - Runtime: python3.9 + Runtime: python3.11 Timeout: 60 Code: ZipFile: | @@ -682,7 +682,7 @@ Resources: Properties: Description: Copies objects from a source S3 bucket to a destination Handler: index.handler - Runtime: python3.8 + Runtime: python3.11 Role: !GetAtt 'CopyZipsRole.Arn' Timeout: 240 Code: From d19f89f39f865fdfaabe3d42934f5316ce9b741a Mon Sep 17 00:00:00 2001 From: Gabriel Costa Date: Mon, 18 Nov 2024 12:25:34 -0800 Subject: [PATCH 7/7] Update entrypoint.sh Removed scoutsuite for testing --- .project_automation/functional_tests/entrypoint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.project_automation/functional_tests/entrypoint.sh b/.project_automation/functional_tests/entrypoint.sh index a45bff6..9129de6 100755 --- a/.project_automation/functional_tests/entrypoint.sh +++ b/.project_automation/functional_tests/entrypoint.sh @@ -33,8 +33,8 @@ run_test() { echo $AWS_DEFAULT_REGION unset AWS_DEFAULT_REGION echo $AWS_DEFAULT_REGION - taskcat test run -n -t $1 - .project_automation/functional_tests/scoutsuite/scoutsuite.sh + taskcat test run -t $1 + #.project_automation/functional_tests/scoutsuite/scoutsuite.sh } # if account id is xxxx do this