fix: Exception when posting multiple attributes on attributes/add

Fix #433

Few cleanups in code.
pull/434/head
Raphaël Vinot 2019-08-06 14:14:28 +02:00
parent 549e3a5a84
commit e993886dd7
2 changed files with 91 additions and 37 deletions

View File

@ -347,20 +347,38 @@ class ExpandedPyMISP(PyMISP):
return a return a
def add_attribute(self, event: Union[MISPEvent, int, str, UUID], attribute: MISPAttribute, pythonify: bool=False): def add_attribute(self, event: Union[MISPEvent, int, str, UUID], attribute: MISPAttribute, pythonify: bool=False):
'''Add an attribute to an existing MISP event''' '''Add an attribute to an existing MISP event
NOTE MISP 2.4.113+: you can pass a list of attributes.
In that case, the pythonified response is the following: {'attributes': [MISPAttribute], 'errors': {errors by attributes}}'''
event_id = self.__get_uuid_or_id_from_abstract_misp(event) event_id = self.__get_uuid_or_id_from_abstract_misp(event)
new_attribute = self._prepare_request('POST', f'attributes/add/{event_id}', data=attribute) new_attribute = self._prepare_request('POST', f'attributes/add/{event_id}', data=attribute)
new_attribute = self._check_response(new_attribute, expect_json=True) new_attribute = self._check_response(new_attribute, expect_json=True)
if ('errors' in new_attribute and new_attribute['errors'][0] == 403 if isinstance(attribute, list):
and new_attribute['errors'][1]['message'] == 'You do not have permission to do that.'): # Multiple attributes were passed at once, the handling is totally different
# At this point, we assume the user tried to add an attribute on an event they don't own if self._old_misp((2, 4, 113), '2020-01-01', sys._getframe().f_code.co_name):
# Re-try with a proposal return new_attribute
return self.add_attribute_proposal(event_id, attribute, pythonify) if not (self.global_pythonify or pythonify):
if not (self.global_pythonify or pythonify) or 'errors' in new_attribute: return new_attribute
return new_attribute to_return = {'attributes': []}
a = MISPAttribute() if 'errors' in new_attribute:
a.from_dict(**new_attribute) to_return['errors'] = new_attribute['errors']
return a
for attribute in new_attribute['Attribute']:
a = MISPAttribute()
a.from_dict(**attribute)
to_return['attributes'].append(a)
return to_return
else:
if ('errors' in new_attribute and new_attribute['errors'][0] == 403
and new_attribute['errors'][1]['message'] == 'You do not have permission to do that.'):
# At this point, we assume the user tried to add an attribute on an event they don't own
# Re-try with a proposal
return self.add_attribute_proposal(event_id, attribute, pythonify)
if not (self.global_pythonify or pythonify) or 'errors' in new_attribute:
return new_attribute
a = MISPAttribute()
a.from_dict(**new_attribute)
return a
def update_attribute(self, attribute: MISPAttribute, attribute_id: int=None, pythonify: bool=False): def update_attribute(self, attribute: MISPAttribute, attribute_id: int=None, pythonify: bool=False):
'''Update an attribute on a MISP instance''' '''Update an attribute on a MISP instance'''
@ -1402,9 +1420,11 @@ class ExpandedPyMISP(PyMISP):
if return_format == 'csv' and (self.global_pythonify or pythonify) and not headerless: if return_format == 'csv' and (self.global_pythonify or pythonify) and not headerless:
return self._csv_to_dict(normalized_response) return self._csv_to_dict(normalized_response)
elif 'errors' in normalized_response:
if 'errors' in normalized_response:
return normalized_response return normalized_response
elif return_format == 'json' and self.global_pythonify or pythonify:
if return_format == 'json' and self.global_pythonify or pythonify:
# The response is in json, we can convert it to a list of pythonic MISP objects # The response is in json, we can convert it to a list of pythonic MISP objects
to_return = [] to_return = []
if controller == 'events': if controller == 'events':
@ -1443,8 +1463,8 @@ class ExpandedPyMISP(PyMISP):
elif controller == 'objects': elif controller == 'objects':
raise PyMISPNotImplementedYet('Not implemented yet') raise PyMISPNotImplementedYet('Not implemented yet')
return to_return return to_return
else:
return normalized_response return normalized_response
def search_index(self, published: Optional[bool]=None, eventid: Optional[SearchType]=None, def search_index(self, published: Optional[bool]=None, eventid: Optional[SearchType]=None,
tags: Optional[SearchParameterTypes]=None, tags: Optional[SearchParameterTypes]=None,
@ -1559,7 +1579,8 @@ class ExpandedPyMISP(PyMISP):
normalized_response = self._check_response(response, expect_json=True) normalized_response = self._check_response(response, expect_json=True)
if not (self.global_pythonify or pythonify) or 'errors' in normalized_response: if not (self.global_pythonify or pythonify) or 'errors' in normalized_response:
return normalized_response return normalized_response
elif self.global_pythonify or pythonify:
if self.global_pythonify or pythonify:
to_return = [] to_return = []
for s in normalized_response: for s in normalized_response:
entries = {} entries = {}
@ -1579,8 +1600,7 @@ class ExpandedPyMISP(PyMISP):
entries['sighting'] = ms entries['sighting'] = ms
to_return.append(entries) to_return.append(entries)
return to_return return to_return
else: return normalized_response
return normalized_response
def search_logs(self, limit: Optional[int]=None, page: Optional[int]=None, def search_logs(self, limit: Optional[int]=None, page: Optional[int]=None,
log_id: Optional[int]=None, title: Optional[str]=None, log_id: Optional[int]=None, title: Optional[str]=None,
@ -1743,12 +1763,14 @@ class ExpandedPyMISP(PyMISP):
misp_entity.sharing_group_id = sharing_group_id # Set new sharing group id misp_entity.sharing_group_id = sharing_group_id # Set new sharing group id
if isinstance(misp_entity, MISPEvent): if isinstance(misp_entity, MISPEvent):
return self.update_event(misp_entity, pythonify=pythonify) return self.update_event(misp_entity, pythonify=pythonify)
elif isinstance(misp_entity, MISPObject):
if isinstance(misp_entity, MISPObject):
return self.update_object(misp_entity, pythonify=pythonify) return self.update_object(misp_entity, pythonify=pythonify)
elif isinstance(misp_entity, MISPAttribute):
if isinstance(misp_entity, MISPAttribute):
return self.update_attribute(misp_entity, pythonify=pythonify) return self.update_attribute(misp_entity, pythonify=pythonify)
else:
raise PyMISPError('The misp_entity must be MISPEvent, MISPObject or MISPAttribute') raise PyMISPError('The misp_entity must be MISPEvent, MISPObject or MISPAttribute')
def tag(self, misp_entity: Union[AbstractMISP, str], tag: str): def tag(self, misp_entity: Union[AbstractMISP, str], tag: str):
"""Tag an event or an attribute. misp_entity can be a UUID""" """Tag an event or an attribute. misp_entity can be a UUID"""
@ -1792,7 +1814,7 @@ class ExpandedPyMISP(PyMISP):
return str(obj) return str(obj)
if isinstance(obj, (int, str)): if isinstance(obj, (int, str)):
return obj return obj
elif 'id' in obj: if 'id' in obj:
return obj['id'] return obj['id']
return obj['uuid'] return obj['uuid']
@ -1806,27 +1828,28 @@ class ExpandedPyMISP(PyMISP):
'''Catch-all method to normalize anything that can be converted to a timestamp''' '''Catch-all method to normalize anything that can be converted to a timestamp'''
if isinstance(value, datetime): if isinstance(value, datetime):
return value.timestamp() return value.timestamp()
elif isinstance(value, date):
if isinstance(value, date):
return datetime.combine(value, datetime.max.time()).timestamp() return datetime.combine(value, datetime.max.time()).timestamp()
elif isinstance(value, str):
if isinstance(value, str):
if value.isdigit(): if value.isdigit():
return value return value
else: try:
try: float(value)
float(value) return value
return value except ValueError:
except ValueError: # The value can also be '1d', '10h', ...
# The value can also be '1d', '10h', ... return value
return value return value
else:
return value
def _check_response(self, response, lenient_response_type=False, expect_json=False): def _check_response(self, response, lenient_response_type=False, expect_json=False):
"""Check if the response from the server is not an unexpected error""" """Check if the response from the server is not an unexpected error"""
if response.status_code >= 500: if response.status_code >= 500:
logger.critical(everything_broken.format(response.request.headers, response.request.body, response.text)) logger.critical(everything_broken.format(response.request.headers, response.request.body, response.text))
raise MISPServerError(f'Error code 500:\n{response.text}') raise MISPServerError(f'Error code 500:\n{response.text}')
elif 400 <= response.status_code < 500:
if 400 <= response.status_code < 500:
# The server returns a json message with the error details # The server returns a json message with the error details
error_message = response.json() error_message = response.json()
logger.error(f'Something went wrong ({response.status_code}): {error_message}') logger.error(f'Something went wrong ({response.status_code}): {error_message}')
@ -1849,7 +1872,7 @@ class ExpandedPyMISP(PyMISP):
raise PyMISPUnexpectedResponse(f'Unexpected response from server: {response.text}') raise PyMISPUnexpectedResponse(f'Unexpected response from server: {response.text}')
if lenient_response_type and not response.headers.get('content-type').startswith('application/json'): if lenient_response_type and not response.headers.get('content-type').startswith('application/json'):
return response.text return response.text
if not len(response.content): if not response.content:
# Empty response # Empty response
logger.error('Got an empty response.') logger.error('Got an empty response.')
return {'errors': 'The response is empty.'} return {'errors': 'The response is empty.'}

View File

@ -1327,6 +1327,37 @@ class TestComprehensive(unittest.TestCase):
new_attribute.type = 'ip-dst' new_attribute.type = 'ip-dst'
new_attribute = self.user_misp_connector.add_attribute(first.id, new_attribute) new_attribute = self.user_misp_connector.add_attribute(first.id, new_attribute)
self.assertEqual(new_attribute.value, '1.2.3.4') self.assertEqual(new_attribute.value, '1.2.3.4')
# Test attribute already in event
# new_attribute.uuid = str(uuid4())
# new_attribute = self.user_misp_connector.add_attribute(first.id, new_attribute)
new_similar = MISPAttribute()
new_similar.value = '1.2.3.4'
new_similar.type = 'ip-dst'
similar_error = self.user_misp_connector.add_attribute(first.id, new_similar)
self.assertEqual(similar_error['errors'][1]['errors']['value'][0], 'A similar attribute already exists for this event.')
# Test add multiple attributes at once
attr1 = MISPAttribute()
attr1.value = '1.2.3.4'
attr1.type = 'ip-dst'
attr2 = MISPAttribute()
attr2.value = '1.2.3.5'
attr2.type = 'ip-dst'
attr3 = MISPAttribute()
attr3.value = first.attributes[0].value
attr3.type = first.attributes[0].type
attr4 = MISPAttribute()
attr4.value = '1.2.3.6'
attr4.type = 'ip-dst'
attr4.add_tag('tlp:amber___test')
response = self.user_misp_connector.add_attribute(first.id, [attr1, attr2, attr3, attr4])
# FIXME: https://github.com/MISP/MISP/issues/4959
# self.assertEqual(response['attributes'][0].value, '1.2.3.5')
# self.assertEqual(response['attributes'][1].value, '1.2.3.6')
# self.assertEqual(response['attributes'][1].tags[0].name, 'tlp:amber___test')
# self.assertEqual(response['errors']['attribute_0']['value'][0], 'A similar attribute already exists for this event.')
# self.assertEqual(response['errors']['attribute_2']['value'][0], 'A similar attribute already exists for this event.')
# Add attribute as proposal # Add attribute as proposal
new_proposal = MISPAttribute() new_proposal = MISPAttribute()
new_proposal.value = '5.2.3.4' new_proposal.value = '5.2.3.4'
@ -1406,7 +1437,7 @@ class TestComprehensive(unittest.TestCase):
# Test attribute*S* # Test attribute*S*
attributes = self.admin_misp_connector.attributes() attributes = self.admin_misp_connector.attributes()
self.assertEqual(len(attributes), 5) self.assertEqual(len(attributes), 7)
# attributes = self.user_misp_connector.attributes() # attributes = self.user_misp_connector.attributes()
# self.assertEqual(len(attributes), 5) # self.assertEqual(len(attributes), 5)
# Test event*S* # Test event*S*