new: Self registration, object level search (initial)

pull/573/head
Raphaël Vinot 2020-05-07 12:17:31 +02:00
parent e020bac5f6
commit 4a060b3c07
5 changed files with 485 additions and 342 deletions

View File

@ -24,14 +24,14 @@ Response (if any):
try:
from .exceptions import PyMISPError, NewEventError, NewAttributeError, MissingDependency, NoURL, NoKey, InvalidMISPObject, UnknownMISPObjectTemplate, PyMISPInvalidFormat, MISPServerError, PyMISPNotImplementedYet, PyMISPUnexpectedResponse, PyMISPEmptyResponse # noqa
from .abstract import AbstractMISP, MISPEncode, pymisp_json_default, MISPTag, Distribution, ThreatLevel, Analysis # noqa
from .mispevent import MISPEvent, MISPAttribute, MISPObjectReference, MISPObjectAttribute, MISPObject, MISPUser, MISPOrganisation, MISPSighting, MISPLog, MISPShadowAttribute, MISPWarninglist, MISPTaxonomy, MISPNoticelist, MISPObjectTemplate, MISPSharingGroup, MISPRole, MISPServer, MISPFeed, MISPEventDelegation, MISPUserSetting # noqa
from .mispevent import MISPEvent, MISPAttribute, MISPObjectReference, MISPObjectAttribute, MISPObject, MISPUser, MISPOrganisation, MISPSighting, MISPLog, MISPShadowAttribute, MISPWarninglist, MISPTaxonomy, MISPNoticelist, MISPObjectTemplate, MISPSharingGroup, MISPRole, MISPServer, MISPFeed, MISPEventDelegation, MISPUserSetting, MISPInbox # noqa
from .tools import AbstractMISPObjectGenerator # noqa
from .tools import Neo4j # noqa
from .tools import stix # noqa
from .tools import openioc # noqa
from .tools import ext_lookups # noqa
from .api import PyMISP # noqa
from .api import PyMISP, register_user # noqa
from .api import PyMISP as ExpandedPyMISP # noqa
from .tools import load_warninglists # noqa
# Let's not bother with old python

View File

@ -21,7 +21,7 @@ except ImportError:
import logging
from enum import Enum
from typing import Union, Optional
from typing import Union, Optional, Any, Dict, Iterable, List, Set
from .exceptions import PyMISPInvalidFormat, PyMISPError
@ -76,7 +76,7 @@ class Analysis(Enum):
completed = 2
def _int_to_str(d: dict) -> dict:
def _int_to_str(d: Dict[str, Any]) -> Dict[str, Any]:
# transform all integer back to string
for k, v in d.items():
if isinstance(v, dict):
@ -114,9 +114,9 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta):
"""
super().__init__()
self.__edited: bool = True # As we create a new object, we assume it is edited
self.__not_jsonable: list = []
self._fields_for_feed: set
self.__self_defined_describe_types: Union[dict, None] = None
self.__not_jsonable: List[str] = []
self._fields_for_feed: Set
self.__self_defined_describe_types: Optional[Dict] = None
self.uuid: str
if kwargs.get('force_timestamps') is not None:
@ -126,13 +126,13 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta):
self.__force_timestamps: bool = False
@property
def describe_types(self) -> dict:
def describe_types(self) -> Dict:
if self.__self_defined_describe_types:
return self.__self_defined_describe_types
return self.__describe_types
@describe_types.setter
def describe_types(self, describe_types: dict):
def describe_types(self, describe_types: Dict):
self.__self_defined_describe_types = describe_types
@property
@ -166,7 +166,7 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta):
"""Add entries to the __not_jsonable list"""
self.__not_jsonable += args
def set_not_jsonable(self, args: list) -> None:
def set_not_jsonable(self, args: List[str]) -> None:
"""Set __not_jsonable to a new list"""
self.__not_jsonable = args
@ -174,7 +174,7 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta):
"""Load a JSON string"""
self.from_dict(**loads(json_string))
def to_dict(self) -> dict:
def to_dict(self) -> Dict:
"""Dump the class to a dictionary.
This method automatically removes the timestamp recursively in every object
that has been edited is order to let MISP update the event accordingly."""
@ -206,11 +206,11 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta):
to_return = _int_to_str(to_return)
return to_return
def jsonable(self) -> dict:
def jsonable(self) -> Dict:
"""This method is used by the JSON encoder"""
return self.to_dict()
def _to_feed(self) -> dict:
def _to_feed(self) -> Dict:
if not hasattr(self, '_fields_for_feed') or not self._fields_for_feed:
raise PyMISPError('Unable to export in the feed format, _fields_for_feed is missing.')
if hasattr(self, '_set_default') and callable(self._set_default): # type: ignore
@ -281,7 +281,7 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta):
else:
raise PyMISPError('edited can only be True or False')
def __setattr__(self, name: str, value):
def __setattr__(self, name: str, value: Any):
if name[0] != '_' and not self.__edited and name in self:
# The private members don't matter
# If we already have a key with that name, we're modifying it.
@ -315,7 +315,7 @@ class AbstractMISP(MutableMapping, MISPFileCache, metaclass=ABCMeta):
self.edited = True
return misp_tag
def _set_tags(self, tags):
def _set_tags(self, tags: Iterable['MISPTag']):
"""Set a list of prepared MISPTag."""
if all(isinstance(x, MISPTag) for x in tags):
self.Tag = tags
@ -341,6 +341,7 @@ class MISPTag(AbstractMISP):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.name: str
self.exportable: bool
def from_dict(self, **kwargs):
if kwargs.get('Tag'):
@ -351,14 +352,14 @@ class MISPTag(AbstractMISP):
if not hasattr(self, 'colour'):
self.colour = '#ffffff'
def _to_feed(self):
def _to_feed(self) -> Dict:
if hasattr(self, 'exportable') and not self.exportable:
return False
return {}
return super()._to_feed()
if HAS_RAPIDJSON:
def pymisp_json_default(obj: Union[AbstractMISP, datetime, date, Enum, UUID]) -> Union[dict, str]:
def pymisp_json_default(obj: Union[AbstractMISP, datetime, date, Enum, UUID]) -> Union[Dict, str]:
if isinstance(obj, AbstractMISP):
return obj.jsonable()
elif isinstance(obj, (datetime, date)):
@ -368,7 +369,7 @@ if HAS_RAPIDJSON:
elif isinstance(obj, UUID):
return str(obj)
else:
def pymisp_json_default(obj: Union[AbstractMISP, datetime, date, Enum, UUID]) -> Union[dict, str]:
def pymisp_json_default(obj: Union[AbstractMISP, datetime, date, Enum, UUID]) -> Union[Dict, str]:
if isinstance(obj, AbstractMISP):
return obj.jsonable()
elif isinstance(obj, (datetime, date)):

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ from collections import defaultdict
import logging
import hashlib
from pathlib import Path
from typing import List, Optional, Union, IO
from typing import List, Optional, Union, IO, Dict, Any
from .abstract import AbstractMISP, MISPTag
from .exceptions import UnknownMISPObjectTemplate, InvalidMISPObject, PyMISPError, NewEventError, NewAttributeError
@ -82,7 +82,7 @@ def _make_datetime(value) -> datetime:
return value
def make_bool(value: Union[bool, int, str, dict, list, None]) -> bool:
def make_bool(value: Optional[Union[bool, int, str, dict, list]]) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, int):
@ -102,6 +102,10 @@ class MISPOrganisation(AbstractMISP):
_fields_for_feed: set = {'name', 'uuid'}
def __init__(self):
super().__init__()
self.id: int
def from_dict(self, **kwargs):
if 'Organisation' in kwargs:
kwargs = kwargs['Organisation']
@ -169,17 +173,17 @@ class MISPAttribute(AbstractMISP):
_fields_for_feed: set = {'uuid', 'value', 'category', 'type', 'comment', 'data',
'timestamp', 'to_ids', 'disable_correlation', 'first_seen', 'last_seen'}
def __init__(self, describe_types: Optional[dict]=None, strict: bool=False):
def __init__(self, describe_types: Optional[Dict]=None, strict: bool=False):
"""Represents an Attribute
:describe_type: Use it is you want to overwrite the defualt describeTypes.json file (you don't)
:strict: If false, fallback to sane defaults for the attribute type if the ones passed by the user are incorrect
"""
super().__init__()
if describe_types:
self.describe_types: dict = describe_types
self.describe_types: Dict[str, Any] = describe_types
self.__categories: List[str] = self.describe_types['categories']
self.__category_type_mapping: dict = self.describe_types['category_type_mappings']
self.__sane_default: dict = self.describe_types['sane_defaults']
self.__category_type_mapping: Dict[str, List[str]] = self.describe_types['category_type_mappings']
self.__sane_default: Dict[str, Dict[str, Union[str, int]]] = self.describe_types['sane_defaults']
self.__strict: bool = strict
self.data: Optional[BytesIO] = None
self.first_seen: datetime
@ -194,7 +198,7 @@ class MISPAttribute(AbstractMISP):
self.Event: MISPEvent
self.RelatedAttribute: List[MISPAttribute]
def add_tag(self, tag: Optional[Union[str, MISPTag, dict]]=None, **kwargs) -> MISPTag:
def add_tag(self, tag: Optional[Union[str, MISPTag, Dict]]=None, **kwargs) -> MISPTag:
return super()._add_tag(tag, **kwargs)
@property
@ -207,7 +211,7 @@ class MISPAttribute(AbstractMISP):
"""Set a list of prepared MISPTag."""
super()._set_tags(tags)
def _prepare_data(self, data: Union[Path, str, bytes, BytesIO, None]):
def _prepare_data(self, data: Optional[Union[Path, str, bytes, BytesIO]]):
if not data:
super().__setattr__('data', None)
return
@ -239,15 +243,15 @@ class MISPAttribute(AbstractMISP):
# not a encrypted zip file, assuming it is a new malware sample
self._prepare_new_malware_sample()
def __setattr__(self, name, value):
def __setattr__(self, name: str, value: Any):
if name in ['first_seen', 'last_seen']:
value = _make_datetime(value)
_datetime = _make_datetime(value)
if name == 'last_seen' and hasattr(self, 'first_seen') and self.first_seen > value:
if name == 'last_seen' and hasattr(self, 'first_seen') and self.first_seen > _datetime:
raise PyMISPError('last_seen ({value}) has to be after first_seen ({self.first_seen})')
if name == 'first_seen' and hasattr(self, 'last_seen') and self.last_seen < value:
if name == 'first_seen' and hasattr(self, 'last_seen') and self.last_seen < _datetime:
raise PyMISPError('first_seen ({value}) has to be before last_seen ({self.last_seen})')
super().__setattr__(name, value)
super().__setattr__(name, _datetime)
elif name == 'data':
self._prepare_data(value)
else:
@ -278,7 +282,7 @@ class MISPAttribute(AbstractMISP):
if not hasattr(self, 'timestamp'):
self.timestamp = datetime.timestamp(datetime.now())
def _to_feed(self) -> dict:
def _to_feed(self) -> Dict:
to_return = super()._to_feed()
if self.data:
to_return['data'] = base64.b64encode(self.data.getvalue()).decode()
@ -292,7 +296,7 @@ class MISPAttribute(AbstractMISP):
return self.describe_types['types']
@property
def malware_binary(self) -> Union[BytesIO, None]:
def malware_binary(self) -> Optional[BytesIO]:
"""Returns a BytesIO of the malware (if the attribute has one, obvs)."""
if hasattr(self, '_malware_binary'):
return self._malware_binary
@ -330,7 +334,7 @@ class MISPAttribute(AbstractMISP):
"""Alias for add_shadow_attribute"""
return self.add_shadow_attribute(shadow_attribute, **kwargs)
def add_shadow_attribute(self, shadow_attribute: Union[MISPShadowAttribute, dict, None]=None, **kwargs) -> MISPShadowAttribute:
def add_shadow_attribute(self, shadow_attribute: Optional[Union[MISPShadowAttribute, Dict]]=None, **kwargs) -> MISPShadowAttribute:
"""Add a shadow attribute to the attribute (by name or a MISPShadowAttribute object)"""
if isinstance(shadow_attribute, MISPShadowAttribute):
misp_shadow_attribute = shadow_attribute
@ -346,7 +350,7 @@ class MISPAttribute(AbstractMISP):
self.edited = True
return misp_shadow_attribute
def add_sighting(self, sighting: Union[MISPSighting, dict, None]=None, **kwargs) -> MISPSighting:
def add_sighting(self, sighting: Optional[Union[MISPSighting, dict]]=None, **kwargs) -> MISPSighting:
"""Add a sighting to the attribute (by name or a MISPSighting object)"""
if isinstance(sighting, MISPSighting):
misp_sighting = sighting
@ -488,7 +492,7 @@ class MISPAttribute(AbstractMISP):
super().from_dict(**kwargs)
def to_dict(self) -> dict:
def to_dict(self) -> Dict:
to_return = super().to_dict()
if self.data:
to_return['data'] = base64.b64encode(self.data.getvalue()).decode()
@ -623,7 +627,7 @@ class MISPObject(AbstractMISP):
self.last_seen: datetime
self.__fast_attribute_access: dict = defaultdict(list) # Hashtable object_relation: [attributes]
self.ObjectReference: List[MISPObjectReference] = []
self.Attribute: List[MISPAttribute] = []
self.Attribute: List[MISPObjectAttribute] = []
self.SharingGroup: MISPSharingGroup
self._default_attributes_parameters: dict
if isinstance(default_attributes_parameters, MISPAttribute):
@ -645,8 +649,8 @@ class MISPObject(AbstractMISP):
self._default_attributes_parameters.pop('data', None) # in case the original in a sample or an attachment
# Those values are set for the current object, if they exist, but not pop'd because they are still useful for the attributes
self.distribution = self._default_attributes_parameters.get('distribution', 5)
self.sharing_group_id = self._default_attributes_parameters.get('sharing_group_id', 0)
self.distribution: int = self._default_attributes_parameters.get('distribution', 5)
self.sharing_group_id: int = self._default_attributes_parameters.get('sharing_group_id', 0)
else:
self.distribution = 5 # Default to inherit
self.sharing_group_id = 0
@ -656,7 +660,7 @@ class MISPObject(AbstractMISP):
self.update_not_jsonable('ObjectReference')
def _load_template_path(self, template_path: Union[Path, str]) -> bool:
self._definition: Union[dict, None] = self._load_json(template_path)
self._definition: Optional[Dict] = self._load_json(template_path)
if not self._definition:
return False
setattr(self, 'meta-category', self._definition['meta-category'])
@ -671,7 +675,7 @@ class MISPObject(AbstractMISP):
if not hasattr(self, 'timestamp'):
self.timestamp = datetime.timestamp(datetime.now())
def _to_feed(self) -> dict:
def _to_feed(self) -> Dict:
to_return = super(MISPObject, self)._to_feed()
if self.references:
to_return['ObjectReference'] = [reference._to_feed() for reference in self.references]
@ -714,11 +718,11 @@ class MISPObject(AbstractMISP):
self._strict = False
@property
def attributes(self) -> List[MISPAttribute]:
def attributes(self) -> List['MISPObjectAttribute']:
return self.Attribute
@attributes.setter
def attributes(self, attributes: List[MISPAttribute]):
def attributes(self, attributes: List['MISPObjectAttribute']):
if all(isinstance(x, MISPObjectAttribute) for x in attributes):
self.Attribute = attributes
self.__fast_attribute_access = defaultdict(list)
@ -826,17 +830,17 @@ class MISPObject(AbstractMISP):
return self._fast_attribute_access.get(object_relation, [])
@property
def _fast_attribute_access(self):
def _fast_attribute_access(self) -> Dict:
if not self.__fast_attribute_access:
for a in self.attributes:
self.__fast_attribute_access[a.object_relation].append(a)
return self.__fast_attribute_access
def has_attributes_by_relation(self, list_of_relations: List[str]):
def has_attributes_by_relation(self, list_of_relations: List[str]) -> bool:
'''True if all the relations in the list are defined in the object'''
return all(relation in self._fast_attribute_access for relation in list_of_relations)
def add_attribute(self, object_relation: str, simple_value: Union[str, int, float]=None, **value) -> Union[MISPAttribute, None]:
def add_attribute(self, object_relation: str, simple_value: Optional[Union[str, int, float]]=None, **value) -> Optional[MISPAttribute]:
"""Add an attribute. object_relation is required and the value key is a
dictionary with all the keys supported by MISPAttribute"""
if simple_value is not None: # /!\ The value *can* be 0
@ -876,7 +880,7 @@ class MISPObject(AbstractMISP):
to_return.append(a)
return to_return
def to_dict(self, strict: bool=False) -> dict:
def to_dict(self, strict: bool=False) -> Dict:
if strict or self._strict and self._known_template:
self._validate()
return super(MISPObject, self).to_dict()
@ -886,10 +890,12 @@ class MISPObject(AbstractMISP):
self._validate()
return super(MISPObject, self).to_json(sort_keys=sort_keys, indent=indent)
def _validate(self):
def _validate(self) -> bool:
if not self._definition:
raise PyMISPError('No object definition available, unable to validate.')
"""Make sure the object we're creating has the required fields"""
if self._definition.get('required'):
required_missing = set(self._definition.get('required')) - set(self._fast_attribute_access.keys())
required_missing = set(self._definition['required']) - set(self._fast_attribute_access.keys())
if required_missing:
raise InvalidMISPObject('{} are required.'.format(required_missing))
if self._definition.get('requiredOneOf'):
@ -916,7 +922,7 @@ class MISPEvent(AbstractMISP):
_fields_for_feed: set = {'uuid', 'info', 'threat_level_id', 'analysis', 'timestamp',
'publish_timestamp', 'published', 'date', 'extends_uuid'}
def __init__(self, describe_types: dict=None, strict_validation: bool=False, **kwargs):
def __init__(self, describe_types: Optional[Dict]=None, strict_validation: bool=False, **kwargs):
super().__init__(**kwargs)
if strict_validation:
schema_file = 'schema.json'
@ -971,7 +977,7 @@ class MISPEvent(AbstractMISP):
self.threat_level_id = 4
@property
def manifest(self) -> dict:
def manifest(self) -> Dict:
required = ['info', 'Orgc']
for r in required:
if not hasattr(self, r):
@ -1000,7 +1006,7 @@ class MISPEvent(AbstractMISP):
to_return += attribute.hash_values(algorithm)
return to_return
def to_feed(self, valid_distributions: List[int]=[0, 1, 2, 3, 4, 5], with_meta: bool=False) -> dict:
def to_feed(self, valid_distributions: List[int]=[0, 1, 2, 3, 4, 5], with_meta: bool=False) -> Dict:
""" Generate a json output for MISP Feed.
Notes:
* valid_distributions only makes sense if the distribution key is set (i.e. the event is exported from a MISP instance)
@ -1090,7 +1096,7 @@ class MISPEvent(AbstractMISP):
raise PyMISPError('All the attributes have to be of type MISPShadowAttribute.')
@property
def related_events(self): # -> List[MISPEvent]:
def related_events(self) -> List['MISPEvent']:
return self.RelatedEvent
@property
@ -1233,7 +1239,7 @@ class MISPEvent(AbstractMISP):
self.SharingGroup.from_dict(**kwargs.pop('SharingGroup'))
super(MISPEvent, self).from_dict(**kwargs)
def to_dict(self) -> dict:
def to_dict(self) -> Dict:
to_return = super().to_dict()
if to_return.get('date'):
@ -1652,3 +1658,18 @@ class MISPUserSetting(AbstractMISP):
def __repr__(self):
return f'<{self.__class__.__name__}(name={self.setting}'
class MISPInbox(AbstractMISP):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.data: Dict
def from_dict(self, **kwargs):
if 'Inbox' in kwargs:
kwargs = kwargs['Inbox']
super().from_dict(**kwargs)
def __repr__(self):
return f'<{self.__class__.__name__}(name={self.type})>'

View File

@ -26,7 +26,7 @@ logger = logging.getLogger('pymisp')
try:
from pymisp import ExpandedPyMISP, MISPEvent, MISPOrganisation, MISPUser, Distribution, ThreatLevel, Analysis, MISPObject, MISPAttribute, MISPSighting, MISPShadowAttribute, MISPTag, MISPSharingGroup, MISPFeed, MISPServer, MISPUserSetting
from pymisp import register_user, PyMISP, MISPEvent, MISPOrganisation, MISPUser, Distribution, ThreatLevel, Analysis, MISPObject, MISPAttribute, MISPSighting, MISPShadowAttribute, MISPTag, MISPSharingGroup, MISPFeed, MISPServer, MISPUserSetting
from pymisp.tools import CSVLoader, DomainIPObject, ASNObject, GenericObjectGenerator
from pymisp.exceptions import MISPServerError
except ImportError:
@ -57,7 +57,8 @@ class TestComprehensive(unittest.TestCase):
def setUpClass(cls):
cls.maxDiff = None
# Connect as admin
cls.admin_misp_connector = ExpandedPyMISP(url, key, verifycert, debug=False)
cls.admin_misp_connector = PyMISP(url, key, verifycert, debug=False)
cls.admin_misp_connector.set_server_setting('Security.allow_self_registration', True, force=True)
if not fast_mode:
r = cls.admin_misp_connector.update_misp()
print(r)
@ -76,7 +77,7 @@ class TestComprehensive(unittest.TestCase):
user.email = 'testusr@user.local'
user.org_id = cls.test_org.id
cls.test_usr = cls.admin_misp_connector.add_user(user, pythonify=True)
cls.user_misp_connector = ExpandedPyMISP(url, cls.test_usr.authkey, verifycert, debug=True)
cls.user_misp_connector = PyMISP(url, cls.test_usr.authkey, verifycert, debug=True)
cls.user_misp_connector.toggle_global_pythonify()
# Creates a publisher
user = MISPUser()
@ -84,14 +85,14 @@ class TestComprehensive(unittest.TestCase):
user.org_id = cls.test_org.id
user.role_id = 4
cls.test_pub = cls.admin_misp_connector.add_user(user, pythonify=True)
cls.pub_misp_connector = ExpandedPyMISP(url, cls.test_pub.authkey, verifycert)
cls.pub_misp_connector = PyMISP(url, cls.test_pub.authkey, verifycert)
# Creates a user that can accept a delegation request
user = MISPUser()
user.email = 'testusr@delegate.recipient.local'
user.org_id = cls.test_org_delegate.id
user.role_id = 2
cls.test_usr_delegate = cls.admin_misp_connector.add_user(user, pythonify=True)
cls.delegate_user_misp_connector = ExpandedPyMISP(url, cls.test_usr_delegate.authkey, verifycert, debug=False)
cls.delegate_user_misp_connector = PyMISP(url, cls.test_usr_delegate.authkey, verifycert, debug=False)
cls.delegate_user_misp_connector.toggle_global_pythonify()
if not fast_mode:
# Update all json stuff
@ -1907,7 +1908,7 @@ class TestComprehensive(unittest.TestCase):
try:
test_roles_user = self.admin_misp_connector.add_user(user, pythonify=True)
test_tag = self.admin_misp_connector.add_tag(tag, pythonify=True)
test_roles_user_connector = ExpandedPyMISP(url, test_roles_user.authkey, verifycert, debug=False)
test_roles_user_connector = PyMISP(url, test_roles_user.authkey, verifycert, debug=False)
test_roles_user_connector.toggle_global_pythonify()
# ===== Read Only
self.admin_misp_connector.update_user({'role_id': 6}, test_roles_user)
@ -2150,6 +2151,36 @@ class TestComprehensive(unittest.TestCase):
finally:
self.admin_misp_connector.delete_event(first)
def test_registrations(self):
r = register_user(url, 'self_register@user.local', organisation=self.test_org,
org_name=self.test_org.name, verify=verifycert)
self.assertTrue(r['saved'])
r = register_user(url, 'discard@tesst.de', verify=verifycert)
self.assertTrue(r['saved'])
registrations = self.admin_misp_connector.user_registrations(pythonify=True)
self.assertTrue(len(registrations), 2)
self.assertEqual(registrations[0].data['email'], 'self_register@user.local')
self.assertEqual(registrations[0].data['org_name'], 'Test Org')
self.assertEqual(registrations[1].data['email'], 'discard@tesst.de')
m = self.admin_misp_connector.accept_user_registration(registrations[0], unsafe_fallback=True)
self.assertTrue(m['saved'])
# delete new user
for user in self.admin_misp_connector.users(pythonify=True):
if user.email == registrations[0].data['email']:
self.admin_misp_connector.delete_user(user)
break
# Expected: accept registration fails because the orgname is missing
m = self.admin_misp_connector.accept_user_registration(registrations[1], unsafe_fallback=True)
self.assertEqual(m['errors'][1]['message'], 'No organisation selected. Supply an Organisation ID')
m = self.admin_misp_connector.discard_user_registration(registrations[1].id)
self.assertEqual(m['name'], '1 registration(s) discarded.')
if __name__ == '__main__':
unittest.main()