def update_requested(old_exclusion: dict, update_request: dict, exclusion_config: dict, is_admin: bool): requested_update = update_request['updateRequested'] if requested_update is None or requested_update == {}: return True, None valid_update_keys = {'formFields', 'expirationDate'} given_update_keys = set(requested_update.keys()) extra_keys = given_update_keys - valid_update_keys if extra_keys: return False, {'extraKeys': list(extra_keys)} if 'formFields' in requested_update: result = dict_merge(old_exclusion.get('formFields', {}), requested_update['formFields']) is_valid, message = _validate_form_fields(exclusion_config, result) if not is_valid: return False, message if 'expirationDate' in requested_update: max_duration_in_days = exclusion_config['maxDurationInDays'] try: new_expiration = datetime.strptime( requested_update['expirationDate'], '%Y/%m/%d') except ValueError as err: logger.debug('Error parsing datetime %s', err, exc_info=True) return False, 'Unable to parse datetime' delta = new_expiration - datetime.now() if delta < timedelta(): return False, 'expirationDate must be in the future' if max_duration_in_days <= delta.days: return False, f'expirationDate must be less than the configured maxDurationInDays: {max_duration_in_days}' return True, None return True, None
def validate_update_request(old_exclusion: dict, update_request: dict, machine: dict, exclusion_config: dict, is_admin: bool): """ Validate the exclusions put request. - state change is valid - necessary permissions are present - referenced resources exist """ prospective_exclusion = dict_merge(old_exclusion, update_request) old_state = get_state(old_exclusion) new_state = get_state(prospective_exclusion) schema = state_machine.get_state_transition(machine, old_state, new_state) if not schema: raise exceptions.HttpInvalidExclusionStateChange( old_exclusion, update_request, {'message': f'cannot go from {old_state} to {new_state}'}) required_keys = [ key for key, (required, validator) in schema.items() if required ] missing_keys = [ required_key for required_key in required_keys if required_key not in update_request ] extra_keys = [ request_key for request_key in update_request if request_key not in schema ] if missing_keys or extra_keys: raise exceptions.HttpInvalidExclusionStateChange( old_exclusion, update_request, { 'missingKeys': missing_keys, 'extraKeys': extra_keys, }) validation_errors = [] for key, (_, validator) in schema.items(): if key not in update_request: continue if callable(validator): is_valid, message = validator(old_exclusion, update_request, exclusion_config, is_admin) if not is_valid: validation_errors.append({ 'property': key, 'message': message, }) if validation_errors: raise exceptions.HttpInvalidExclusionStateChange( old_exclusion, update_request, { 'validationErrors': validation_errors, })
def update_requires_replacement(current_exclusion, update_request) -> bool: if not current_exclusion: return False key_updates = {'accountId', 'requirementId', 'resourceId'} & set( update_request.keys()) if key_updates: logger.debug('Requested update of a key part/value') prospective_merge = dict_merge(current_exclusion, update_request) if exclusions_table.get_exclusion_id( prospective_merge) != exclusions_table.get_exclusion_id( current_exclusion): logger.debug( 'Provided an exclusion update that would update the exclusion ID, adding delete_exclusion to transaction items' ) return True return False
def update_exclusion(old_exclusion: dict, update_request: dict, exclusion_config: dict, is_admin: bool) -> dict: """ Validate and apply the exclusion update. Return the updated exclusion to be saved in the database. """ if not update_request: raise exceptions.HttpInvalidException( 'Must supply exclusion in the body') machine = state_machine.ADMIN_STATE_TRANSITIONS if is_admin else state_machine.USER_STATE_TRANSITIONS validate_update_request(old_exclusion, update_request, machine, exclusion_config, is_admin) new_exclusion = dict_merge(old_exclusion, update_request) if old_exclusion.get('status') != new_exclusion['status']: new_exclusion['lastStatusChangeDate'] = datetime.now().isoformat() logger.debug('Updated exclusion: %s', json.dumps(new_exclusion)) return new_exclusion
def wrapper_decorator(event, context): response = generate_default_response() try: event = parse_event(event) logger.info('Event: %s', json.dumps(event, default=str)) result = func(event, context) # if recieved raw lambda response, merge with default response if isinstance(result, dict) and 'statusCode' in result: response = dict_merge(response, result) else: response['body'] = result except exceptions.HttpException as err: logger.error('HttpException', exc_info=True) response['statusCode'] = err.status response['body'] = err.body response['body'] = format_result(response['body']) logger.info('Response: %s', json.dumps(response, default=str)) logger.debug('Response Body: %s', response['body']) return response
def form_fields(old_exclusion: dict, update_request: dict, exclusion_config: dict, is_admin: bool): result = dict_merge(old_exclusion.get('formFields', {}), update_request['formFields']) return _validate_form_fields(exclusion_config, result)
def put_exclusions_handler(event, context): user_record = event.get('userRecord', {}) authz.require_is_admin(user_record) body = event.get('body', {}) current_exclusion_id = body.get('exclusionId', '') update_request = body.get('exclusion', {}) if current_exclusion_id: _, requirement_id, _ = split_exclusion_id(current_exclusion_id) else: requirement_id = update_request.get('requirementId', '') # input validation if not update_request: raise exceptions.HttpInvalidException('Must supply exclusion to put') # data validation current_exclusion = get_current_exclusion(current_exclusion_id) prospective_exclusion = dict_merge(current_exclusion, update_request) target_account_id = prospective_exclusion.get('accountId') delete_exclusion = current_exclusion if update_requires_replacement( current_exclusion, update_request) else {} requirement = requirements_table.get(requirement_id) exclusion_type = requirement.get('exclusionType') exclusion_config = config_table.get_config( config_table.EXCLUSIONS).get(exclusion_type) logger.debug( '%s', json.dumps( { 'current_exclusion': current_exclusion, 'prospective_exclusion': prospective_exclusion, 'target_account_id': target_account_id, 'delete_exclusion': delete_exclusion, 'requirement': requirement, 'exclusion_type': exclusion_type, 'exclusion_config': exclusion_config, }, default=str)) if not requirement: raise exceptions.HttpNotFoundException( f'Requirement not found: {requirement_id}') if not exclusion_config: raise exceptions.HttpInvalidException( f'Cannot find exclusion type: {exclusion_type}') # authorization authz.require_can_request_exclusion(user_record, target_account_id) new_exclusion = exclusions.update_exclusion(current_exclusion, update_request, exclusion_config, True) if new_exclusion: new_exclusion['exclusionId'] = exclusions_table.get_exclusion_id( new_exclusion) new_exclusion['lastModifiedByAdmin'] = user_record.get('email') new_exclusion['rqrmntId_rsrceRegex'] = '#'.join( [requirement_id, new_exclusion['resourceId']]) new_exclusion['type'] = exclusion_type if delete_exclusion: delete_exclusion['exclusionId'] = exclusions_table.get_exclusion_id( delete_exclusion) exclusions_table.update_exclusion(new_exclusion, delete_exclusion) if user_record.get('email'): audit_table.put_audit_trail( user_record['email'], audit_table.PUT_EXCLUSION_ADMIN, { 'updateRequest': update_request, 'newExclusion': new_exclusion, 'deleteExclusion': delete_exclusion, }) return { 'newExclusion': new_exclusion, 'deleteExclusion': delete_exclusion, }
def put_exclusions_for_user_handler(event, context): latest_scan_id = event.get('scanId', '') user_record = event.get('userRecord', {}) body = event.get('body', {}) ncr_id = body.get('ncrId', '') update_request = body.get('exclusion', {}) scan_id, account_id, resource_id, requirement_id = split_ncr_id(ncr_id) # input validation if scan_id != latest_scan_id: raise exceptions.HttpInvalidException( 'Can only exclude ncrs from latest scans') if not update_request: raise exceptions.HttpInvalidException('Must supply exclusion to put') # data validation requirement = requirements_table.get(requirement_id) ncr = ncr_table.get_ncr(scan_id, account_id, resource_id, requirement_id) current_exclusion = exclusions_table.get_exclusion( account_id=account_id, requirement_id=requirement_id, resource_id=resource_id) exclusion_type = requirement.get('exclusionType') exclusion_types = config_table.get_config(config_table.EXCLUSIONS) exclusion_config = exclusion_types.get(exclusion_type, {}) if not requirement: raise exceptions.HttpNotFoundException( f'Requirement not found: {requirement_id}') if not ncr: raise exceptions.HttpNotFoundException(f'NCR does not exist: {ncr_id}') if not exclusion_config: raise exceptions.HttpInvalidException( f'Cannot find exclusion type: {exclusion_type}') # authorization if exclusions.is_wildcard_exclusion(current_exclusion): raise exceptions.HttpForbiddenException( 'Wildcard exclusion applied to ncr') allowed_actions = ncr_util.get_allowed_actions(user_record, account_id, requirement, current_exclusion) prospective_exclusion = dict_merge(current_exclusion, update_request) prospective_state = exclusions.get_state(prospective_exclusion) if prospective_state in exclusions.REQUEST_EXCLUSION_STATES: if not allowed_actions['requestExclusion']: raise exceptions.HttpForbiddenException('Cannot requestExclusion') if prospective_state in exclusions.REQUEST_EXCLUSION_CHANGE_STATES: if not allowed_actions['requestExclusionChange']: raise exceptions.HttpForbiddenException( 'Cannot requestExclusionChange') # update new_exclusion = exclusions.update_exclusion(current_exclusion, update_request, exclusion_config, False) new_exclusion['accountId'] = account_id new_exclusion['resourceId'] = resource_id new_exclusion['requirementId'] = requirement_id new_exclusion['type'] = exclusion_type new_exclusion['exclusionId'] = exclusions_table.get_exclusion_id( new_exclusion) new_exclusion['lastModifiedByUser'] = user_record.get('email') new_exclusion[ 'rqrmntId_rsrceRegex'] = f'{new_exclusion["requirementId"]}#{new_exclusion["resourceId"]}' exclusions_table.update_exclusion(new_exclusion, {}) if user_record.get('email'): audit_table.put_audit_trail( user_record['email'], audit_table.PUT_EXCLUSION_USER, { 'updateRequest': update_request, 'newExclusion': new_exclusion, 'deleteExclusion': {}, }) new_allowed_actions = ncr_util.get_allowed_actions(user_record, account_id, requirement, new_exclusion) updated_ncr = update_ncr_exclusion(ncr, new_exclusion, exclusion_types) logger.debug('Updated ncr: %s', json.dumps(updated_ncr, default=str)) ncr_table.put_item(Item=updated_ncr) return { 'newExclusion': new_exclusion, 'newNcr': { 'ncrId': ncr_id, 'resource': updated_ncr, 'allowedActions': new_allowed_actions, } }