diff --git a/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Lambda layers/config_lambda_layer.py/__MACOSX/._config_lambda_layer.py b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Lambda layers/config_lambda_layer.py/__MACOSX/._config_lambda_layer.py new file mode 100644 index 00000000..3d992ba9 Binary files /dev/null and b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Lambda layers/config_lambda_layer.py/__MACOSX/._config_lambda_layer.py differ diff --git a/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Lambda layers/config_lambda_layer.py/config_lambda_layer.py b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Lambda layers/config_lambda_layer.py/config_lambda_layer.py new file mode 100644 index 00000000..673efe97 --- /dev/null +++ b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Lambda layers/config_lambda_layer.py/config_lambda_layer.py @@ -0,0 +1,244 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +import json +import sys +import datetime +import boto3 +import botocore + +try: + import liblogging +except ImportError: + pass + +# Set to True to get the lambda to assume the Role attached on the Config Service (useful for cross-account). +ASSUME_ROLE_MODE = False + +# Other parameters (no change needed) +CONFIG_ROLE_TIMEOUT_SECONDS = 900 +DEFAULT_RESOURCE_TYPE = 'AWS::::Account' + +config = boto3.client('config') + + +def evaluate_parameters(rule_parameters): + kmskeyid_list = {} + if "KmsKeyId" in rule_parameters: + kmskeyid_list = [kmskeyid.strip() for kmskeyid in rule_parameters['KmsKeyId'].split(',')] + kmskeyid_list = list(filter(None, kmskeyid_list)) + + for arn in kmskeyid_list: + if not arn.startswith("arn:aws:kms:"): + raise ValueError( + 'Invalid value for the parameter "KmsKeyId", expected valid ARN(s) of Kms Key' + ) + return kmskeyid_list + +#################### +# Helper Functions # +#################### + +# Build an error to be displayed in the logs when the parameter is invalid. +def build_parameters_value_error_response(ex): + """Return an error dictionary when the evaluate_parameters() raises a ValueError. + Keyword arguments: + ex -- Exception text + """ + return build_error_response(internal_error_message="Parameter value is invalid", + internal_error_details="An ValueError was raised during the validation of the Parameter value", + customer_error_code="InvalidParameterValueException", + customer_error_message=str(ex)) + +# This gets the client after assuming the Config service role +# either in the same AWS account or cross-account. +def get_client(service, event): + """Return the service boto client. It should be used instead of directly calling the client. + Keyword arguments: + service -- the service name used for calling the boto.client() + event -- the event variable given in the lambda handler + """ + if not ASSUME_ROLE_MODE: + return boto3.client(service) + credentials = get_assume_role_credentials(event["executionRoleArn"]) + return boto3.client(service, aws_access_key_id=credentials['AccessKeyId'], + aws_secret_access_key=credentials['SecretAccessKey'], + aws_session_token=credentials['SessionToken'] + ) + +# This generate an evaluation for config +def build_evaluation(resource_id, compliance_type, event, resource_type=DEFAULT_RESOURCE_TYPE, annotation=None): + """Form an evaluation as a dictionary. Usually suited to report on scheduled rules. + Keyword arguments: + resource_id -- the unique id of the resource to report + compliance_type -- either COMPLIANT, NON_COMPLIANT or NOT_APPLICABLE + event -- the event variable given in the lambda handler + resource_type -- the CloudFormation resource type (or AWS::::Account) to report on the rule (default DEFAULT_RESOURCE_TYPE) + annotation -- an annotation to be added to the evaluation (default None) + """ + eval_cc = {} + if annotation: + eval_cc['Annotation'] = annotation + eval_cc['ComplianceResourceType'] = resource_type + eval_cc['ComplianceResourceId'] = resource_id + eval_cc['ComplianceType'] = compliance_type + eval_cc['OrderingTimestamp'] = str(json.loads(event['invokingEvent'])['notificationCreationTime']) + return eval_cc + +def build_evaluation_from_config_item(configuration_item, compliance_type, annotation=None): + """Form an evaluation as a dictionary. Usually suited to report on configuration change rules. + Keyword arguments: + configuration_item -- the configurationItem dictionary in the invokingEvent + compliance_type -- either COMPLIANT, NON_COMPLIANT or NOT_APPLICABLE + annotation -- an annotation to be added to the evaluation (default None) + """ + eval_ci = {} + if annotation: + eval_ci['Annotation'] = annotation + eval_ci['ComplianceResourceType'] = configuration_item['resourceType'] + eval_ci['ComplianceResourceId'] = configuration_item['resourceId'] + eval_ci['ComplianceType'] = compliance_type + eval_ci['OrderingTimestamp'] = configuration_item['configurationItemCaptureTime'] + return eval_ci + +#################### +# Boilerplate Code # +#################### + +# Helper function used to validate input +def check_defined(reference, reference_name): + if not reference: + raise Exception('Error: ', reference_name, 'is not defined') + return reference + +# Check whether the message is OversizedConfigurationItemChangeNotification or not +def is_oversized_changed_notification(message_type): + check_defined(message_type, 'messageType') + return message_type == 'OversizedConfigurationItemChangeNotification' + +# Check whether the message is a ScheduledNotification or not. +def is_scheduled_notification(message_type): + check_defined(message_type, 'messageType') + return message_type == 'ScheduledNotification' + +# Get configurationItem using getResourceConfigHistory API +# in case of OversizedConfigurationItemChangeNotification +def get_configuration(resource_type, resource_id, configuration_capture_time): + result = config.get_resource_config_history( + resourceType=resource_type, + resourceId=resource_id, + laterTime=configuration_capture_time, + limit=1) + configuration_item = result['configurationItems'][0] + return convert_api_configuration(configuration_item) + +# Convert from the API model to the original invocation model +def convert_api_configuration(configuration_item): + for k, v in configuration_item.items(): + if isinstance(v, datetime.datetime): + configuration_item[k] = str(v) + configuration_item['awsAccountId'] = configuration_item['accountId'] + configuration_item['ARN'] = configuration_item['arn'] + configuration_item['configurationStateMd5Hash'] = configuration_item['configurationItemMD5Hash'] + configuration_item['configurationItemVersion'] = configuration_item['version'] + configuration_item['configuration'] = json.loads(configuration_item['configuration']) + if 'relationships' in configuration_item: + for i in range(len(configuration_item['relationships'])): + configuration_item['relationships'][i]['name'] = configuration_item['relationships'][i]['relationshipName'] + return configuration_item + +# Based on the type of message get the configuration item +# either from configurationItem in the invoking event +# or using the getResourceConfigHistiry API in getConfiguration function. +def get_configuration_item(invoking_event): + check_defined(invoking_event, 'invokingEvent') + if is_oversized_changed_notification(invoking_event['messageType']): + configuration_item_summary = check_defined(invoking_event['configuration_item_summary'], 'configurationItemSummary') + return get_configuration(configuration_item_summary['resourceType'], configuration_item_summary['resourceId'], configuration_item_summary['configurationItemCaptureTime']) + if is_scheduled_notification(invoking_event['messageType']): + return None + return check_defined(invoking_event['configurationItem'], 'configurationItem') + +# Check whether the resource has been deleted. If it has, then the evaluation is unnecessary. +def is_applicable(configuration_item, event): + try: + check_defined(configuration_item, 'configurationItem') + check_defined(event, 'event') + except: + return True + status = configuration_item['configurationItemStatus'] + event_left_scope = event['eventLeftScope'] + if status == 'ResourceDeleted': + print("Resource Deleted, setting Compliance Status to NOT_APPLICABLE.") + return status in ('OK', 'ResourceDiscovered') and not event_left_scope + +def get_assume_role_credentials(role_arn): + sts_client = boto3.client('sts') + try: + assume_role_response = sts_client.assume_role(RoleArn=role_arn, + RoleSessionName="configLambdaExecution", + DurationSeconds=CONFIG_ROLE_TIMEOUT_SECONDS) + if 'liblogging' in sys.modules: + liblogging.logSession(role_arn, assume_role_response) + return assume_role_response['Credentials'] + except botocore.exceptions.ClientError as ex: + # Scrub error message for any internal account info leaks + print(str(ex)) + if 'AccessDenied' in ex.response['Error']['Code']: + ex.response['Error']['Message'] = "AWS Config does not have permission to assume the IAM role." + else: + ex.response['Error']['Message'] = "InternalError" + ex.response['Error']['Code'] = "InternalError" + raise ex + +# This removes older evaluation (usually useful for periodic rule not reporting on AWS::::Account). +def clean_up_old_evaluations(latest_evaluations, event): + + cleaned_evaluations = [] + old_eval = config.get_compliance_details_by_config_rule( + ConfigRuleName=event['configRuleName'], + ComplianceTypes=['COMPLIANT', 'NON_COMPLIANT'], + Limit=100) + + old_eval_list = [] + + while True: + for old_result in old_eval['EvaluationResults']: + old_eval_list.append(old_result) + if 'NextToken' in old_eval: + next_token = old_eval['NextToken'] + old_eval = config.get_compliance_details_by_config_rule( + ConfigRuleName=event['configRuleName'], + ComplianceTypes=['COMPLIANT', 'NON_COMPLIANT'], + Limit=100, + NextToken=next_token) + else: + break + + for old_eval in old_eval_list: + old_resource_id = old_eval['EvaluationResultIdentifier']['EvaluationResultQualifier']['ResourceId'] + newer_founded = False + for latest_eval in latest_evaluations: + if old_resource_id == latest_eval['ComplianceResourceId']: + newer_founded = True + if not newer_founded: + cleaned_evaluations.append(build_evaluation(old_resource_id, "NOT_APPLICABLE", event)) + + return cleaned_evaluations + latest_evaluations + + +def is_internal_error(exception): + return ((not isinstance(exception, botocore.exceptions.ClientError)) or exception.response['Error']['Code'].startswith('5') + or 'InternalError' in exception.response['Error']['Code'] or 'ServiceError' in exception.response['Error']['Code']) + +def build_internal_error_response(internal_error_message, internal_error_details=None): + return build_error_response(internal_error_message, internal_error_details, 'InternalError', 'InternalError') + +def build_error_response(internal_error_message, internal_error_details=None, customer_error_code=None, customer_error_message=None): + error_response = { + 'internalErrorMessage': internal_error_message, + 'internalErrorDetails': internal_error_details, + 'customerErrorMessage': customer_error_message, + 'customerErrorCode': customer_error_code + } + print(error_response) + return error_response \ No newline at end of file diff --git a/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Lambda layers/lambda layer stack.yaml/__MACOSX/._lambda layer.yaml b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Lambda layers/lambda layer stack.yaml/__MACOSX/._lambda layer.yaml new file mode 100644 index 00000000..088b80fa Binary files /dev/null and b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Lambda layers/lambda layer stack.yaml/__MACOSX/._lambda layer.yaml differ diff --git a/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Lambda layers/lambda layer stack.yaml/lambda layer.yaml b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Lambda layers/lambda layer stack.yaml/lambda layer.yaml new file mode 100644 index 00000000..57ec744a --- /dev/null +++ b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Lambda layers/lambda layer stack.yaml/lambda layer.yaml @@ -0,0 +1,16 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +Transform: AWS::Serverless-2016-10-31 + +Description: 'This template creates AWS Config lambda Layer' + +Resources: + ConfigLambdaLayer: + Type: AWS::Serverless::LayerVersion + Properties: + LayerName: Config-Lambda-Layer + Description: Lambda Layer for config rules boilerplate code + CompatibleRuntimes: + - python3.6 + - python3.7 + - python3.8 + ContentUri: 's3://custom-config-lambda-2/python.zip' \ No newline at end of file diff --git a/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Read Me.docx b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Read Me.docx new file mode 100644 index 00000000..fe9ea19b Binary files /dev/null and b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/Read Me.docx differ diff --git a/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/core-s3-outside-access-disabled - CODE/core-s3-outside-access-disabled.py b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/core-s3-outside-access-disabled - CODE/core-s3-outside-access-disabled.py new file mode 100644 index 00000000..8c83b4b9 --- /dev/null +++ b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/core-s3-outside-access-disabled - CODE/core-s3-outside-access-disabled.py @@ -0,0 +1,325 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +import json +import sys +import os +import datetime +import boto3 +import botocore +import config_lambda_layer as cl + +try: + import liblogging +except ImportError: + pass + +########## +# Paramters # +########## + +# Define the default resource to report to Config Rules +DEFAULT_RESOURCE_TYPE = 'AWS::::Account' + +# Set to True to get the lambda to assume the Role attached on the Config Service (useful for cross-account). +ASSUME_ROLE_MODE = False + +# Other parameters (no change needed) +CONFIG_ROLE_TIMEOUT_SECONDS = 900 + +config = boto3.client('config') +s3_client = boto3.client('s3') + +############# +# Main Code # +############# + +def assume_role(role_name): + """Function to assume role from another account""" + sts_connection = boto3.client('sts') + master_account = sts_connection.assume_role( + RoleArn=os.environ['OrgAssumableConfigRoleArn'], + RoleSessionName="cross_acct_lambda" + ) + access_key = master_account['Credentials']['AccessKeyId'] + secret_key = master_account['Credentials']['SecretAccessKey'] + session_token = master_account['Credentials']['SessionToken'] + + client = boto3.client('organizations', + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + aws_session_token=session_token) + + return client + + +def get_accounts(): + client = assume_role(os.environ['OrgAssumableConfigRoleArn']) + org_id = os.environ['OrganizationUnitId'] + accounts = [] + all_account_info = [] + response = client.list_accounts_for_parent( + ParentId=org_id, MaxResults=20) + all_account_info += response['Accounts'] + while 'NextToken' in response: + response = client.list_accounts_for_parent( + ParentId=org_id, MaxResults=20, NextToken=response['NextToken']) + all_account_info += response['Accounts'] + + for account in all_account_info: + # Create list of accounts from OU + accounts.append(account['Id']) + return accounts + + + +def evaluate_compliance(event, compliance, valid_rule_parameters): + account_id = event['accountId'] + #account_ids = cl.get_accounts_in_same_ou_as(account_id, os.getenv('OrgAssumableConfigRoleArn', 'arn:invalid:check:env')) + account_ids = get_accounts() + account_ids.append(account_id) + result = [] + compliance = '' + + bucket_names = s3_client.list_buckets() + buckets = [bucket['Name']for bucket in bucket_names['Buckets']] + policy = '' + for bucket in buckets: + bucket = bucket + try: + s3_policy = s3_client.get_bucket_policy(Bucket=bucket) + policy = json.loads(s3_policy['Policy']) + except: + pass + + for statement in policy['Statement']: + principal = statement['Principal'] + condition = '' + if 'Condition' in statement: + condition = statement['Condition'] + if principal == "*" and not condition: + compliance = "NON_COMPLIANT" + annotation_string = "Principal index contains wildcard and not condition" + break + elif principal != "*" and not condition: + for index in principal: + if "Service" in index: + compliance = "COMPLIANT" + annotation_string = "Principal index is valid " + continue + if "AWS" in index: + compliance = "COMPLIANT" + annotation_string = "Principal index is valid " + else: + compliance = "NON_COMPLIANT" + annotation_string = "Principal index is not AWS or Service " + continue + if type(principal[index]) is list: + for value in principal[index]: + if value == "*": + compliance = "NON_COMPLIANT" + annotation_string = "Principal index contains wildcard" + break + if value.startswith('arn:'): + value = value.split(':')[4] + if value not in account_ids: + compliance = "NON_COMPLIANT" + annotation_string = "Target arn is not within account (OU)." + break + else: + compliance = "COMPLIANT" + continue + if compliance == "NON_COMPLIANT": + break + if compliance == "NON_COMPLIANT": + break + elif type(principal[index]) is str: + value = principal[index] + if value.startswith('arn:'): + value = value.split(':')[4] + if value not in account_ids: + compliance = "NON_COMPLIANT" + annotation_string = "Target arn is not within account (OU)." + break + else: + compliance = "COMPLIANT" + continue + elif principal == "*" and condition: + if 'StringEquals' in condition: + if 'AWS:SourceOwner' in condition['StringEquals']: + value = condition['StringEquals']['AWS:SourceOwner'] + if value in account_ids: + compliance = "COMPLIANT" + annotation_string = "Target arn is within account (OU)." + else: + compliance = "NON_COMPLIANT" + annotation_string = "Target arn is not within account (OU)." + break + else: + compliance = "NON_COMPLIANT" + annotation_string = "SourceOwner is not in StringEquals condition1" + break + elif 'Bool' in condition: + if 'aws:SecureTransport' in condition['Bool']: + compliance = "COMPLIANT" + annotation_string = "SecureTransport is in the condition" + else: + compliance = "NON_COMPLIANT" + annotation_string = "SecureTransport is not in Bool the condition" + break + else: + compliance = "NON_COMPLIANT" + annotation_string = "StringEquals or Bool is incorrect in the condition" + break + else: + for index in principal: + if "Service" in index: + compliance = "COMPLIANT" + annotation_string = "Principal index is valid " + continue + if "AWS" in index: + compliance = "COMPLIANT" + annotation_string = "Principal index is valid " + else: + compliance = "NON_COMPLIANT" + annotation_string = "Principal index is not AWS or Service " + continue + if type(principal[index]) is list: + for value in principal[index]: + if value == "*": + if 'StringEquals' in condition: + if 'AWS:SourceOwner' in condition['StringEquals']: + value = condition['StringEquals']['AWS:SourceOwner'] + if value in account_ids: + compliance = "COMPLIANT" + annotation_string = "Target arn is within account (OU)." + else: + compliance = "NON_COMPLIANT" + annotation_string = "Target arn is not within account (OU)." + break + else: + compliance = "NON_COMPLIANT" + annotation_string = "SourceOwner is not in StringEquals condition1" + break + elif 'Bool' in condition: + if 'aws:SecureTransport' in condition['Bool']: + compliance = "COMPLIANT" + annotation_string = "SecureTransport is in condition1" + else: + compliance = "NON_COMPLIANT" + annotation_string = "SecureTransport is not in Bool condition" + break + else: + compliance = "NON_COMPLIANT" + annotation_string = "StringEquals or Bool is incorrect in condition" + break + if value.startswith('arn:'): + value = value.split(':')[4] + if value not in account_ids: + compliance = "NON_COMPLIANT" + annotation_string = "Target arn is not within account (OU). " + break + if compliance == "NON_COMPLIANT": + break + elif type(principal[index]) is str: + value = principal[index] + if value != "*": + if value.startswith('arn:'): + value = value.split(':')[4] + if value not in account_ids: + compliance = "NON_COMPLIANT" + annotation_string = "Target arn is not within account (OU). " + break + + result.append(cl.build_evaluation(bucket, compliance, event, DEFAULT_RESOURCE_TYPE, annotation_string)) + + return result + + + +################## +# Lambda Handler # +################## + +def lambda_handler(event, context): + if 'liblogging' in sys.modules: + liblogging.logEvent(event) + + cl.check_defined(event, 'event') + invoking_event = json.loads(event['invokingEvent']) + rule_parameters = {} + if 'ruleParameters' in event: + rule_parameters = json.loads(event['ruleParameters']) + + try: + valid_rule_parameters = cl.evaluate_parameters(rule_parameters) + except ValueError as ex: + return cl.build_parameters_value_error_response(ex) + + try: + if invoking_event['messageType'] in ['ConfigurationItemChangeNotification', 'ScheduledNotification', + 'OversizedConfigurationItemChangeNotification']: + configuration_item = cl.get_configuration_item(invoking_event) + if cl.is_applicable(configuration_item, event): + compliance_result = evaluate_compliance(event, configuration_item, valid_rule_parameters) + else: + compliance_result = "NOT_APPLICABLE" + else: + return cl.build_internal_error_response('Unexpected message type', str(invoking_event)) + except botocore.exceptions.ClientError as ex: + if cl.is_internal_error(ex): + return cl.build_internal_error_response("Unexpected error while completing API request", str(ex)) + return cl.build_error_response("Customer error while making API request", str(ex), ex.response['Error']['Code'], + ex.response['Error']['Message']) + except ValueError as ex: + return cl.build_internal_error_response(str(ex), str(ex)) + + evaluations = [] + latest_evaluations = [] + + if not compliance_result: + latest_evaluations.append( + cl.build_evaluation(event['accountId'], "NOT_APPLICABLE", event, resource_type='AWS::::Account')) + evaluations = cl.clean_up_old_evaluations(latest_evaluations, event) + elif isinstance(compliance_result, str): + if configuration_item: + evaluations.append(cl.build_evaluation_from_config_item(configuration_item, compliance_result)) + else: + evaluations.append( + cl.build_evaluation(event['accountId'], compliance_result, event, resource_type=DEFAULT_RESOURCE_TYPE)) + elif isinstance(compliance_result, list): + for evaluation in compliance_result: + missing_fields = False + for field in ('ComplianceResourceType', 'ComplianceResourceId', 'ComplianceType', 'OrderingTimestamp'): + if field not in evaluation: + print("Missing " + field + " from custom evaluation.") + missing_fields = True + + if not missing_fields: + latest_evaluations.append(evaluation) + evaluations = cl.clean_up_old_evaluations(latest_evaluations, event) + elif isinstance(compliance_result, dict): + missing_fields = False + for field in ('ComplianceResourceType', 'ComplianceResourceId', 'ComplianceType', 'OrderingTimestamp'): + if field not in compliance_result: + print("Missing " + field + " from custom evaluation.") + missing_fields = True + if not missing_fields: + evaluations.append(compliance_result) + else: + evaluations.append(cl.build_evaluation_from_config_item(configuration_item, 'NOT_APPLICABLE')) + + # Put together the request that reports the evaluation status + result_token = event['resultToken'] + test_mode = False + if result_token == 'TESTMODE': + # Used solely for RDK test to skip actual put_evaluation API call + test_mode = True + + # Invoke the Config API to report the result of the evaluation + evaluation_copy = [] + evaluation_copy = evaluations[:] + while evaluation_copy: + config.put_evaluations(Evaluations=evaluation_copy[:100], ResultToken=result_token, TestMode=test_mode) + del evaluation_copy[:100] + + # Used solely for RDK test to be able to test Lambda function + return evaluations \ No newline at end of file diff --git a/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/core-s3-policy-outside-access-disabled-STACK.yaml b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/core-s3-policy-outside-access-disabled-STACK.yaml new file mode 100644 index 00000000..a65d4524 --- /dev/null +++ b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/core-s3-policy-outside-access-disabled-STACK.yaml @@ -0,0 +1,103 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Description: 'This template Checks the given resource policy for the S3 Bucket and ensures S3 Bucket is owned by the same account.' + + +Parameters: + OrganizationUnitId: + Type: String + OrgAssumableConfigRoleArn: + Type: String + S3BucketName: + Type: String + Default: 'bucket-name' + S3zipfile: + Type: String + Default: 'filename.zip' + ConfigLambdaLayer: + Type: String + Default: 'Config-Lambda-Layer:1' + +Resources: + CoreS3OutsideAccessDisabledRule: + Type: 'AWS::Config::ConfigRule' + Properties: + ConfigRuleName: core-s3-policy-outside-access-disabled + Source: + Owner: CUSTOM_LAMBDA + SourceIdentifier: !GetAtt CoreS3OutsideAccessDisabledFunction.Arn + SourceDetails: + - EventSource: aws.config + MessageType: ScheduledNotification + Description: Checks the given resource policy for the S3 Bucket and ensures S3 Bucket is owned by the same account. + Scope: + ComplianceResourceTypes: + - AWS::S3::Bucket + DependsOn: CoreS3OutsideAccessDisabledPermission + + CoreS3OutsideAccessDisabledFunction: + Type: 'AWS::Lambda::Function' + Properties: + Code: + S3Bucket: !Ref S3BucketName + S3Key: !Ref S3zipfile + Role: !GetAtt CoreS3OutsideAccessDisabledRole.Arn + Timeout: 300 + MemorySize: 256 + FunctionName: core-s3-outside-access-disabled + Handler: core-s3-outside-access-disabled.lambda_handler + Runtime: python3.8 + Layers: + - !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:${ConfigLambdaLayer}' + Environment: + Variables: + OrgAssumableConfigRoleArn: !Ref OrgAssumableConfigRoleArn + OrganizationUnitId: !Ref OrganizationUnitId + + + CoreS3OutsideAccessDisabledPermission: + Type: 'AWS::Lambda::Permission' + Properties: + Action: 'lambda:InvokeFunction' + FunctionName: !GetAtt CoreS3OutsideAccessDisabledFunction.Arn + Principal: config.amazonaws.com + CoreS3OutsideAccessDisabledRole: + Type: 'AWS::IAM::Role' + Properties: + RoleName: !Sub 'core-s3-policy-outside-access-disabled-role-${AWS::Region}' + Path: / + ManagedPolicyArns: + - 'arn:aws:iam::aws:policy/service-role/AWSConfigRulesExecutionRole' + - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' + Policies: + - PolicyName: root + PolicyDocument: + Version: 2012-10-17 + Statement: + - Action: + - 'logs:*' + - 'config:DeleteEvaluationResults' + - 'glue:GetResourcePolicy' + - 'config:PutEvaluations' + - 's3:ListAllMyBuckets' + - 's3:GetBucketPolicy' + Resource: '*' + Effect: Allow + - PolicyName: LambdaExec + PolicyDocument: + Statement: + - Effect: Allow + Action: + - 'sts:AssumeRole' + Resource: !Sub '${OrgAssumableConfigRoleArn}' + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Action: + - 'sts:AssumeRole' + Effect: Allow + Principal: + Service: + - lambda.amazonaws.com diff --git a/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/cross-account-organisations-role.yaml b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/cross-account-organisations-role.yaml new file mode 100644 index 00000000..b6918b7c --- /dev/null +++ b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/S3_POLICY_OUTSIDE_ACCESS/cross-account-organisations-role.yaml @@ -0,0 +1,31 @@ +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +AWSTemplateFormatVersion: "2010-09-09" + +Description: "This template creates an IAM cross account role with organisational permissions" + +Parameters: + PrincipalOrgId: + Type: String + Default: "o-q8vqgayqo0" + +Resources: + CrossAccountOrganisationsRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + AWS: "*" + Action: + "sts:AssumeRole" + Condition: + StringEquals: + "aws:PrincipalOrgID": !Ref PrincipalOrgId + Path: / + ManagedPolicyArns: + - "arn:aws:iam::aws:policy/AWSSSOMasterAccountAdministrator" + - "arn:aws:iam::aws:policy/AWSSSOMemberAccountAdministrator" + - "arn:aws:iam::aws:policy/AWSOrganizationsFullAccess" diff --git a/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/parameters.json b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/parameters.json new file mode 100644 index 00000000..f3e10cf1 --- /dev/null +++ b/python-rdklib/S3_POLICY_OUTSIDE_ACCESS/parameters.json @@ -0,0 +1,12 @@ +{ + "Version": "1.0", + "Parameters": { + "RuleName": "S3_POLICY_OUTSIDE_ACCESS", + "SourceRuntime": "python3.7-lib", + "CodeKey": "S3_POLICY_OUTSIDE_ACCESS.zip", + "InputParameters": "[{\"OrganizationUnitId\": \"Desired OU to evaluate\"},{\"OrgAssumableConfigRoleArn\": \"Your cross assumable role\"},{\"S3BucketName\": \"Bucket Name of Code\"},{\"S3zipfile\": \" Name of Code\"},{\"ConfigLambdaLayer\": \" Name of Lambda Layer\"}]", + "OptionalParameters": "{}", + "SourcePeriodic": "TwentyFour_Hours" + }, + "Tags": "[]" +} \ No newline at end of file