diff --git a/cybox/common/vocabs.py b/cybox/common/vocabs.py index 108949c7..5dacaf5f 100644 --- a/cybox/common/vocabs.py +++ b/cybox/common/vocabs.py @@ -6,11 +6,13 @@ from cybox.common import PatternFieldGroup from cybox.utils import normalize_to_xml, denormalize_from_xml +INVALID_XSI_TYPE = "bad_xsi_type" + class VocabString(PatternFieldGroup, cybox.Entity): _namespace = 'http://cybox.mitre.org/default_vocabularies-2' # All subclasses should override this - _XSI_TYPE = "BAD_XSI_TYPE" + _XSI_TYPE = INVALID_XSI_TYPE def __init__(self, value=None): super(VocabString, self).__init__() @@ -33,20 +35,33 @@ def __eq__(self, other): def is_plain(self): """Whether the VocabString can be represented as a single value. - If `xsi:type` and `value` are the only non-None properties, the - VocabString can be represented by a single value rather than a - dictionary. This makes the JSON representation simpler without losing - any data fidelity. + If `value` is the only non-None properties and a custom XSI type has + not been set, the VocabString can be represented by a single value + rather than a dictionary. This makes the JSON representation simpler + without losing any data fidelity. """ return ( - # ignore value and xsi_type + # ignore value self.vocab_name is None and self.vocab_reference is None and + self.xsi_type == self._XSI_TYPE and PatternFieldGroup.is_plain(self) ) + def is_valid(self): + """For a vocab string to be valid, it must have an xsi:type, a + vocab_name, or a vocab_reference.""" + return ( + self.xsi_type != INVALID_XSI_TYPE or + self.vocab_name is not None or + self.vocab_reference is not None + ) + def to_obj(self): + if not self.is_valid(): + raise ValueError("Vocab being used has not been specified") + vocab_obj = common_binding.ControlledVocabularyStringType() vocab_obj.set_valueOf_(normalize_to_xml(self.value, self.delimiter)) @@ -62,15 +77,17 @@ def to_obj(self): return vocab_obj def to_dict(self): + if not self.is_valid(): + raise ValueError("Vocab being used has not been specified") + if self.is_plain(): return self.value vocab_dict = {} if self.value is not None: vocab_dict['value'] = self.value - if self.xsi_type is not None: + if self.xsi_type != self._XSI_TYPE: vocab_dict['xsi:type'] = self.xsi_type - if self.vocab_name is not None: vocab_dict['vocab_name'] = self.vocab_name if self.vocab_reference is not None: @@ -113,7 +130,7 @@ def from_dict(cls, vocab_dict): if not isinstance(vocab_dict, dict): vocab_str.value = vocab_dict else: - vocab_str.xsi_type = vocab_dict.get('xsi:type') + vocab_str.xsi_type = vocab_dict.get('xsi:type', cls._XSI_TYPE) vocab_str.value = vocab_dict.get('value') vocab_str.vocab_name = vocab_dict.get('vocab_name') vocab_str.vocab_reference = vocab_dict.get('vocab_reference') diff --git a/cybox/core/action.py b/cybox/core/action.py index 428b0a52..afa757db 100644 --- a/cybox/core/action.py +++ b/cybox/core/action.py @@ -51,6 +51,7 @@ class AssociatedObjects(cybox.EntityList): class ActionRelationship(cybox.Entity): + _binding = core_binding _namespace = 'http://cybox.mitre.org/cybox-2' def __init__(self): @@ -79,8 +80,8 @@ def from_dict(action_relationship_dict): if not action_relationship_dict: return None action_relationship_ = ActionRelationship() - action_relationship_.type = VocabString.from_dict(action_relationship_dict.get('type')) - action_relationship_.action_references = [ActionReference.from_dict(x) for x in action_relationship_dict.get('action_reference')] + action_relationship_.type = ActionType.from_dict(action_relationship_dict.get('type')) + action_relationship_.action_references = [ActionReference.from_dict(x) for x in action_relationship_dict.get('action_reference', [])] return action_relationship_ @staticmethod @@ -88,7 +89,7 @@ def from_obj(action_relationship_obj): if not action_relationship_obj: return None action_relationship_ = ActionRelationship() - action_relationship_.type = VocabString.from_obj(action_relationship_obj.get_Type()) + action_relationship_.type = ActionType.from_obj(action_relationship_obj.get_Type()) action_relationship_.action_references = [ActionReference.from_obj(x) for x in action_relationship_obj.get_Action_Reference()] return action_relationship_ diff --git a/cybox/core/object.py b/cybox/core/object.py index f10a9f78..b7ae58d6 100644 --- a/cybox/core/object.py +++ b/cybox/core/object.py @@ -164,7 +164,7 @@ def from_dict(object_dict, obj_class=None): class Relationship(VocabString): - _XSI_TYPE = 'cyboxVocabs:ObjectRelationshipVocab-1.0' + _XSI_TYPE = 'cyboxVocabs:ObjectRelationshipVocab-1.1' class RelatedObject(Object): diff --git a/cybox/test/common/vocab_test.py b/cybox/test/common/vocab_test.py index e9b16125..09397bed 100644 --- a/cybox/test/common/vocab_test.py +++ b/cybox/test/common/vocab_test.py @@ -3,7 +3,7 @@ import unittest -from cybox.common import VocabString +from cybox.common import HashName, VocabString import cybox.test from cybox.utils import normalize_to_xml @@ -13,6 +13,16 @@ class TestVocabString(unittest.TestCase): def test_plain(self): a = VocabString("test_value") self.assertTrue(a.is_plain()) + self.assertFalse(a.is_valid()) + + def test_is_plain_hashname(self): + md5 = HashName("MD5") + self.assertTrue(md5.is_plain()) + + def test_is_plain_hashname(self): + md5 = HashName("MD5") + md5.xsi_type = "Some Other xsi:type" + self.assertFalse(md5.is_plain()) def test_round_trip(self): vocab_dict = { @@ -52,6 +62,7 @@ def test_round_trip_list(self): 'value': ['Value1', 'Value2', 'Value3'], 'condition': "Equals", 'apply_condition': "ALL", + 'vocab_name': "Test", } vocab_dict2 = cybox.test.round_trip_dict(VocabString, vocab_dict) diff --git a/cybox/test/core/action_test.py b/cybox/test/core/action_test.py index fdf36d46..2a6b3615 100644 --- a/cybox/test/core/action_test.py +++ b/cybox/test/core/action_test.py @@ -3,8 +3,8 @@ import unittest -from cybox.core import Action -from cybox.test import EntityTestCase +from cybox.core import Action, ActionRelationship, ActionType +from cybox.test import EntityTestCase, round_trip class TestAction(EntityTestCase, unittest.TestCase): @@ -47,7 +47,27 @@ class TestAction(EntityTestCase, unittest.TestCase): } -#TODO: Test AssociateObjects and ActionRelationships +class TestActionRelationship(EntityTestCase, unittest.TestCase): + klass = ActionRelationship + + _full_dict = { + 'type': u"Add", + 'action_reference': [ + {'action_id': "example:Action-1"}, + {'action_id': "example:Action-3"}, + ] + } + + def test_nonstandard_type_vocab(self): + ar = ActionRelationship() + ar.type = ActionType(u"AddedMultipleTimes") + ar.type.vocab_reference = "http://example.com/action-types/" + ar.type.xsi_type = None + ar2 = round_trip(ar) + self.assertEqual(ar.to_dict(), ar2.to_dict()) + + +#TODO: Test AssociateObjects if __name__ == "__main__": diff --git a/cybox/test/core/object_test.py b/cybox/test/core/object_test.py index a2ae46b3..9ef3f47d 100644 --- a/cybox/test/core/object_test.py +++ b/cybox/test/core/object_test.py @@ -7,7 +7,7 @@ from cybox.objects.address_object import Address from cybox.objects.email_message_object import EmailMessage from cybox.objects.uri_object import URI -from cybox.test import EntityTestCase, round_trip +from cybox.test import EntityTestCase, round_trip, round_trip_dict from cybox.utils import CacheMiss, set_id_method @@ -120,6 +120,34 @@ def test_missing_related_object(self): rel_obj = o2.observables[0].object_.related_objects[0] self.assertRaises(CacheMiss, rel_obj.get_properties) + def test_relationship_standard_xsitype(self): + d = { + 'id': "example:Object-1", + 'relationship': u"Created", + } + self._test_round_trip_dict(d) + + def test_relationship_nonstandard_xsitype(self): + d = { + 'id': "example:Object-1", + 'relationship': { + 'value': u"Created", + 'xsi:type': "Foo", + } + } + self._test_round_trip_dict(d) + + def test_relationship_vocabnameref(self): + d = { + 'id': "example:Object-1", + 'relationship': { + 'value': u"Created", + 'vocab_name': "Foo", + 'vocab_reference': "http://example.com/FooVocab", + } + } + self._test_round_trip_dict(d) + def _test_round_trip(self, observables): self.maxDiff = None print observables.to_xml() @@ -138,5 +166,11 @@ def _test_returned_objects(self, o2): # Check that the properties are identical self.assertEqual(self.ip.to_dict(), ip_obj.get_properties().to_dict()) + def _test_round_trip_dict(self, d): + d2 = round_trip_dict(RelatedObject, d) + self.maxDiff = None + self.assertEqual(d, d2) + + if __name__ == "__main__": unittest.main()