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_*