def validate_dependencies_for_detector_update( afd_client, model: models.ResourceModel, previous_model: models.ResourceModel): # TODO: revisit this validation when/if we support in-place teardown # For now, throw bad request for unsupported event type update, and validate external models + model versions # (Other updates that would require teardown will throw exception and trigger rollback) if model.EventType.Name != previous_model.EventType.Name: raise exceptions.InvalidRequest( f"Error: EventType.Name update is not allowed") if model.EventType.Inline != previous_model.EventType.Inline: raise exceptions.InvalidRequest( f"Error: EventType.Inline update is not allowed") if not model.EventType.Inline: event_type_name = util.extract_name_from_arn(model.EventType.Arn) ( get_event_types_succeeded, _, ) = validation_helpers.check_if_get_event_types_succeeds( afd_client, event_type_name) if not get_event_types_succeeded: raise exceptions.NotFound("detector.EventType", event_type_name) validation_helpers.validate_external_models_for_detector_model( afd_client, model) validation_helpers.validate_model_versions_for_detector_model( afd_client, model)
def check_variable_entries_are_valid(arguments_to_check: dict): variable_entries_to_check = arguments_to_check.get("variableEntries", []) required_attributes = {"dataSource", "dataType", "defaultValue", "name"} all_attributes = { "dataSource", "dataType", "defaultValue", "description", "name", "variableType", } for variable_entry in variable_entries_to_check: variable_attributes = set(variable_entry.keys()) if not required_attributes.issubset(variable_attributes): missing_attributes = required_attributes.difference( variable_attributes) missing_attributes_message = ( f"Variable Entries did not have the following required attributes: {missing_attributes}" ) LOG.warning(missing_attributes_message) raise exceptions.InvalidRequest(missing_attributes_message) if not variable_attributes.issubset(all_attributes): unrecognized_attributes = variable_attributes.difference( all_attributes) unrecognized_attributes_message = ( f"Error: variable entries has unrecognized attributes: {unrecognized_attributes}" ) LOG.warning(unrecognized_attributes_message) raise exceptions.InvalidRequest(unrecognized_attributes_message) return True
def _validate_inline_event_variable_for_event_type_update( afd_client, event_variable, previous_variables): if not event_variable.Name: raise exceptions.InvalidRequest( "Error occurred: inline event variables must include Name!") # TODO: update this logic if we support in-place Teardown # This difference would require teardown if we were to support it # check for differences in dataSource or dataType differences = {} previous_variable = previous_variables.get(event_variable.Name, None) if previous_variable: differences = validation_helpers.check_variable_differences( previous_variable, event_variable) if differences["dataSource"] or differences["dataType"]: raise exceptions.InvalidRequest( "Error occurred: cannot update event variable data source or data type!" ) if not previous_variable: # create inline variable that does not already exist common_helpers.create_inline_event_variable( frauddetector_client=afd_client, event_variable=event_variable) else: # get existing variable to get arn. Arn is readonly property, so it will not be attached to input model ( get_variables_worked, get_variables_response, ) = validation_helpers.check_if_get_variables_succeeds( afd_client, event_variable.Name) if not get_variables_worked: raise RuntimeError( f"Previously existing event variable {event_variable.Name} no longer exists!" ) event_variable.Arn = get_variables_response.get("variables")[0].get( "arn") # update existing inline variable if hasattr(event_variable, "Tags"): common_helpers.update_tags( frauddetector_client=afd_client, afd_resource_arn=event_variable.Arn, new_tags=event_variable.Tags, ) var_type = [ None, event_variable.VariableType ][event_variable.VariableType != previous_variable.VariableType] api_helpers.call_update_variable( variable_name=event_variable.Name, frauddetector_client=afd_client, variable_default_value=event_variable.DefaultValue, variable_description=event_variable.Description, variable_type=var_type, )
def update_handler( session: Optional[SessionProxy], request: ResourceHandlerRequest, callback_context: MutableMapping[str, Any], # pylint: disable=unused-argument ) -> ProgressEvent: """This function is triggered by the CloudFormation UPDATE event and will update the StepConcurrency level to the new provided value within the up to the max of 256. It will also add a tag to the cluster in order to keep track of the resource. Attributes: session (Optional[SessionProxy]): The session proxy for connecting to the needed AWS API client cluster_id (str): The unique ID of the cluster to get details from callback_context (MutableMapping[str, Any]): Use to store any state between re-invocation via IN_PROGRESS Returns: ProgressEvent: An event with the status of the action """ model = request.desiredResourceState previous_model = request.previousResourceState progress: ProgressEvent = ProgressEvent( status=OperationStatus.IN_PROGRESS, resourceModel=model, ) LOG.info("UPDATE HANDLER") LOG.info("MODEL") LOG.info(model) LOG.info("PREVIOUS") LOG.info(previous_model) model.StepConcurrencyLevel = int(model.StepConcurrencyLevel) if model.UID != previous_model.UID: raise exceptions.InvalidRequest("Cannot update the UID") if model.StepConcurrencyLevel < 1 or model.StepConcurrencyLevel > 256: raise exceptions.InvalidRequest( f"Step Concurency Level must be between 1 and 256, \ {model.StepConcurrencyLevel} was given.") if model.UID != get_uid(session, model.ClusterId): raise exceptions.NotFound(TYPE_NAME, model.ClusterId) try: client = session.client('emr') LOG.info("Updating concurrency to %s for cluster %s", model.StepConcurrencyLevel, model.ClusterId) response = client.modify_cluster( ClusterId=model.ClusterId, StepConcurrencyLevel=model.StepConcurrencyLevel) LOG.info("RESPONSE: %s", response) progress.status = OperationStatus.SUCCESS except Exception as unexpected_exception: LOG.error(str(unexpected_exception)) raise exceptions.InternalFailure( f"Failed Update: {str(unexpected_exception)}") return progress
def create_handler( session: Optional[SessionProxy], request: ResourceHandlerRequest, callback_context: MutableMapping[str, Any], # pylint: disable=unused-argument ) -> ProgressEvent: """This function is triggered by the CloudFormation CREATE event and will set the StepConcurrency level from the default of 1 to the new provided value within the up to the max of 256. It will also add a tag to the cluster in order to keep track of the resource. Attributes: session (Optional[SessionProxy]): The session proxy for connecting to the needed AWS API client cluster_id (str): The unique ID of the cluster to get details from callback_context (MutableMapping[str, Any]): Use to store any state between re-invocation via IN_PROGRESS Returns: ProgressEvent: An event with the status of the action """ LOG.info("Create Handler") model = request.desiredResourceState progress: ProgressEvent = ProgressEvent( status=OperationStatus.IN_PROGRESS, resourceModel=model, ) model.UID = "cluster:" + model.ClusterId model.StepConcurrencyLevel = int(model.StepConcurrencyLevel) uid = get_uid(session, model.ClusterId) LOG.info("UID: %s", uid) if uid == model.UID: raise exceptions.AlreadyExists(TYPE_NAME, model.ClusterId) if model.StepConcurrencyLevel < 1 or model.StepConcurrencyLevel > 256: raise exceptions.InvalidRequest( f"Step Concurency Level must be between 1 and 256, \ {model.StepConcurrencyLevel} was given.") try: client = session.client('emr') LOG.info("Setting concurrency to %s for cluster %s", model.StepConcurrencyLevel, model.ClusterId) response = client.modify_cluster(ClusterId=model.ClusterId, StepConcurrencyLevel=int( model.StepConcurrencyLevel)) LOG.info("RESPONSE TO SET CONCURRENCY:") LOG.info(response) LOG.info("Setting UID tag to %s", model.ClusterId) tag_response = client.add_tags(ResourceId=model.ClusterId, Tags=[{ "Key": "StepConcurrencyUID", "Value": model.UID }]) LOG.info("RESPONSE TO ADD TAGS:") LOG.info(tag_response) progress.status = OperationStatus.SUCCESS except Exception as unexpected_exception: LOG.error(str(unexpected_exception)) raise exceptions.InternalFailure( f"Failed Create: {str(unexpected_exception)}") return progress
def pre_update_handler( session: Optional[SessionProxy], request: BaseHookHandlerRequest, callback_context: MutableMapping[str, Any], type_configuration: TypeConfigurationModel) -> ProgressEvent: target_model = request.hookContext.targetModel progress: ProgressEvent = ProgressEvent(status=OperationStatus.IN_PROGRESS) target_name = request.hookContext.targetName try: LOG.debug("Hook context:") LOG.debug(request.hookContext) # Reading the Resource Hook's target new properties resource_properties = target_model.get("resourceProperties") # Only need to check if the new resource properties match the required TypeConfiguration. # This will block automatically if they are trying to remove a permission boundary. if "AWS::S3::Bucket" == target_name: progress = _validate_block_public_access( target_name, resource_properties, type_configuration.excludedBucketSuffixes) else: raise exceptions.InvalidRequest( f"Unknown target type: {target_name}") except exceptions.InvalidRequest as e: progress.status = OperationStatus.FAILED progress.message = "Unknown target type: {target_name}" except BaseException as e: progress = ProgressEvent.failed(HandlerErrorCode.InternalFailure, f"Unexpected error {e}") return progress
def validate_model_versions_for_detector_model(afd_client, model: models.ResourceModel): if model.AssociatedModels is None: return for item in model.AssociatedModels: if util.is_external_model_arn(item.Arn): continue model_id, model_type, model_version_number = util.get_model_version_details_from_arn(item.Arn) get_model_version_worked, response = check_if_get_model_version_succeeds( frauddetector_client=afd_client, model_id=model_id, model_type=model_type, model_version_number=model_version_number, ) if not get_model_version_worked: raise exceptions.NotFound("ModelVersion", item.Arn) if response["status"] != "ACTIVE": raise exceptions.InvalidRequest( "Specified model must be in status:ACTIVE, ModelVersion arn='{}'".format(item.Arn) )
def pre_create_handler( session: Optional[SessionProxy], request: HookHandlerRequest, callback_context: MutableMapping[str, Any], type_configuration: TypeConfigurationModel) -> ProgressEvent: progress: ProgressEvent = ProgressEvent(status=OperationStatus.IN_PROGRESS) target_name = request.hookContext.targetName try: LOG.debug("Hook context:") LOG.debug(request.hookContext) if "AWS::S3::Bucket" == target_name: progress = _validate_block_public_access( target_name, request.hookContext.targetModel.get("resourceProperties"), type_configuration.excludedBucketSuffixes) else: raise exceptions.InvalidRequest( f"Unknown target type: {target_name}") except exceptions.InvalidRequest as e: progress.status = OperationStatus.FAILED progress.message = "Unknown target type: {target_name}" except BaseException as e: progress = ProgressEvent.failed(HandlerErrorCode.InternalFailure, f"Unexpected error {e}") return progress
def _validate_inline_label_for_event_type_update(afd_client, label, previous_labels): if label.Name is None: raise exceptions.InvalidRequest( "Error occurred: inline labels must include Name!") previous_label = previous_labels.get(label.Name, None) if not previous_label: # put inline label that does not already exist common_helpers.put_inline_label(frauddetector_client=afd_client, label=label) else: # get existing label to get arn. Arn is readonly property, so it will not be attached to input model ( get_labels_worked, get_labels_response, ) = validation_helpers.check_if_get_labels_succeeds( afd_client, label.Name) if not get_labels_worked: raise RuntimeError( f"Previously existing label {label.Name} no longer exists!") label.Arn = get_labels_response.get("labels")[0].get("arn") # put existing inline label and update tags common_helpers.put_inline_label(frauddetector_client=afd_client, label=label) if hasattr(label, "Tags"): common_helpers.update_tags( frauddetector_client=afd_client, afd_resource_arn=label.Arn, new_tags=label.Tags, )
def _validate_inline_entity_type_for_event_type_update(afd_client, entity_type, previous_entity_types): if entity_type.Name is None: raise exceptions.InvalidRequest( "Error occurred: inline entity types must include Name!") previous_entity_type = previous_entity_types.get(entity_type.Name, None) if not previous_entity_type: # put inline entity type that does not already exist common_helpers.put_inline_entity_type(frauddetector_client=afd_client, entity_type=entity_type) else: # get existing entity type to get arn. Arn is readonly property, so it will not be attached to input model ( get_entity_types_worked, get_entity_types_response, ) = validation_helpers.check_if_get_entity_types_succeeds( afd_client, entity_type.Name) if not get_entity_types_worked: raise RuntimeError( f"Previously existing entity type {entity_type.Name} no longer exists!" ) entity_type.Arn = get_entity_types_response.get("entityTypes")[0].get( "arn") # put existing inline entity type and update tags common_helpers.put_inline_entity_type(frauddetector_client=afd_client, entity_type=entity_type) if hasattr(entity_type, "Tags"): common_helpers.update_tags( frauddetector_client=afd_client, afd_resource_arn=entity_type.Arn, new_tags=entity_type.Tags, )
def pre_create_handler( _session: Optional[SessionProxy], request: HookHandlerRequest, _callback_context: MutableMapping[str, Any], _type_configuration: TypeConfigurationModel) -> ProgressEvent: target_model = request.hookContext.targetModel resource_properties = target_model.get("resourceProperties") resource_name = request.hookContext.targetLogicalId target_name = request.hookContext.targetName LOG.info("{} handler triggered for resource {} of type {}".format( TYPE_NAME, resource_name, target_name)) if "AWS::IAM::User" == target_name: progress = _checkIamUserHasPolicies(resource_properties) else: raise exceptions.InvalidRequest(f"Unknown target type: {target_name}") if progress.status != OperationStatus.SUCCESS: LOG.info("{} FAIL: Resource {} - {}".format(TYPE_NAME, resource_name, progress.message)) else: LOG.info("{} SUCCESS: Resource {} - {}".format(TYPE_NAME, resource_name, progress.message)) return progress
def _validate_rule_for_detector_create(afd_client, model: models.ResourceModel, rule: models.Rule): if model.DetectorId != rule.DetectorId: raise exceptions.InvalidRequest( f"Rule {rule.RuleId} detector id {rule.DetectorId} does not match detector id {model.DetectorId}!" ) _validate_outcomes_for_rule(afd_client, rule)
def get_model_version_details_from_arn(arn): if not is_model_version_arn(arn): raise exceptions.InvalidRequest( "Unexpected ARN provided in AssociatedModels: {}".format(arn)) segmented_arn = arn.split("/") model_version_number = segmented_arn[-1] model_id = segmented_arn[-2] model_type = segmented_arn[-3] return model_id, model_type, model_version_number
def pre_create_handler( session: Optional[SessionProxy], request: HookHandlerRequest, callback_context: MutableMapping[str, Any], type_configuration: TypeConfigurationModel) -> ProgressEvent: target_model = request.hookContext.targetModel progress: ProgressEvent = ProgressEvent(status=OperationStatus.IN_PROGRESS) resource_properties = target_model.get("resourceProperties") resource_name = request.hookContext.targetLogicalId target_name = request.hookContext.targetName LOG.info( f"{TYPE_NAME} CREATE_PRE_PROVISION handler triggered for resource {resource_name} of type {target_name}" ) adminPolicyFound = False if "AWS::IAM::Policy" == target_name: adminPolicyFound = _isAdminPolicy(resource_properties) elif "AWS::IAM::User" == target_name or "AWS::IAM::Role" == target_name or "AWS::IAM::Group" == target_name: if resource_properties: policies = resource_properties.get("Policies") if policies: for policy in policies: # iterate through them all - if multiple violations, logs will report them all adminPolicyFound = adminPolicyFound or _isAdminPolicy( policy) else: LOG.info("No policies defined in this resource") else: raise exceptions.InternalFailure(f"{target_name} model empty") else: raise exceptions.InvalidRequest(f"Unknown target type: {target_name}") if adminPolicyFound: progress.status = OperationStatus.FAILED progress.errorCode = HandlerErrorCode.NonCompliant progress.message = "One or more policies granting 'Allow action * on resource *' found" else: progress.status = OperationStatus.SUCCESS progress.message = "No policies granting 'Allow action * on resource *' found" if progress.status != OperationStatus.SUCCESS: LOG.info( f"{TYPE_NAME} FAIL: Resource {resource_name} - {progress.message}") else: LOG.info( f"{TYPE_NAME} SUCCESS: Resource {resource_name} - {progress.message}" ) return progress
def set_cidr_list(model): if model.HostCounts: if not isinstance(model.HostCounts, list) or not all( isinstance(item, int) for item in model.HostCounts): raise exceptions.InvalidRequest( f"Host number list must be an array of integers, received {model.HostCounts}" ) try: cidr_list = Lister().split_by_host_numbers(model.CidrToSplit, model.HostCounts) except ValueError as value_error: raise exceptions.InvalidRequest(str(value_error)) elif model.PrefixForEvenSplit: cidr_list = Lister().split_by_prefix(model.CidrToSplit, model.PrefixForEvenSplit) else: raise exceptions.InvalidRequest( f"Must pass either a host count list or a prefix to split the cidr by" ) return cidr_list
def _validate_inline_label_for_create(afd_client, label): if label.Name is None: raise exceptions.InvalidRequest( "Error occurred: inline labels must include Name!") get_labels_worked, _ = validation_helpers.check_if_get_labels_succeeds( afd_client, label.Name) if get_labels_worked: raise exceptions.AlreadyExists("label", label.Name) common_helpers.put_inline_label(afd_client, label)
def _validate_inline_entity_type_for_create(afd_client, entity_type): if entity_type.Name is None: raise exceptions.InvalidRequest( "Error occurred: inline entity types must include Name!") get_entity_types_worked, _ = validation_helpers.check_if_get_entity_types_succeeds( afd_client, entity_type.Name) if get_entity_types_worked: raise exceptions.AlreadyExists("entity_type", entity_type.Name) common_helpers.put_inline_entity_type(afd_client, entity_type)
def _validate_inline_event_variable_for_create(afd_client, event_variable): if event_variable.Name is None: raise exceptions.InvalidRequest( "Error occurred: inline event variables must include Name!") get_variables_worked, _ = validation_helpers.check_if_get_variables_succeeds( afd_client, event_variable.Name) if get_variables_worked: raise exceptions.AlreadyExists("event_variable", event_variable.Name) common_helpers.create_inline_event_variable( frauddetector_client=afd_client, event_variable=event_variable)
def pre_create_handler( session: Optional[SessionProxy], request: HookHandlerRequest, callback_context: MutableMapping[str, Any], type_configuration: TypeConfigurationModel ) -> ProgressEvent: target_name = request.hookContext.targetName if "AWS::S3::Bucket" == target_name: response = _validate_s3_bucket_encryption(request.hookContext.targetModel.get("resourceProperties"), type_configuration.encryptionAlgorithm) else: raise exceptions.InvalidRequest(f"Unknown target type: {target_name}") LOG.info(response) return response
def execute_create_detector_handler_work(session: SessionProxy, model: models.ResourceModel, progress: ProgressEvent): afd_client = client_helpers.get_afd_client(session) # For contract_create_duplicate, we need to fail if the resource already exists get_detectors_works, _ = validation_helpers.check_if_get_detectors_succeeds( afd_client, model.DetectorId) if get_detectors_works: raise exceptions.AlreadyExists("detector", model.DetectorId) # For contract_invalid_create, fail if any read-only properties are present if model.Arn is not None or model.CreatedTime is not None or model.LastUpdatedTime is not None: raise exceptions.InvalidRequest( "Error occurred: cannot create read-only properties.") # Validate existence of referenced resources, validate and create inline resources (except for Rules, Detector, DV) # Also check existence of external models # TODO: split out creation from validation create_worker_helpers.validate_dependencies_for_detector_create( afd_client, model) # Create Detector, Rules, Detector Version ID model_helpers.put_detector_for_model(afd_client, model) rule_dicts = create_worker_helpers.create_rules_for_detector_resource( afd_client, model) detector_version_response = create_worker_helpers.create_detector_version_for_detector_resource( afd_client, model, rule_dicts) # The DV will be created as draft by default, so if the desired status is not draft, update DV status if model.DetectorVersionStatus != DRAFT_STATUS: api_helpers.call_update_detector_version_status( frauddetector_client=afd_client, detector_id=model.DetectorId, detector_version_id=detector_version_response.get( "detectorVersionId", "1"), # version here should be 1 status=model.DetectorVersionStatus, ) # after satisfying all contract tests and AFD requirements, get the resulting model model = read_worker_helpers.validate_detector_exists_and_return_detector_resource_model( afd_client, model) progress.resourceModel = model progress.status = OperationStatus.SUCCESS LOG.info(f"Returning Progress with status: {progress.status}") return progress
def pre_update_handler( session: Optional[SessionProxy], request: BaseHookHandlerRequest, callback_context: MutableMapping[str, Any], type_configuration: TypeConfigurationModel ) -> ProgressEvent: target_model = request.hookContext.targetModel target_name = request.hookContext.targetName progress: ProgressEvent = ProgressEvent( status=OperationStatus.IN_PROGRESS ) if "AWS::RDS::DBInstance" == target_name: LOG.info(f"Triggered PreUpdateHookHandler for target {target_name}") return _validate_rds_encryption(progress, target_name, target_model.get("resourceProperties"), type_configuration.excludeDBInstanceClassList) else: raise exceptions.InvalidRequest(f"Unknown target type: {target_name}")
def execute_create_label_handler_work(session, model, progress): afd_client = client_helpers.get_afd_client(session) # For contract_create_duplicate, we need to fail if resource already exists get_labels_works, _ = validation_helpers.check_if_get_labels_succeeds(afd_client, model.Name) if get_labels_works: raise exceptions.AlreadyExists("label", model.Name) # For contract_invalid_create, fail if any read-only properties are present if model.Arn is not None or model.CreatedTime is not None or model.LastUpdatedTime is not None: raise exceptions.InvalidRequest("Error occurred: cannot create read-only properties.") # API does not handle 'None' property gracefully if model.Tags is None: del model.Tags # after satisfying contract call put label return common_helpers.put_label_and_return_progress(afd_client, model, progress)
def execute_create_event_type_handler_work(session, model, progress): afd_client = client_helpers.get_afd_client(session) # For contract_create_duplicate, we need to fail if the resource already exists get_event_type_works, _ = validation_helpers.check_if_get_event_types_succeeds( afd_client, model.Name) if get_event_type_works: raise exceptions.AlreadyExists("event_type", model.Name) # For contract_invalid_create, fail if any read-only properties are present if model.Arn is not None or model.CreatedTime is not None or model.LastUpdatedTime is not None: raise exceptions.InvalidRequest( "Error occurred: cannot create read-only properties.") # Validate existence of referenced resources, validate and create inline resources create_worker_helpers.validate_dependencies_for_create(afd_client, model) # after satisfying contract call put event_type return common_helpers.put_event_type_and_return_progress( afd_client, model, progress)
def pre_create_handler( _session: Optional[SessionProxy], request: HookHandlerRequest, _callback_context: MutableMapping[str, Any], _type_configuration: TypeConfigurationModel) -> ProgressEvent: target_name = request.hookContext.targetName target_model = request.hookContext.targetModel if "AWS::IAM::Policy" == target_name: response = _validate_iam_policy_encryption( target_model.get("resourceProperties", target_model)) elif "AWS::IAM::Role" == target_name: policies = target_model.get("resourceProperties", target_model).get("Policies") if policies: response = _validate_iam_role_policies_encryption(policies) else: response = ProgressEvent( status=OperationStatus.SUCCESS, message=f"No inline policies in role to validate", errorCode=None) else: raise exceptions.InvalidRequest(f"Unknown target type: {target_name}") return response