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,
            })
示例#3
0
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
示例#5
0
    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)
示例#7
0
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,
    }
示例#8
0
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,
        }
    }