From 558948098088dd8305f598a6b76ced30582c7751 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Fri, 19 Jul 2019 14:50:11 -0400 Subject: [PATCH] Improved the exception class hierarchy: - Removed all plain python base classes (e.g. ValueError, TypeError) - Renamed InvalidPropertyConfigurationError -> PropertyPresenceError, since incorrect values could be considered a property config error, and I really just wanted this class to apply to presence (co-)constraint violations. - Added ObjectConfigurationError as a superclass of InvalidValueError, PropertyPresenceError, and any other exception that could be raised during _STIXBase object init, which is when the spec compliance checks happen. This class is intended to represent general spec violations. - Did some class reordering in exceptions.py, so all the ObjectConfigurationError subclasses were together. Changed how property "cleaning" errors were handled: - Previous docs said they should all be ValueErrors, but that would require extra exception check-and-replace complexity in the property implementations, so that requirement is removed. Doc is changed to just say that cleaning problems should cause exceptions to be raised. _STIXBase._check_property() now handles most exception types, not just ValueError. - Decided to try chaining the original clean error to the InvalidValueError, in case the extra diagnostics would be helpful in the future. This is done via 'six' adapter function and only works on python3. - A small amount of testing was removed, since it was looking at custom exception properties which became unavailable once the exception was replaced with InvalidValueError. Did another pass through unit tests to fix breakage caused by the changed exception class hierarchy. Removed unnecessary observable extension handling code from parse_observable(), since it was all duplicated in ExtensionsProperty. The redundant code in parse_observable() had different exception behavior than ExtensionsProperty, which makes the API inconsistent and unit tests more complicated. (Problems in ExtensionsProperty get replaced with InvalidValueError, but extensions problems handled directly in parse_observable() don't get the same replacement, and so the exception type is different.) Redid the workbench monkeypatching. The old way was impossible to make work, and had caused ugly ripple effect hackage in other parts of the codebase. Now, it replaces the global object maps with factory functions which behave the same way when called, as real classes. Had to fix up a few unit tests to get them all passing with this monkeypatching in place. Also remove all the xfail markings in the workbench test suite, since all tests now pass. Since workbench monkeypatching isn't currently affecting any unit tests, tox.ini was simplified to remove the special-casing for running the workbench tests. Removed the v20 workbench test suite, since the workbench currently only works with the latest stix object version. --- stix2/__init__.py | 2 +- stix2/base.py | 24 +- stix2/core.py | 13 - stix2/exceptions.py | 170 ++++++------ stix2/properties.py | 2 +- stix2/test/v20/test_bundle.py | 9 +- stix2/test/v20/test_custom.py | 9 +- stix2/test/v20/test_granular_markings.py | 16 +- stix2/test/v20/test_malware.py | 3 +- stix2/test/v20/test_object_markings.py | 3 +- stix2/test/v20/test_observed_data.py | 27 +- stix2/test/v20/test_properties.py | 30 ++- stix2/test/v20/test_workbench.py | 316 ----------------------- stix2/test/v21/constants.py | 2 +- stix2/test/v21/test_bundle.py | 9 +- stix2/test/v21/test_core.py | 2 +- stix2/test/v21/test_custom.py | 11 +- stix2/test/v21/test_granular_markings.py | 16 +- stix2/test/v21/test_grouping.py | 2 +- stix2/test/v21/test_location.py | 7 +- stix2/test/v21/test_malware.py | 3 +- stix2/test/v21/test_object_markings.py | 3 +- stix2/test/v21/test_observed_data.py | 26 +- stix2/test/v21/test_properties.py | 30 ++- stix2/test/v21/test_workbench.py | 12 - stix2/v21/sdo.py | 26 +- stix2/workbench.py | 60 ++--- tox.ini | 4 +- 28 files changed, 261 insertions(+), 576 deletions(-) delete mode 100644 stix2/test/v20/test_workbench.py diff --git a/stix2/__init__.py b/stix2/__init__.py index 714bf46..68e0264 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -54,7 +54,7 @@ from .patterns import ( WithinQualifier, ) from .utils import new_version, revoke -from .v20 import * # This import will always be the latest STIX 2.X version +from .v21 import * # This import will always be the latest STIX 2.X version from .version import __version__ _collect_stix2_mappings() diff --git a/stix2/base.py b/stix2/base.py index a9a801e..9fe1617 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -5,6 +5,7 @@ import copy import datetime as dt import simplejson as json +import six from .exceptions import ( AtLeastOnePropertyError, CustomContentError, DependentPropertiesError, @@ -88,10 +89,25 @@ class _STIXBase(collections.Mapping): if prop_name in kwargs: try: kwargs[prop_name] = prop.clean(kwargs[prop_name]) - except ValueError as exc: - if self.__allow_custom and isinstance(exc, CustomContentError): - return - raise InvalidValueError(self.__class__, prop_name, reason=str(exc)) + except InvalidValueError: + # No point in wrapping InvalidValueError in another + # InvalidValueError... so let those propagate. + raise + except CustomContentError as exc: + if not self.__allow_custom: + six.raise_from( + InvalidValueError( + self.__class__, prop_name, reason=str(exc), + ), + exc, + ) + except Exception as exc: + six.raise_from( + InvalidValueError( + self.__class__, prop_name, reason=str(exc), + ), + exc, + ) # interproperty constraint methods diff --git a/stix2/core.py b/stix2/core.py index 830d98c..1031d61 100644 --- a/stix2/core.py +++ b/stix2/core.py @@ -169,19 +169,6 @@ def parse_observable(data, _valid_refs=None, allow_custom=False, version=None): raise CustomContentError("Can't parse unknown observable type '%s'! For custom observables, " "use the CustomObservable decorator." % obj['type']) - EXT_MAP = STIX2_OBJ_MAPS[v]['observable-extensions'] - - if 'extensions' in obj and obj['type'] in EXT_MAP: - for name, ext in obj['extensions'].items(): - try: - ext_class = EXT_MAP[obj['type']][name] - except KeyError: - if not allow_custom: - raise CustomContentError("Can't parse unknown extension type '%s'" - "for observable type '%s'!" % (name, obj['type'])) - else: # extension was found - obj['extensions'][name] = ext_class(allow_custom=allow_custom, **obj['extensions'][name]) - return obj_class(allow_custom=allow_custom, **obj) diff --git a/stix2/exceptions.py b/stix2/exceptions.py index 946300c..c680774 100644 --- a/stix2/exceptions.py +++ b/stix2/exceptions.py @@ -5,7 +5,15 @@ class STIXError(Exception): """Base class for errors generated in the stix2 library.""" -class InvalidValueError(STIXError, ValueError): +class ObjectConfigurationError(STIXError): + """ + Represents specification violations regarding the composition of STIX + objects. + """ + pass + + +class InvalidValueError(ObjectConfigurationError): """An invalid value was provided to a STIX object's ``__init__``.""" def __init__(self, cls, prop_name, reason): @@ -19,18 +27,18 @@ class InvalidValueError(STIXError, ValueError): return msg.format(self) -class InvalidPropertyConfigurationError(STIXError, ValueError): +class PropertyPresenceError(ObjectConfigurationError): """ Represents an invalid combination of properties on a STIX object. This class can be used directly when the object requirements are more complicated and none of the more specific exception subclasses apply. """ def __init__(self, message, cls): - super(InvalidPropertyConfigurationError, self).__init__(message) + super(PropertyPresenceError, self).__init__(message) self.cls = cls -class MissingPropertiesError(InvalidPropertyConfigurationError): +class MissingPropertiesError(PropertyPresenceError): """Missing one or more required properties when constructing STIX object.""" def __init__(self, cls, properties): @@ -44,7 +52,7 @@ class MissingPropertiesError(InvalidPropertyConfigurationError): super(MissingPropertiesError, self).__init__(msg, cls) -class ExtraPropertiesError(InvalidPropertyConfigurationError): +class ExtraPropertiesError(PropertyPresenceError): """One or more extra properties were provided when constructing STIX object.""" def __init__(self, cls, properties): @@ -58,7 +66,7 @@ class ExtraPropertiesError(InvalidPropertyConfigurationError): super(ExtraPropertiesError, self).__init__(msg, cls) -class MutuallyExclusivePropertiesError(InvalidPropertyConfigurationError): +class MutuallyExclusivePropertiesError(PropertyPresenceError): """Violating interproperty mutually exclusive constraint of a STIX object type.""" def __init__(self, cls, properties): @@ -72,7 +80,7 @@ class MutuallyExclusivePropertiesError(InvalidPropertyConfigurationError): super(MutuallyExclusivePropertiesError, self).__init__(msg, cls) -class DependentPropertiesError(InvalidPropertyConfigurationError): +class DependentPropertiesError(PropertyPresenceError): """Violating interproperty dependency constraint of a STIX object type.""" def __init__(self, cls, dependencies): @@ -86,7 +94,7 @@ class DependentPropertiesError(InvalidPropertyConfigurationError): super(DependentPropertiesError, self).__init__(msg, cls) -class AtLeastOnePropertyError(InvalidPropertyConfigurationError): +class AtLeastOnePropertyError(PropertyPresenceError): """Violating a constraint of a STIX object type that at least one of the given properties must be populated.""" def __init__(self, cls, properties): @@ -94,27 +102,14 @@ class AtLeastOnePropertyError(InvalidPropertyConfigurationError): msg = "At least one of the ({1}) properties for {0} must be " \ "populated.".format( - cls.__name__, - ", ".join(x for x in self.properties), - ) + cls.__name__, + ", ".join(x for x in self.properties), + ) super(AtLeastOnePropertyError, self).__init__(msg, cls) -class ImmutableError(STIXError, ValueError): - """Attempted to modify an object after creation.""" - - def __init__(self, cls, key): - super(ImmutableError, self).__init__() - self.cls = cls - self.key = key - - def __str__(self): - msg = "Cannot modify '{0.key}' property in '{0.cls.__name__}' after creation." - return msg.format(self) - - -class DictionaryKeyError(STIXError, ValueError): +class DictionaryKeyError(ObjectConfigurationError): """Dictionary key does not conform to the correct format.""" def __init__(self, key, reason): @@ -127,7 +122,7 @@ class DictionaryKeyError(STIXError, ValueError): return msg.format(self) -class InvalidObjRefError(STIXError, ValueError): +class InvalidObjRefError(ObjectConfigurationError): """A STIX Cyber Observable Object contains an invalid object reference.""" def __init__(self, cls, prop_name, reason): @@ -141,47 +136,7 @@ class InvalidObjRefError(STIXError, ValueError): return msg.format(self) -class UnmodifiablePropertyError(STIXError, ValueError): - """Attempted to modify an unmodifiable property of object when creating a new version.""" - - def __init__(self, unchangable_properties): - super(UnmodifiablePropertyError, self).__init__() - self.unchangable_properties = unchangable_properties - - def __str__(self): - msg = "These properties cannot be changed when making a new version: {0}." - return msg.format(", ".join(self.unchangable_properties)) - - -class RevokeError(STIXError, ValueError): - """Attempted to an operation on a revoked object.""" - - def __init__(self, called_by): - super(RevokeError, self).__init__() - self.called_by = called_by - - def __str__(self): - if self.called_by == "revoke": - return "Cannot revoke an already revoked object." - else: - return "Cannot create a new version of a revoked object." - - -class ParseError(STIXError, ValueError): - """Could not parse object.""" - - def __init__(self, msg): - super(ParseError, self).__init__(msg) - - -class CustomContentError(STIXError, ValueError): - """Custom STIX Content (SDO, Observable, Extension, etc.) detected.""" - - def __init__(self, msg): - super(CustomContentError, self).__init__(msg) - - -class InvalidSelectorError(STIXError, AssertionError): +class InvalidSelectorError(ObjectConfigurationError): """Granular Marking selector violation. The selector must resolve into an existing STIX object property.""" def __init__(self, cls, key): @@ -194,20 +149,7 @@ class InvalidSelectorError(STIXError, AssertionError): return msg.format(self.key, self.cls.__class__.__name__) -class MarkingNotFoundError(STIXError, AssertionError): - """Marking violation. The marking reference must be present in SDO or SRO.""" - - def __init__(self, cls, key): - super(MarkingNotFoundError, self).__init__() - self.cls = cls - self.key = key - - def __str__(self): - msg = "Marking {0} was not found in {1}!" - return msg.format(self.key, self.cls.__class__.__name__) - - -class TLPMarkingDefinitionError(STIXError, AssertionError): +class TLPMarkingDefinitionError(ObjectConfigurationError): """Marking violation. The marking-definition for TLP MUST follow the mandated instances from the spec.""" def __init__(self, user_obj, spec_obj): @@ -218,3 +160,69 @@ class TLPMarkingDefinitionError(STIXError, AssertionError): def __str__(self): msg = "Marking {0} does not match spec marking {1}!" return msg.format(self.user_obj, self.spec_obj) + + +class ImmutableError(STIXError): + """Attempted to modify an object after creation.""" + + def __init__(self, cls, key): + super(ImmutableError, self).__init__() + self.cls = cls + self.key = key + + def __str__(self): + msg = "Cannot modify '{0.key}' property in '{0.cls.__name__}' after creation." + return msg.format(self) + + +class UnmodifiablePropertyError(STIXError): + """Attempted to modify an unmodifiable property of object when creating a new version.""" + + def __init__(self, unchangable_properties): + super(UnmodifiablePropertyError, self).__init__() + self.unchangable_properties = unchangable_properties + + def __str__(self): + msg = "These properties cannot be changed when making a new version: {0}." + return msg.format(", ".join(self.unchangable_properties)) + + +class RevokeError(STIXError): + """Attempted an operation on a revoked object.""" + + def __init__(self, called_by): + super(RevokeError, self).__init__() + self.called_by = called_by + + def __str__(self): + if self.called_by == "revoke": + return "Cannot revoke an already revoked object." + else: + return "Cannot create a new version of a revoked object." + + +class ParseError(STIXError): + """Could not parse object.""" + + def __init__(self, msg): + super(ParseError, self).__init__(msg) + + +class CustomContentError(STIXError): + """Custom STIX Content (SDO, Observable, Extension, etc.) detected.""" + + def __init__(self, msg): + super(CustomContentError, self).__init__(msg) + + +class MarkingNotFoundError(STIXError): + """Marking violation. The marking reference must be present in SDO or SRO.""" + + def __init__(self, cls, key): + super(MarkingNotFoundError, self).__init__() + self.cls = cls + self.key = key + + def __str__(self): + msg = "Marking {0} was not found in {1}!" + return msg.format(self.key, self.cls.__class__.__name__) diff --git a/stix2/properties.py b/stix2/properties.py index 4e2f5f6..b9a5aff 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -102,7 +102,7 @@ class Property(object): - Return a value that is valid for this property. If ``value`` is not valid for this property, this will attempt to transform it first. If ``value`` is not valid and no such transformation is possible, it - should raise a ValueError. + should raise an exception. - ``def default(self):`` - provide a default value for this property. - ``default()`` can return the special value ``NOW`` to use the current diff --git a/stix2/test/v20/test_bundle.py b/stix2/test/v20/test_bundle.py index 57c189e..df59f89 100644 --- a/stix2/test/v20/test_bundle.py +++ b/stix2/test/v20/test_bundle.py @@ -4,6 +4,7 @@ import pytest import stix2 +from ...exceptions import InvalidValueError from .constants import IDENTITY_ID EXPECTED_BUNDLE = """{ @@ -156,15 +157,15 @@ def test_create_bundle_with_arg_listarg_and_kwarg(indicator, malware, relationsh def test_create_bundle_invalid(indicator, malware, relationship): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.v20.Bundle(objects=[1]) assert excinfo.value.reason == "This property may only contain a dictionary or object" - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.v20.Bundle(objects=[{}]) assert excinfo.value.reason == "This property may only contain a non-empty dictionary or object" - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.v20.Bundle(objects=[{'type': 'bundle'}]) assert excinfo.value.reason == 'This property may not contain a Bundle object' @@ -232,7 +233,7 @@ def test_bundle_with_different_spec_objects(): }, ] - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.v20.Bundle(objects=data) assert "Spec version 2.0 bundles don't yet support containing objects of a different spec version." in str(excinfo.value) diff --git a/stix2/test/v20/test_custom.py b/stix2/test/v20/test_custom.py index 32632b9..8a99b2c 100644 --- a/stix2/test/v20/test_custom.py +++ b/stix2/test/v20/test_custom.py @@ -2,6 +2,7 @@ import pytest import stix2 +from ...exceptions import InvalidValueError from .constants import FAKE_TIME, IDENTITY_ID, MARKING_DEFINITION_ID IDENTITY_CUSTOM_PROP = stix2.v20.Identity( @@ -133,7 +134,7 @@ def test_custom_property_dict_in_bundled_object(): 'identity_class': 'individual', 'x_foo': 'bar', } - with pytest.raises(stix2.exceptions.ExtraPropertiesError): + with pytest.raises(InvalidValueError): stix2.v20.Bundle(custom_identity) bundle = stix2.v20.Bundle(custom_identity, allow_custom=True) @@ -199,7 +200,7 @@ def test_custom_property_object_in_observable_extension(): def test_custom_property_dict_in_observable_extension(): - with pytest.raises(stix2.exceptions.ExtraPropertiesError): + with pytest.raises(InvalidValueError): stix2.v20.File( name='test', extensions={ @@ -718,7 +719,7 @@ def test_custom_extension(): def test_custom_extension_wrong_observable_type(): # NewExtension is an extension of DomainName, not File ext = NewExtension(property1='something') - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.v20.File( name="abc.txt", extensions={ @@ -911,7 +912,7 @@ def test_parse_observable_with_custom_extension(): ], ) def test_parse_observable_with_unregistered_custom_extension(data): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.parse_observable(data, version='2.0') assert "Can't parse unknown extension type" in str(excinfo.value) diff --git a/stix2/test/v20/test_granular_markings.py b/stix2/test/v20/test_granular_markings.py index b5f2e3d..e912cc1 100644 --- a/stix2/test/v20/test_granular_markings.py +++ b/stix2/test/v20/test_granular_markings.py @@ -2,7 +2,7 @@ import pytest from stix2 import markings -from stix2.exceptions import MarkingNotFoundError +from stix2.exceptions import InvalidSelectorError, MarkingNotFoundError from stix2.v20 import TLP_RED, Malware from .constants import MALWARE_MORE_KWARGS as MALWARE_KWARGS_CONST @@ -179,7 +179,7 @@ def test_add_marking_mark_same_property_same_marking(): ], ) def test_add_marking_bad_selector(data, marking): - with pytest.raises(AssertionError): + with pytest.raises(InvalidSelectorError): markings.add_markings(data, marking[0], marking[1]) @@ -299,7 +299,7 @@ def test_get_markings_multiple_selectors(data): ) def test_get_markings_bad_selector(data, selector): """Test bad selectors raise exception""" - with pytest.raises(AssertionError): + with pytest.raises(InvalidSelectorError): markings.get_markings(data, selector) @@ -560,7 +560,7 @@ def test_remove_marking_bad_selector(): before = { "description": "test description", } - with pytest.raises(AssertionError): + with pytest.raises(InvalidSelectorError): markings.remove_markings(before, ["marking-definition--1", "marking-definition--2"], ["title"]) @@ -642,7 +642,7 @@ def test_is_marked_smoke(data): ) def test_is_marked_invalid_selector(data, selector): """Test invalid selector raises an error.""" - with pytest.raises(AssertionError): + with pytest.raises(InvalidSelectorError): markings.is_marked(data, selectors=selector) @@ -836,7 +836,7 @@ def test_is_marked_positional_arguments_combinations(): def test_create_sdo_with_invalid_marking(): - with pytest.raises(AssertionError) as excinfo: + with pytest.raises(InvalidSelectorError) as excinfo: Malware( granular_markings=[ { @@ -974,7 +974,7 @@ def test_set_marking_bad_selector(marking): **MALWARE_KWARGS ) - with pytest.raises(AssertionError): + with pytest.raises(InvalidSelectorError): before = markings.set_markings(before, marking[0], marking[1]) assert before == after @@ -1080,7 +1080,7 @@ def test_clear_marking_all_selectors(data): ) def test_clear_marking_bad_selector(data, selector): """Test bad selector raises exception.""" - with pytest.raises(AssertionError): + with pytest.raises(InvalidSelectorError): markings.clear_markings(data, selector) diff --git a/stix2/test/v20/test_malware.py b/stix2/test/v20/test_malware.py index 900a4b9..bd49007 100644 --- a/stix2/test/v20/test_malware.py +++ b/stix2/test/v20/test_malware.py @@ -6,6 +6,7 @@ import pytz import stix2 +from ...exceptions import InvalidValueError from .constants import FAKE_TIME, MALWARE_ID, MALWARE_KWARGS EXPECTED_MALWARE = """{ @@ -145,7 +146,7 @@ def test_parse_malware(data): def test_parse_malware_invalid_labels(): data = re.compile('\\[.+\\]', re.DOTALL).sub('1', EXPECTED_MALWARE) - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.parse(data, version="2.0") assert "Invalid value for Malware 'labels'" in str(excinfo.value) diff --git a/stix2/test/v20/test_object_markings.py b/stix2/test/v20/test_object_markings.py index 156c42d..191f33a 100644 --- a/stix2/test/v20/test_object_markings.py +++ b/stix2/test/v20/test_object_markings.py @@ -4,6 +4,7 @@ import pytest from stix2 import exceptions, markings from stix2.v20 import TLP_AMBER, Malware +from ...exceptions import MarkingNotFoundError from .constants import FAKE_TIME, MALWARE_ID from .constants import MALWARE_KWARGS as MALWARE_KWARGS_CONST from .constants import MARKING_IDS @@ -350,7 +351,7 @@ def test_remove_markings_bad_markings(): object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], **MALWARE_KWARGS ) - with pytest.raises(AssertionError) as excinfo: + with pytest.raises(MarkingNotFoundError) as excinfo: markings.remove_markings(before, [MARKING_IDS[4]], None) assert str(excinfo.value) == "Marking ['%s'] was not found in Malware!" % MARKING_IDS[4] diff --git a/stix2/test/v20/test_observed_data.py b/stix2/test/v20/test_observed_data.py index 95daf22..a822efb 100644 --- a/stix2/test/v20/test_observed_data.py +++ b/stix2/test/v20/test_observed_data.py @@ -6,6 +6,7 @@ import pytz import stix2 +from ...exceptions import InvalidValueError from .constants import IDENTITY_ID, OBSERVED_DATA_ID OBJECTS_REGEX = re.compile('\"objects\": {(?:.*?)(?:(?:[^{]*?)|(?:{[^{]*?}))*}', re.DOTALL) @@ -239,7 +240,7 @@ def test_parse_artifact_valid(data): ) def test_parse_artifact_invalid(data): odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) - with pytest.raises(ValueError): + with pytest.raises(InvalidValueError): stix2.parse(odata_str, version="2.0") @@ -468,11 +469,10 @@ def test_parse_email_message_with_at_least_one_error(data): "4": "artifact", "5": "file", } - with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.parse_observable(data, valid_refs, version='2.0') - assert excinfo.value.cls == stix2.v20.EmailMIMEComponent - assert excinfo.value.properties == ["body", "body_raw_ref"] + assert excinfo.value.cls == stix2.v20.EmailMessage assert "At least one of the" in str(excinfo.value) assert "must be populated" in str(excinfo.value) @@ -734,7 +734,7 @@ def test_file_example_with_NTFSExt(): def test_file_example_with_empty_NTFSExt(): - with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.v20.File( name="abc.txt", extensions={ @@ -742,8 +742,7 @@ def test_file_example_with_empty_NTFSExt(): }, ) - assert excinfo.value.cls == stix2.v20.NTFSExt - assert excinfo.value.properties == sorted(list(stix2.NTFSExt._properties.keys())) + assert excinfo.value.cls == stix2.v20.File def test_file_example_with_PDFExt(): @@ -1112,16 +1111,14 @@ def test_process_example_empty_error(): def test_process_example_empty_with_extensions(): - with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.v20.Process( extensions={ "windows-process-ext": {}, }, ) - assert excinfo.value.cls == stix2.v20.WindowsProcessExt - properties_of_extension = list(stix2.v20.WindowsProcessExt._properties.keys()) - assert excinfo.value.properties == sorted(properties_of_extension) + assert excinfo.value.cls == stix2.v20.Process def test_process_example_windows_process_ext(): @@ -1144,7 +1141,7 @@ def test_process_example_windows_process_ext(): def test_process_example_windows_process_ext_empty(): - with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.v20.Process( pid=1221, name="gedit-bin", @@ -1153,9 +1150,7 @@ def test_process_example_windows_process_ext_empty(): }, ) - assert excinfo.value.cls == stix2.v20.WindowsProcessExt - properties_of_extension = list(stix2.v20.WindowsProcessExt._properties.keys()) - assert excinfo.value.properties == sorted(properties_of_extension) + assert excinfo.value.cls == stix2.v20.Process def test_process_example_extensions_empty(): @@ -1289,7 +1284,7 @@ def test_user_account_unix_account_ext_example(): def test_windows_registry_key_example(): - with pytest.raises(ValueError): + with pytest.raises(InvalidValueError): stix2.v20.WindowsRegistryValueType( name="Foo", data="qwerty", diff --git a/stix2/test/v20/test_properties.py b/stix2/test/v20/test_properties.py index 04a26f4..9952eac 100644 --- a/stix2/test/v20/test_properties.py +++ b/stix2/test/v20/test_properties.py @@ -3,7 +3,9 @@ import uuid import pytest import stix2 -from stix2.exceptions import AtLeastOnePropertyError, DictionaryKeyError +from stix2.exceptions import ( + AtLeastOnePropertyError, CustomContentError, DictionaryKeyError, +) from stix2.properties import ( BinaryProperty, BooleanProperty, DictionaryProperty, EmbeddedObjectProperty, EnumProperty, ExtensionsProperty, FloatProperty, @@ -465,23 +467,27 @@ def test_extension_property_valid(): }) -@pytest.mark.parametrize( - "data", [ - 1, - {'foobar-ext': { - 'pe_type': 'exe', - }}, - ], -) -def test_extension_property_invalid(data): +def test_extension_property_invalid1(): ext_prop = ExtensionsProperty(spec_version="2.0", enclosing_type='file') with pytest.raises(ValueError): - ext_prop.clean(data) + ext_prop.clean(1) + + +def test_extension_property_invalid2(): + ext_prop = ExtensionsProperty(spec_version="2.0", enclosing_type='file') + with pytest.raises(CustomContentError): + ext_prop.clean( + { + 'foobar-ext': { + 'pe_type': 'exe', + }, + }, + ) def test_extension_property_invalid_type(): ext_prop = ExtensionsProperty(spec_version="2.0", enclosing_type='indicator') - with pytest.raises(ValueError) as excinfo: + with pytest.raises(CustomContentError) as excinfo: ext_prop.clean( { 'windows-pebinary-ext': { diff --git a/stix2/test/v20/test_workbench.py b/stix2/test/v20/test_workbench.py deleted file mode 100644 index c254966..0000000 --- a/stix2/test/v20/test_workbench.py +++ /dev/null @@ -1,316 +0,0 @@ -import os - -import stix2 -from stix2.workbench import ( - AttackPattern, Campaign, CourseOfAction, ExternalReference, - FileSystemSource, Filter, Identity, Indicator, IntrusionSet, Malware, - MarkingDefinition, ObservedData, Relationship, Report, StatementMarking, - ThreatActor, Tool, Vulnerability, add_data_source, all_versions, - attack_patterns, campaigns, courses_of_action, create, get, identities, - indicators, intrusion_sets, malware, observed_data, query, reports, save, - set_default_created, set_default_creator, set_default_external_refs, - set_default_object_marking_refs, threat_actors, tools, vulnerabilities, -) - -from .constants import ( - ATTACK_PATTERN_ID, ATTACK_PATTERN_KWARGS, CAMPAIGN_ID, CAMPAIGN_KWARGS, - COURSE_OF_ACTION_ID, COURSE_OF_ACTION_KWARGS, IDENTITY_ID, IDENTITY_KWARGS, - INDICATOR_ID, INDICATOR_KWARGS, INTRUSION_SET_ID, INTRUSION_SET_KWARGS, - MALWARE_ID, MALWARE_KWARGS, OBSERVED_DATA_ID, OBSERVED_DATA_KWARGS, - REPORT_ID, REPORT_KWARGS, THREAT_ACTOR_ID, THREAT_ACTOR_KWARGS, TOOL_ID, - TOOL_KWARGS, VULNERABILITY_ID, VULNERABILITY_KWARGS, -) - - -def test_workbench_environment(): - - # Create a STIX object - ind = create(Indicator, id=INDICATOR_ID, **INDICATOR_KWARGS) - save(ind) - - resp = get(INDICATOR_ID) - assert resp['labels'][0] == 'malicious-activity' - - resp = all_versions(INDICATOR_ID) - assert len(resp) == 1 - - # Search on something other than id - q = [Filter('type', '=', 'vulnerability')] - resp = query(q) - assert len(resp) == 0 - - -def test_workbench_get_all_attack_patterns(): - mal = AttackPattern(id=ATTACK_PATTERN_ID, **ATTACK_PATTERN_KWARGS) - save(mal) - - resp = attack_patterns() - assert len(resp) == 1 - assert resp[0].id == ATTACK_PATTERN_ID - - -def test_workbench_get_all_campaigns(): - cam = Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) - save(cam) - - resp = campaigns() - assert len(resp) == 1 - assert resp[0].id == CAMPAIGN_ID - - -def test_workbench_get_all_courses_of_action(): - coa = CourseOfAction(id=COURSE_OF_ACTION_ID, **COURSE_OF_ACTION_KWARGS) - save(coa) - - resp = courses_of_action() - assert len(resp) == 1 - assert resp[0].id == COURSE_OF_ACTION_ID - - -def test_workbench_get_all_identities(): - idty = Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) - save(idty) - - resp = identities() - assert len(resp) == 1 - assert resp[0].id == IDENTITY_ID - - -def test_workbench_get_all_indicators(): - resp = indicators() - assert len(resp) == 1 - assert resp[0].id == INDICATOR_ID - - -def test_workbench_get_all_intrusion_sets(): - ins = IntrusionSet(id=INTRUSION_SET_ID, **INTRUSION_SET_KWARGS) - save(ins) - - resp = intrusion_sets() - assert len(resp) == 1 - assert resp[0].id == INTRUSION_SET_ID - - -def test_workbench_get_all_malware(): - mal = Malware(id=MALWARE_ID, **MALWARE_KWARGS) - save(mal) - - resp = malware() - assert len(resp) == 1 - assert resp[0].id == MALWARE_ID - - -def test_workbench_get_all_observed_data(): - od = ObservedData(id=OBSERVED_DATA_ID, **OBSERVED_DATA_KWARGS) - save(od) - - resp = observed_data() - assert len(resp) == 1 - assert resp[0].id == OBSERVED_DATA_ID - - -def test_workbench_get_all_reports(): - rep = Report(id=REPORT_ID, **REPORT_KWARGS) - save(rep) - - resp = reports() - assert len(resp) == 1 - assert resp[0].id == REPORT_ID - - -def test_workbench_get_all_threat_actors(): - thr = ThreatActor(id=THREAT_ACTOR_ID, **THREAT_ACTOR_KWARGS) - save(thr) - - resp = threat_actors() - assert len(resp) == 1 - assert resp[0].id == THREAT_ACTOR_ID - - -def test_workbench_get_all_tools(): - tool = Tool(id=TOOL_ID, **TOOL_KWARGS) - save(tool) - - resp = tools() - assert len(resp) == 1 - assert resp[0].id == TOOL_ID - - -def test_workbench_get_all_vulnerabilities(): - vuln = Vulnerability(id=VULNERABILITY_ID, **VULNERABILITY_KWARGS) - save(vuln) - - resp = vulnerabilities() - assert len(resp) == 1 - assert resp[0].id == VULNERABILITY_ID - - -def test_workbench_add_to_bundle(): - vuln = Vulnerability(**VULNERABILITY_KWARGS) - bundle = stix2.v20.Bundle(vuln) - assert bundle.objects[0].name == 'Heartbleed' - - -def test_workbench_relationships(): - rel = Relationship(INDICATOR_ID, 'indicates', MALWARE_ID) - save(rel) - - ind = get(INDICATOR_ID) - resp = ind.relationships() - assert len(resp) == 1 - assert resp[0].relationship_type == 'indicates' - assert resp[0].source_ref == INDICATOR_ID - assert resp[0].target_ref == MALWARE_ID - - -def test_workbench_created_by(): - intset = IntrusionSet(name="Breach 123", created_by_ref=IDENTITY_ID) - save(intset) - creator = intset.created_by() - assert creator.id == IDENTITY_ID - - -def test_workbench_related(): - rel1 = Relationship(MALWARE_ID, 'targets', IDENTITY_ID) - rel2 = Relationship(CAMPAIGN_ID, 'uses', MALWARE_ID) - save([rel1, rel2]) - - resp = get(MALWARE_ID).related() - assert len(resp) == 3 - assert any(x['id'] == CAMPAIGN_ID for x in resp) - assert any(x['id'] == INDICATOR_ID for x in resp) - assert any(x['id'] == IDENTITY_ID for x in resp) - - resp = get(MALWARE_ID).related(relationship_type='indicates') - assert len(resp) == 1 - - -def test_workbench_related_with_filters(): - malware = Malware(labels=["ransomware"], name="CryptorBit", created_by_ref=IDENTITY_ID) - rel = Relationship(malware.id, 'variant-of', MALWARE_ID) - save([malware, rel]) - - filters = [Filter('created_by_ref', '=', IDENTITY_ID)] - resp = get(MALWARE_ID).related(filters=filters) - - assert len(resp) == 1 - assert resp[0].name == malware.name - assert resp[0].created_by_ref == IDENTITY_ID - - # filters arg can also be single filter - resp = get(MALWARE_ID).related(filters=filters[0]) - assert len(resp) == 1 - - -def test_add_data_source(): - fs_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "stix2_data") - fs = FileSystemSource(fs_path) - add_data_source(fs) - - resp = tools() - assert len(resp) == 3 - resp_ids = [tool.id for tool in resp] - assert TOOL_ID in resp_ids - assert 'tool--03342581-f790-4f03-ba41-e82e67392e23' in resp_ids - assert 'tool--242f3da3-4425-4d11-8f5c-b842886da966' in resp_ids - - -def test_additional_filter(): - resp = tools(Filter('created_by_ref', '=', 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5')) - assert len(resp) == 2 - - -def test_additional_filters_list(): - resp = tools([ - Filter('created_by_ref', '=', 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5'), - Filter('name', '=', 'Windows Credential Editor'), - ]) - assert len(resp) == 1 - - -def test_default_creator(): - set_default_creator(IDENTITY_ID) - campaign = Campaign(**CAMPAIGN_KWARGS) - - assert 'created_by_ref' not in CAMPAIGN_KWARGS - assert campaign.created_by_ref == IDENTITY_ID - - -def test_default_created_timestamp(): - timestamp = "2018-03-19T01:02:03.000Z" - set_default_created(timestamp) - campaign = Campaign(**CAMPAIGN_KWARGS) - - assert 'created' not in CAMPAIGN_KWARGS - assert stix2.utils.format_datetime(campaign.created) == timestamp - assert stix2.utils.format_datetime(campaign.modified) == timestamp - - -def test_default_external_refs(): - ext_ref = ExternalReference( - source_name="ACME Threat Intel", - description="Threat report", - ) - set_default_external_refs(ext_ref) - campaign = Campaign(**CAMPAIGN_KWARGS) - - assert campaign.external_references[0].source_name == "ACME Threat Intel" - assert campaign.external_references[0].description == "Threat report" - - -def test_default_object_marking_refs(): - stmt_marking = StatementMarking("Copyright 2016, Example Corp") - mark_def = MarkingDefinition( - definition_type="statement", - definition=stmt_marking, - ) - set_default_object_marking_refs(mark_def) - campaign = Campaign(**CAMPAIGN_KWARGS) - - assert campaign.object_marking_refs[0] == mark_def.id - - -def test_workbench_custom_property_object_in_observable_extension(): - ntfs = stix2.v20.NTFSExt( - allow_custom=True, - sid=1, - x_foo='bar', - ) - artifact = stix2.v20.File( - name='test', - extensions={'ntfs-ext': ntfs}, - ) - observed_data = ObservedData( - allow_custom=True, - first_observed="2015-12-21T19:00:00Z", - last_observed="2015-12-21T19:00:00Z", - number_observed=1, - objects={"0": artifact}, - ) - - assert observed_data.objects['0'].extensions['ntfs-ext'].x_foo == "bar" - assert '"x_foo": "bar"' in str(observed_data) - - -def test_workbench_custom_property_dict_in_observable_extension(): - artifact = stix2.v20.File( - allow_custom=True, - name='test', - extensions={ - 'ntfs-ext': { - 'allow_custom': True, - 'sid': 1, - 'x_foo': 'bar', - }, - }, - ) - observed_data = ObservedData( - allow_custom=True, - first_observed="2015-12-21T19:00:00Z", - last_observed="2015-12-21T19:00:00Z", - number_observed=1, - objects={"0": artifact}, - ) - - assert observed_data.objects['0'].extensions['ntfs-ext'].x_foo == "bar" - assert '"x_foo": "bar"' in str(observed_data) diff --git a/stix2/test/v21/constants.py b/stix2/test/v21/constants.py index fd1ff38..c3ce3c0 100644 --- a/stix2/test/v21/constants.py +++ b/stix2/test/v21/constants.py @@ -77,7 +77,7 @@ GROUPING_KWARGS = dict( context="suspicious-activity", object_refs=[ "malware--c8d2fae5-7271-400c-b81d-931a4caf20b9", - "identity--988145ed-a3b4-4421-b7a7-273376be67ce" + "identity--988145ed-a3b4-4421-b7a7-273376be67ce", ], ) diff --git a/stix2/test/v21/test_bundle.py b/stix2/test/v21/test_bundle.py index 58d3b3f..54ef318 100644 --- a/stix2/test/v21/test_bundle.py +++ b/stix2/test/v21/test_bundle.py @@ -4,6 +4,7 @@ import pytest import stix2 +from ...exceptions import InvalidValueError from .constants import IDENTITY_ID EXPECTED_BUNDLE = """{ @@ -164,15 +165,15 @@ def test_create_bundle_with_arg_listarg_and_kwarg(indicator, malware, relationsh def test_create_bundle_invalid(indicator, malware, relationship): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.v21.Bundle(objects=[1]) assert excinfo.value.reason == "This property may only contain a dictionary or object" - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.v21.Bundle(objects=[{}]) assert excinfo.value.reason == "This property may only contain a non-empty dictionary or object" - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.v21.Bundle(objects=[{'type': 'bundle'}]) assert excinfo.value.reason == 'This property may not contain a Bundle object' @@ -183,7 +184,7 @@ def test_parse_bundle(version): assert bundle.type == "bundle" assert bundle.id.startswith("bundle--") - assert type(bundle.objects[0]) is stix2.v21.Indicator + assert isinstance(bundle.objects[0], stix2.v21.Indicator) assert bundle.objects[0].type == 'indicator' assert bundle.objects[1].type == 'malware' assert bundle.objects[2].type == 'relationship' diff --git a/stix2/test/v21/test_core.py b/stix2/test/v21/test_core.py index f04e600..25348cd 100644 --- a/stix2/test/v21/test_core.py +++ b/stix2/test/v21/test_core.py @@ -79,7 +79,7 @@ def test_register_object_with_version(): v = 'v21' assert bundle.objects[0].type in core.STIX2_OBJ_MAPS[v]['objects'] - assert v in str(bundle.objects[0].__class__) + assert bundle.objects[0].spec_version == "2.1" def test_register_marking_with_version(): diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py index 6e1e585..a5c9244 100644 --- a/stix2/test/v21/test_custom.py +++ b/stix2/test/v21/test_custom.py @@ -3,6 +3,7 @@ import pytest import stix2 import stix2.base +from ...exceptions import InvalidValueError from .constants import FAKE_TIME, IDENTITY_ID, MARKING_DEFINITION_ID IDENTITY_CUSTOM_PROP = stix2.v21.Identity( @@ -97,7 +98,7 @@ def test_identity_custom_property_allowed(): def test_parse_identity_custom_property(data): with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: stix2.parse(data, version="2.1") - assert excinfo.value.cls == stix2.v21.Identity + assert issubclass(excinfo.value.cls, stix2.v21.Identity) assert excinfo.value.properties == ['foo'] assert "Unexpected properties for" in str(excinfo.value) @@ -136,7 +137,7 @@ def test_custom_property_dict_in_bundled_object(): 'identity_class': 'individual', 'x_foo': 'bar', } - with pytest.raises(stix2.exceptions.ExtraPropertiesError): + with pytest.raises(InvalidValueError): stix2.v21.Bundle(custom_identity) bundle = stix2.v21.Bundle(custom_identity, allow_custom=True) @@ -203,7 +204,7 @@ def test_custom_property_object_in_observable_extension(): def test_custom_property_dict_in_observable_extension(): - with pytest.raises(stix2.exceptions.ExtraPropertiesError): + with pytest.raises(InvalidValueError): stix2.v21.File( name='test', extensions={ @@ -722,7 +723,7 @@ def test_custom_extension(): def test_custom_extension_wrong_observable_type(): # NewExtension is an extension of DomainName, not File ext = NewExtension(property1='something') - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.v21.File( name="abc.txt", extensions={ @@ -915,7 +916,7 @@ def test_parse_observable_with_custom_extension(): ], ) def test_parse_observable_with_unregistered_custom_extension(data): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.parse_observable(data, version='2.1') assert "Can't parse unknown extension type" in str(excinfo.value) diff --git a/stix2/test/v21/test_granular_markings.py b/stix2/test/v21/test_granular_markings.py index e178f86..1c3194b 100644 --- a/stix2/test/v21/test_granular_markings.py +++ b/stix2/test/v21/test_granular_markings.py @@ -1,7 +1,7 @@ import pytest from stix2 import markings -from stix2.exceptions import MarkingNotFoundError +from stix2.exceptions import InvalidSelectorError, MarkingNotFoundError from stix2.v21 import TLP_RED, Malware from .constants import MALWARE_MORE_KWARGS as MALWARE_KWARGS_CONST @@ -209,7 +209,7 @@ def test_add_marking_mark_same_property_same_marking(): ], ) def test_add_marking_bad_selector(data, marking): - with pytest.raises(AssertionError): + with pytest.raises(InvalidSelectorError): markings.add_markings(data, marking[0], marking[1]) @@ -329,7 +329,7 @@ def test_get_markings_multiple_selectors(data): ) def test_get_markings_bad_selector(data, selector): """Test bad selectors raise exception""" - with pytest.raises(AssertionError): + with pytest.raises(InvalidSelectorError): markings.get_markings(data, selector) @@ -714,7 +714,7 @@ def test_remove_marking_bad_selector(): before = { "description": "test description", } - with pytest.raises(AssertionError): + with pytest.raises(InvalidSelectorError): markings.remove_markings(before, ["marking-definition--1", "marking-definition--2"], ["title"]) @@ -805,7 +805,7 @@ def test_is_marked_smoke(data): ) def test_is_marked_invalid_selector(data, selector): """Test invalid selector raises an error.""" - with pytest.raises(AssertionError): + with pytest.raises(InvalidSelectorError): markings.is_marked(data, selectors=selector) @@ -1000,7 +1000,7 @@ def test_is_marked_positional_arguments_combinations(): def test_create_sdo_with_invalid_marking(): - with pytest.raises(AssertionError) as excinfo: + with pytest.raises(InvalidSelectorError) as excinfo: Malware( granular_markings=[ { @@ -1192,7 +1192,7 @@ def test_set_marking_bad_selector(marking): **MALWARE_KWARGS ) - with pytest.raises(AssertionError): + with pytest.raises(InvalidSelectorError): before = markings.set_markings(before, marking[0], marking[1]) assert before == after @@ -1298,7 +1298,7 @@ def test_clear_marking_all_selectors(data): ) def test_clear_marking_bad_selector(data, selector): """Test bad selector raises exception.""" - with pytest.raises(AssertionError): + with pytest.raises(InvalidSelectorError): markings.clear_markings(data, selector) diff --git a/stix2/test/v21/test_grouping.py b/stix2/test/v21/test_grouping.py index 449400b..a92a180 100644 --- a/stix2/test/v21/test_grouping.py +++ b/stix2/test/v21/test_grouping.py @@ -124,5 +124,5 @@ def test_parse_grouping(data): assert grp.context == "suspicious-activity" assert grp.object_refs == [ "malware--c8d2fae5-7271-400c-b81d-931a4caf20b9", - "identity--988145ed-a3b4-4421-b7a7-273376be67ce" + "identity--988145ed-a3b4-4421-b7a7-273376be67ce", ] diff --git a/stix2/test/v21/test_location.py b/stix2/test/v21/test_location.py index c734334..e8c8597 100644 --- a/stix2/test/v21/test_location.py +++ b/stix2/test/v21/test_location.py @@ -5,6 +5,7 @@ import pytest import pytz import stix2 +import stix2.exceptions from .constants import LOCATION_ID @@ -111,7 +112,7 @@ def test_parse_location(data): ], ) def test_location_bad_latitude(data): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: stix2.parse(data) assert "Invalid value for Location 'latitude'" in str(excinfo.value) @@ -140,7 +141,7 @@ def test_location_bad_latitude(data): ], ) def test_location_bad_longitude(data): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: stix2.parse(data) assert "Invalid value for Location 'longitude'" in str(excinfo.value) @@ -190,7 +191,7 @@ def test_location_properties_missing_when_precision_is_present(data): ], ) def test_location_negative_precision(data): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: stix2.parse(data) assert "Invalid value for Location 'precision'" in str(excinfo.value) diff --git a/stix2/test/v21/test_malware.py b/stix2/test/v21/test_malware.py index 9ae0ed2..0fc652a 100644 --- a/stix2/test/v21/test_malware.py +++ b/stix2/test/v21/test_malware.py @@ -6,6 +6,7 @@ import pytz import stix2 +from ...exceptions import InvalidValueError from .constants import FAKE_TIME, MALWARE_ID, MALWARE_KWARGS EXPECTED_MALWARE = """{ @@ -136,7 +137,7 @@ def test_parse_malware(data): def test_parse_malware_invalid_labels(): data = re.compile('\\[.+\\]', re.DOTALL).sub('1', EXPECTED_MALWARE) - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InvalidValueError) as excinfo: stix2.parse(data) assert "Invalid value for Malware 'malware_types'" in str(excinfo.value) diff --git a/stix2/test/v21/test_object_markings.py b/stix2/test/v21/test_object_markings.py index 7b19d4f..a21fbf6 100644 --- a/stix2/test/v21/test_object_markings.py +++ b/stix2/test/v21/test_object_markings.py @@ -3,6 +3,7 @@ import pytest from stix2 import exceptions, markings from stix2.v21 import TLP_AMBER, Malware +from ...exceptions import MarkingNotFoundError from .constants import FAKE_TIME, MALWARE_ID from .constants import MALWARE_KWARGS as MALWARE_KWARGS_CONST from .constants import MARKING_IDS @@ -349,7 +350,7 @@ def test_remove_markings_bad_markings(): object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], **MALWARE_KWARGS ) - with pytest.raises(AssertionError) as excinfo: + with pytest.raises(MarkingNotFoundError) as excinfo: markings.remove_markings(before, [MARKING_IDS[4]], None) assert str(excinfo.value) == "Marking ['%s'] was not found in Malware!" % MARKING_IDS[4] diff --git a/stix2/test/v21/test_observed_data.py b/stix2/test/v21/test_observed_data.py index 09e6a67..0e97ca6 100644 --- a/stix2/test/v21/test_observed_data.py +++ b/stix2/test/v21/test_observed_data.py @@ -305,7 +305,7 @@ def test_parse_artifact_valid(data): ) def test_parse_artifact_invalid(data): odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) - with pytest.raises(ValueError): + with pytest.raises(stix2.exceptions.InvalidValueError): stix2.parse(odata_str, version="2.1") @@ -534,11 +534,10 @@ def test_parse_email_message_with_at_least_one_error(data): "4": "artifact", "5": "file", } - with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: stix2.parse_observable(data, valid_refs, version='2.1') - assert excinfo.value.cls == stix2.v21.EmailMIMEComponent - assert excinfo.value.properties == ["body", "body_raw_ref"] + assert excinfo.value.cls == stix2.v21.EmailMessage assert "At least one of the" in str(excinfo.value) assert "must be populated" in str(excinfo.value) @@ -788,7 +787,7 @@ def test_file_example_with_NTFSExt(): def test_file_example_with_empty_NTFSExt(): - with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: stix2.v21.File( name="abc.txt", extensions={ @@ -796,8 +795,7 @@ def test_file_example_with_empty_NTFSExt(): }, ) - assert excinfo.value.cls == stix2.v21.NTFSExt - assert excinfo.value.properties == sorted(list(stix2.NTFSExt._properties.keys())) + assert excinfo.value.cls == stix2.v21.File def test_file_example_with_PDFExt(): @@ -1152,14 +1150,12 @@ def test_process_example_empty_error(): def test_process_example_empty_with_extensions(): - with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: stix2.v21.Process(extensions={ "windows-process-ext": {}, }) - assert excinfo.value.cls == stix2.v21.WindowsProcessExt - properties_of_extension = list(stix2.v21.WindowsProcessExt._properties.keys()) - assert excinfo.value.properties == sorted(properties_of_extension) + assert excinfo.value.cls == stix2.v21.Process def test_process_example_windows_process_ext(): @@ -1181,7 +1177,7 @@ def test_process_example_windows_process_ext(): def test_process_example_windows_process_ext_empty(): - with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: stix2.v21.Process( pid=1221, extensions={ @@ -1189,9 +1185,7 @@ def test_process_example_windows_process_ext_empty(): }, ) - assert excinfo.value.cls == stix2.v21.WindowsProcessExt - properties_of_extension = list(stix2.v21.WindowsProcessExt._properties.keys()) - assert excinfo.value.properties == sorted(properties_of_extension) + assert excinfo.value.cls == stix2.v21.Process def test_process_example_extensions_empty(): @@ -1324,7 +1318,7 @@ def test_user_account_unix_account_ext_example(): def test_windows_registry_key_example(): - with pytest.raises(ValueError): + with pytest.raises(stix2.exceptions.InvalidValueError): stix2.v21.WindowsRegistryValueType( name="Foo", data="qwerty", diff --git a/stix2/test/v21/test_properties.py b/stix2/test/v21/test_properties.py index 557e419..fde13d3 100644 --- a/stix2/test/v21/test_properties.py +++ b/stix2/test/v21/test_properties.py @@ -1,7 +1,9 @@ import pytest import stix2 -from stix2.exceptions import AtLeastOnePropertyError, DictionaryKeyError +from stix2.exceptions import ( + AtLeastOnePropertyError, CustomContentError, DictionaryKeyError, +) from stix2.properties import ( BinaryProperty, BooleanProperty, DictionaryProperty, EmbeddedObjectProperty, EnumProperty, ExtensionsProperty, FloatProperty, @@ -474,23 +476,27 @@ def test_extension_property_valid(): }) -@pytest.mark.parametrize( - "data", [ - 1, - {'foobar-ext': { - 'pe_type': 'exe', - }}, - ], -) -def test_extension_property_invalid(data): +def test_extension_property_invalid1(): ext_prop = ExtensionsProperty(spec_version='2.1', enclosing_type='file') with pytest.raises(ValueError): - ext_prop.clean(data) + ext_prop.clean(1) + + +def test_extension_property_invalid2(): + ext_prop = ExtensionsProperty(spec_version='2.1', enclosing_type='file') + with pytest.raises(CustomContentError): + ext_prop.clean( + { + 'foobar-ext': { + 'pe_type': 'exe', + }, + }, + ) def test_extension_property_invalid_type(): ext_prop = ExtensionsProperty(spec_version='2.1', enclosing_type='indicator') - with pytest.raises(ValueError) as excinfo: + with pytest.raises(CustomContentError) as excinfo: ext_prop.clean( { 'windows-pebinary-ext': { diff --git a/stix2/test/v21/test_workbench.py b/stix2/test/v21/test_workbench.py index 0d84422..3a4a3fd 100644 --- a/stix2/test/v21/test_workbench.py +++ b/stix2/test/v21/test_workbench.py @@ -1,7 +1,5 @@ import os -import pytest - import stix2 from stix2.workbench import ( AttackPattern, Campaign, CourseOfAction, ExternalReference, @@ -24,7 +22,6 @@ from .constants import ( ) -@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') def test_workbench_environment(): # Create a STIX object @@ -79,7 +76,6 @@ def test_workbench_get_all_identities(): assert resp[0].id == IDENTITY_ID -@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') def test_workbench_get_all_indicators(): resp = indicators() assert len(resp) == 1 @@ -95,7 +91,6 @@ def test_workbench_get_all_intrusion_sets(): assert resp[0].id == INTRUSION_SET_ID -@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') def test_workbench_get_all_malware(): mal = Malware(id=MALWARE_ID, **MALWARE_KWARGS) save(mal) @@ -114,7 +109,6 @@ def test_workbench_get_all_observed_data(): assert resp[0].id == OBSERVED_DATA_ID -@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') def test_workbench_get_all_reports(): rep = Report(id=REPORT_ID, **REPORT_KWARGS) save(rep) @@ -124,7 +118,6 @@ def test_workbench_get_all_reports(): assert resp[0].id == REPORT_ID -@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') def test_workbench_get_all_threat_actors(): thr = ThreatActor(id=THREAT_ACTOR_ID, **THREAT_ACTOR_KWARGS) save(thr) @@ -134,7 +127,6 @@ def test_workbench_get_all_threat_actors(): assert resp[0].id == THREAT_ACTOR_ID -@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') def test_workbench_get_all_tools(): tool = Tool(id=TOOL_ID, **TOOL_KWARGS) save(tool) @@ -159,7 +151,6 @@ def test_workbench_add_to_bundle(): assert bundle.objects[0].name == 'Heartbleed' -@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') def test_workbench_relationships(): rel = Relationship(INDICATOR_ID, 'indicates', MALWARE_ID) save(rel) @@ -179,7 +170,6 @@ def test_workbench_created_by(): assert creator.id == IDENTITY_ID -@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') def test_workbench_related(): rel1 = Relationship(MALWARE_ID, 'targets', IDENTITY_ID) rel2 = Relationship(CAMPAIGN_ID, 'uses', MALWARE_ID) @@ -195,7 +185,6 @@ def test_workbench_related(): assert len(resp) == 1 -@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') def test_workbench_related_with_filters(): malware = Malware( malware_types=["ransomware"], name="CryptorBit", @@ -216,7 +205,6 @@ def test_workbench_related_with_filters(): assert len(resp) == 1 -@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') def test_add_data_source(): fs_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "stix2_data") fs = FileSystemSource(fs_path) diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py index 66000c1..490de39 100644 --- a/stix2/v21/sdo.py +++ b/stix2/v21/sdo.py @@ -7,7 +7,7 @@ from six.moves.urllib.parse import quote_plus from ..core import STIXDomainObject from ..custom import _custom_object_builder -from ..exceptions import InvalidPropertyConfigurationError +from ..exceptions import PropertyPresenceError from ..properties import ( BinaryProperty, BooleanProperty, EmbeddedObjectProperty, EnumProperty, FloatProperty, IDProperty, IntegerProperty, ListProperty, @@ -76,7 +76,7 @@ class Campaign(STIXDomainObject): ]) def _check_object_constraints(self): - super(self.__class__, self)._check_object_constraints() + super(Campaign, self)._check_object_constraints() first_seen = self.get('first_seen') last_seen = self.get('last_seen') @@ -215,7 +215,7 @@ class Indicator(STIXDomainObject): ]) def _check_object_constraints(self): - super(self.__class__, self)._check_object_constraints() + super(Indicator, self)._check_object_constraints() valid_from = self.get('valid_from') valid_until = self.get('valid_until') @@ -256,7 +256,7 @@ class Infrastructure(STIXDomainObject): ]) def _check_object_constraints(self): - super(self.__class__, self)._check_object_constraints() + super(Infrastructure, self)._check_object_constraints() first_seen = self.get('first_seen') last_seen = self.get('last_seen') @@ -299,7 +299,7 @@ class IntrusionSet(STIXDomainObject): ]) def _check_object_constraints(self): - super(self.__class__, self)._check_object_constraints() + super(IntrusionSet, self)._check_object_constraints() first_seen = self.get('first_seen') last_seen = self.get('last_seen') @@ -344,7 +344,7 @@ class Location(STIXDomainObject): ]) def _check_object_constraints(self): - super(self.__class__, self)._check_object_constraints() + super(Location, self)._check_object_constraints() if self.get('precision') is not None: self._check_properties_dependency(['longitude', 'latitude'], ['precision']) @@ -360,10 +360,10 @@ class Location(STIXDomainObject): and 'longitude' in self ) ): - raise InvalidPropertyConfigurationError( + raise PropertyPresenceError( "Location objects must have the properties 'region', " "'country', or 'latitude' and 'longitude'", - Location + Location, ) def to_maps_url(self, map_engine="Google Maps"): @@ -454,7 +454,7 @@ class Malware(STIXDomainObject): ]) def _check_object_constraints(self): - super(self.__class__, self)._check_object_constraints() + super(Malware, self)._check_object_constraints() first_seen = self.get('first_seen') last_seen = self.get('last_seen') @@ -464,9 +464,9 @@ class Malware(STIXDomainObject): raise ValueError(msg.format(self)) if self.is_family and "name" not in self: - raise InvalidPropertyConfigurationError( + raise PropertyPresenceError( "'name' is a required property for malware families", - Malware + Malware, ) @@ -576,7 +576,7 @@ class ObservedData(STIXDomainObject): super(ObservedData, self).__init__(*args, **kwargs) def _check_object_constraints(self): - super(self.__class__, self)._check_object_constraints() + super(ObservedData, self)._check_object_constraints() first_observed = self.get('first_observed') last_observed = self.get('last_observed') @@ -694,7 +694,7 @@ class ThreatActor(STIXDomainObject): ]) def _check_object_constraints(self): - super(self.__class__, self)._check_object_constraints() + super(ThreatActor, self)._check_object_constraints() first_observed = self.get('first_seen') last_observed = self.get('last_seen') diff --git a/stix2/workbench.py b/stix2/workbench.py index e621073..d43778f 100644 --- a/stix2/workbench.py +++ b/stix2/workbench.py @@ -20,6 +20,7 @@ """ +import functools import stix2 from . import AttackPattern as _AttackPattern from . import Campaign as _Campaign @@ -122,42 +123,35 @@ def _observed_data_init(self, *args, **kwargs): super(self.__class__, self).__init__(*args, **kwargs) -def _constructor_wrapper(obj_type): - # Use an intermediate wrapper class so the implicit environment will create objects that have our wrapper functions - class_dict = dict( - created_by=_created_by_wrapper, - relationships=_relationships_wrapper, - related=_related_wrapper, - **obj_type.__dict__ - ) - - # Avoid TypeError about super() in ObservedData - if 'ObservedData' in obj_type.__name__: - class_dict['__init__'] = _observed_data_init - - wrapped_type = type(obj_type.__name__, obj_type.__bases__, class_dict) - - @staticmethod - def new_constructor(cls, *args, **kwargs): - x = _environ.create(wrapped_type, *args, **kwargs) - return x - return new_constructor - - def _setup_workbench(): - # Create wrapper classes whose constructors call the implicit environment's create() for obj_type in STIX_OBJS: - new_class_dict = { - '__new__': _constructor_wrapper(obj_type), - '__doc__': 'Workbench wrapper around the `{0} `__ object. {1}'.format(obj_type.__name__, STIX_OBJ_DOCS), - } - new_class = type(obj_type.__name__, (), new_class_dict) - # Add our new class to this module's globals and to the library-wide mapping. - # This allows parse() to use the wrapped classes. - globals()[obj_type.__name__] = new_class - stix2.OBJ_MAP[obj_type._type] = new_class - new_class = None + # The idea here was originally to dynamically create subclasses which + # were cleverly customized such that instantiating them would actually + # invoke _environ.create(). This turns out to be impossible, since + # __new__ can never create the class in the normal way, since that + # invokes __new__ again, resulting in infinite recursion. And + # _environ.create() does exactly that. + # + # So instead, we create something "class-like", in that calling it + # produces an instance of the desired class. But these things will + # be functions instead of classes. One might think this trickery will + # have undesirable side-effects, but actually it seems to work. + # So far... + new_class_dict = { + '__doc__': 'Workbench wrapper around the `{0} `__ object. {1}'.format(obj_type.__name__, STIX_OBJ_DOCS), + 'created_by': _created_by_wrapper, + 'relationships': _relationships_wrapper, + 'related': _related_wrapper, + } + + new_class = type(obj_type.__name__, (obj_type,), new_class_dict) + factory_func = functools.partial(_environ.create, new_class) + + # Add our new "class" to this module's globals and to the library-wide + # mapping. This allows parse() to use the wrapped classes. + globals()[obj_type.__name__] = factory_func + stix2.OBJ_MAP[obj_type._type] = factory_func _setup_workbench() diff --git a/tox.ini b/tox.ini index f3a10fb..5deb4ef 100644 --- a/tox.ini +++ b/tox.ini @@ -11,9 +11,7 @@ deps = taxii2-client medallion commands = - pytest --ignore=stix2/test/v20/test_workbench.py --ignore=stix2/test/v21/test_workbench.py --cov=stix2 stix2/test/ --cov-report term-missing - pytest stix2/test/v20/test_workbench.py --cov=stix2 --cov-report term-missing --cov-append - pytest stix2/test/v21/test_workbench.py --cov=stix2 --cov-report term-missing --cov-append + pytest --cov=stix2 stix2/test/ --cov-report term-missing passenv = CI TRAVIS TRAVIS_*