From 6fa3c84aa3073de13fb64ab02b324fae2837ec43 Mon Sep 17 00:00:00 2001 From: clenk Date: Thu, 24 Aug 2017 15:46:36 -0400 Subject: [PATCH] Improve tests and coverage for custom content In ObservableProperty.clean(), parse_observable() will handle the issue of non-cyber observable objects being presnt in an observable property. The line raising a ValueError would not be reached so it was removed. --- stix2/observables.py | 17 +++--- stix2/test/test_custom.py | 111 +++++++++++++++++++++++++++++++++----- 2 files changed, 106 insertions(+), 22 deletions(-) diff --git a/stix2/observables.py b/stix2/observables.py index 3c033bd..50eb6d1 100644 --- a/stix2/observables.py +++ b/stix2/observables.py @@ -31,9 +31,6 @@ class ObservableProperty(Property): # from .__init__ import parse_observable # avoid circular import for key, obj in dictified.items(): parsed_obj = parse_observable(obj, valid_refs) - if not issubclass(type(parsed_obj), _Observable): - raise ValueError("Objects in an observable property must be " - "Cyber Observable Objects") dictified[key] = parsed_obj return dictified @@ -734,16 +731,17 @@ def parse_observable(data, _valid_refs=[], allow_custom=False): obj['_valid_refs'] = _valid_refs if 'type' not in obj: - raise ParseError("Can't parse object with no 'type' property: %s" % str(obj)) + raise ParseError("Can't parse observable with no 'type' property: %s" % str(obj)) try: obj_class = OBJ_MAP_OBSERVABLE[obj['type']] except KeyError: - raise ParseError("Can't parse unknown object type '%s'! For custom observables, use the CustomObservable decorator." % obj['type']) + raise ParseError("Can't parse unknown observable type '%s'! For custom observables, " + "use the CustomObservable decorator." % obj['type']) if 'extensions' in obj and obj['type'] in EXT_MAP: for name, ext in obj['extensions'].items(): if name not in EXT_MAP[obj['type']]: - raise ParseError("Can't parse Unknown extension type '%s' for object type '%s'!" % (name, obj['type'])) + raise ParseError("Can't parse Unknown extension type '%s' for observable type '%s'!" % (name, obj['type'])) ext_class = EXT_MAP[obj['type']][name] obj['extensions'][name] = ext_class(allow_custom=allow_custom, **obj['extensions'][name]) @@ -787,13 +785,16 @@ def _register_extension(observable, new_extension): try: observable_type = observable._type except AttributeError: - raise ValueError("Custom observables must be created with the @CustomObservable decorator.") + raise ValueError("Unknown observable type. Custom observables must be " + "created with the @CustomObservable decorator.") try: EXT_MAP[observable_type][new_extension._type] = new_extension except KeyError: if observable_type not in OBJ_MAP_OBSERVABLE: - raise ValueError("Unknown observable type '%s'" % observable_type) + raise ValueError("Unknown observable type '%s'. Custom observables " + "must be created with the @CustomObservable decorator." + % observable_type) else: EXT_MAP[observable_type] = {new_extension._type: new_extension} diff --git a/stix2/test/test_custom.py b/stix2/test/test_custom.py index 804776a..33d9287 100644 --- a/stix2/test/test_custom.py +++ b/stix2/test/test_custom.py @@ -6,7 +6,7 @@ from .constants import FAKE_TIME def test_identity_custom_property(): - with pytest.raises(ValueError): + with pytest.raises(ValueError) as excinfo: stix2.Identity( id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", created="2015-12-21T19:59:11Z", @@ -15,6 +15,7 @@ def test_identity_custom_property(): identity_class="individual", custom_properties="foobar", ) + assert str(excinfo.value) == "'custom_properties' must be a dictionary" identity = stix2.Identity( id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", @@ -31,7 +32,7 @@ def test_identity_custom_property(): def test_identity_custom_property_invalid(): - with pytest.raises(stix2.exceptions.ExtraPropertiesError): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: stix2.Identity( id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", created="2015-12-21T19:59:11Z", @@ -40,6 +41,9 @@ def test_identity_custom_property_invalid(): identity_class="individual", x_foo="bar", ) + assert excinfo.value.cls == stix2.Identity + assert excinfo.value.properties == ['x_foo'] + assert "Unexpected properties for" in str(excinfo.value) def test_identity_custom_property_allowed(): @@ -67,8 +71,11 @@ def test_identity_custom_property_allowed(): }""", ]) def test_parse_identity_custom_property(data): - with pytest.raises(stix2.exceptions.ExtraPropertiesError): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: identity = stix2.parse(data) + assert excinfo.value.cls == stix2.Identity + assert excinfo.value.properties == ['foo'] + assert "Unexpected properties for" in str(excinfo.value) identity = stix2.parse(data, allow_custom=True) assert identity.foo == "bar" @@ -88,11 +95,13 @@ def test_custom_object_type(): nt = NewType(property1='something') assert nt.property1 == 'something' - with pytest.raises(stix2.exceptions.MissingPropertiesError): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: NewType(property2=42) + assert "No values for required properties" in str(excinfo.value) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as excinfo: NewType(property1='something', property2=4) + assert "'property2' is too small." in str(excinfo.value) def test_parse_custom_object_type(): @@ -120,11 +129,14 @@ def test_custom_observable_object(): no = NewObservable(property1='something') assert no.property1 == 'something' - with pytest.raises(stix2.exceptions.MissingPropertiesError): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: NewObservable(property2=42) + assert excinfo.value.properties == ['property1'] + assert "No values for required properties" in str(excinfo.value) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as excinfo: NewObservable(property1='something', property2=4) + assert "'property2' is too small." in str(excinfo.value) def test_parse_custom_observable_object(): @@ -137,12 +149,34 @@ def test_parse_custom_observable_object(): assert nt.property1 == 'something' +def test_parse_unregistered_custom_observable_object(): + nt_string = """{ + "type": "x-foobar-observable", + "property1": "something" + }""" + + with pytest.raises(stix2.exceptions.ParseError) as excinfo: + stix2.parse_observable(nt_string) + assert "Can't parse unknown observable type" in str(excinfo.value) + + +def test_parse_invalid_custom_observable_object(): + nt_string = """{ + "property1": "something" + }""" + + with pytest.raises(stix2.exceptions.ParseError) as excinfo: + stix2.parse_observable(nt_string) + assert "Can't parse observable with no 'type' property" in str(excinfo.value) + + def test_observable_custom_property(): - with pytest.raises(ValueError): + with pytest.raises(ValueError) as excinfo: NewObservable( property1='something', custom_properties="foobar", ) + assert "'custom_properties' must be a dictionary" in str(excinfo.value) no = NewObservable( property1='something', @@ -154,11 +188,13 @@ def test_observable_custom_property(): def test_observable_custom_property_invalid(): - with pytest.raises(stix2.exceptions.ExtraPropertiesError): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: NewObservable( property1='something', x_foo="bar", ) + assert excinfo.value.properties == ['x_foo'] + assert "Unexpected properties for" in str(excinfo.value) def test_observable_custom_property_allowed(): @@ -196,31 +232,61 @@ def test_custom_extension(): ext = NewExtension(property1='something') assert ext.property1 == 'something' - with pytest.raises(stix2.exceptions.MissingPropertiesError): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: NewExtension(property2=42) + assert excinfo.value.properties == ['property1'] + assert str(excinfo.value) == "No values for required properties for _Custom: (property1)." - with pytest.raises(ValueError): + with pytest.raises(ValueError) as excinfo: NewExtension(property1='something', property2=4) + assert str(excinfo.value) == "'property2' is too small." -def test_custom_extension_invalid(): +def test_custom_extension_wrong_observable_type(): + ext = NewExtension(property1='something') + with pytest.raises(ValueError) as excinfo: + stix2.File(name="abc.txt", + extensions={ + "ntfs-ext": ext, + }) + + assert 'Cannot determine extension type' in excinfo.value.reason + + +def test_custom_extension_invalid_observable(): + # These extensions are being applied to improperly-created Observables. + # The Observable classes should have been created with the CustomObservable decorator. class Foo(object): pass - with pytest.raises(ValueError): + with pytest.raises(ValueError) as excinfo: @stix2.observables.CustomExtension(Foo, 'x-new-ext', { 'property1': stix2.properties.StringProperty(required=True), }) class FooExtension(): pass # pragma: no cover + assert str(excinfo.value) == "'observable' must be a valid Observable class!" class Bar(stix2.observables._Observable): pass - with pytest.raises(ValueError): + with pytest.raises(ValueError) as excinfo: @stix2.observables.CustomExtension(Bar, 'x-new-ext', { 'property1': stix2.properties.StringProperty(required=True), }) class BarExtension(): pass + assert "Unknown observable type" in str(excinfo.value) + assert "Custom observables must be created with the @CustomObservable decorator." in str(excinfo.value) + + class Baz(stix2.observables._Observable): + _type = 'Baz' + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomExtension(Baz, 'x-new-ext', { + 'property1': stix2.properties.StringProperty(required=True), + }) + class BazExtension(): + pass + assert "Unknown observable type" in str(excinfo.value) + assert "Custom observables must be created with the @CustomObservable decorator." in str(excinfo.value) def test_parse_observable_with_custom_extension(): @@ -237,3 +303,20 @@ def test_parse_observable_with_custom_extension(): parsed = stix2.parse_observable(input_str) assert parsed.extensions['x-new-ext'].property2 == 12 + + +def test_parse_observable_with_unregistered_custom_extension(): + input_str = """{ + "type": "domain-name", + "value": "example.com", + "extensions": { + "x-foobar-ext": { + "property1": "foo", + "property2": 12 + } + } + }""" + + with pytest.raises(ValueError) as excinfo: + stix2.parse_observable(input_str) + assert "Can't parse Unknown extension type" in str(excinfo.value)