From 5b8585b3922e53ca78986266cead61eb56f32220 Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Wed, 3 May 2017 12:14:09 -0400 Subject: [PATCH] added versioning test for embedded_object replaced VersioningError with RevokeError and UnmodifiablePropertyError added __deepcopy__ to base class to handle embedded_objects --- stix2/base.py | 26 +++++++++++++------------- stix2/exceptions.py | 27 +++++++++++++++++++++++---- stix2/test/test_versioning.py | 34 +++++++++++++++++++++++++++++++--- 3 files changed, 67 insertions(+), 20 deletions(-) diff --git a/stix2/base.py b/stix2/base.py index f387022..09b9c39 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -7,7 +7,7 @@ import json from .exceptions import ExtraFieldsError, ImmutableError, InvalidValueError, \ - MissingFieldsError, VersioningError + MissingFieldsError, RevokeError, UnmodifiablePropertyError from .utils import format_datetime, get_timestamp, NOW __all__ = ['STIXJSONEncoder', '_STIXBase'] @@ -102,32 +102,32 @@ class _STIXBase(collections.Mapping): return "{0}({1})".format(self.__class__.__name__, ", ".join(["{0!s}={1!r}".format(k, v) for k, v in props])) + def __deepcopy__(self, memo): + # Assumption: we can ignore the memo argument, because no object will ever contain the same sub-object multiple times. + new_inner = copy.deepcopy(self._inner, memo) + cls = type(self) + return cls(**new_inner) + # Versioning API def new_version(self, **kwargs): unchangable_properties = [] if self.revoked: - raise VersioningError("Cannot create a new version of a revoked object") + raise RevokeError("new_version") new_obj_inner = copy.deepcopy(self._inner) properties_to_change = kwargs.keys() - if "type" in properties_to_change: - unchangable_properties.append("type") - if "id" in properties_to_change: - unchangable_properties.append("id") - if "created" in properties_to_change: - unchangable_properties.append("created") - if "created_by_ref" in properties_to_change: - unchangable_properties.append("created_by_ref") + for prop in ["created", "created_by_ref", "id", "type"]: + if prop in properties_to_change: + unchangable_properties.append(prop) if unchangable_properties: - raise VersioningError("These properties cannot be changed when making a new version: " + ", ".join(unchangable_properties)) + raise UnmodifiablePropertyError(unchangable_properties) if 'modified' not in kwargs: kwargs['modified'] = get_timestamp() new_obj_inner.update(kwargs) - cls = type(self) return cls(**new_obj_inner) def revoke(self): if self.revoked: - raise VersioningError("Cannot revoke an already revoked object") + raise RevokeError("revoke") return self.new_version(revoked=True) diff --git a/stix2/exceptions.py b/stix2/exceptions.py index 9c96e03..c04ada4 100644 --- a/stix2/exceptions.py +++ b/stix2/exceptions.py @@ -51,8 +51,27 @@ class ImmutableError(STIXError, ValueError): super(ImmutableError, self).__init__("Cannot modify properties after creation.") -class VersioningError(STIXError, ValueError): - """Execption while using the Versioning API""" +class UnmodifiablePropertyError(STIXError, ValueError): + """Attempted to modify an unmodifiable property of object when creating a new version""" - def __init__(self, msg): - super(VersioningError, self).__init__(msg) + 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." diff --git a/stix2/test/test_versioning.py b/stix2/test/test_versioning.py index 296386e..cecaa89 100644 --- a/stix2/test/test_versioning.py +++ b/stix2/test/test_versioning.py @@ -23,6 +23,34 @@ def test_making_new_version(): assert campaign_v1.modified < campaign_v2.modified +def test_making_new_version_with_embedded_object(): + campaign_v1 = stix2.Campaign( + id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T20:03:00.000Z", + modified="2016-04-06T20:03:00.000Z", + name="Green Group Attacks Against Finance", + external_references=[{ + "source_name": "capec", + "external_id": "CAPEC-163" + }], + description="Campaign by Green Group against a series of targets in the financial services sector." + ) + + campaign_v2 = campaign_v1.new_version(external_references=[{ + "source_name": "capec", + "external_id": "CAPEC-164" + }]) + + assert campaign_v1.id == campaign_v2.id + assert campaign_v1.created_by_ref == campaign_v2.created_by_ref + assert campaign_v1.created == campaign_v2.created + assert campaign_v1.name == campaign_v2.name + assert campaign_v1.description == campaign_v2.description + assert campaign_v1.modified < campaign_v2.modified + assert campaign_v1.external_references[0].external_id != campaign_v2.external_references[0].external_id + + def test_revoke(): campaign_v1 = stix2.Campaign( id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", @@ -55,7 +83,7 @@ def test_versioning_error_invalid_property(): description="Campaign by Green Group against a series of targets in the financial services sector." ) - with pytest.raises(stix2.exceptions.VersioningError) as excinfo: + with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as excinfo: campaign_v1.new_version(type="threat-actor") str(excinfo.value) == "These properties cannot be changed when making a new version: type" @@ -73,7 +101,7 @@ def test_versioning_error_new_version_of_revoked(): campaign_v2 = campaign_v1.revoke() - with pytest.raises(stix2.exceptions.VersioningError) as excinfo: + with pytest.raises(stix2.exceptions.RevokeError) as excinfo: campaign_v2.new_version(name="barney") str(excinfo.value) == "Cannot create a new version of a revoked object" @@ -91,7 +119,7 @@ def test_versioning_error_revoke_of_revoked(): campaign_v2 = campaign_v1.revoke() - with pytest.raises(stix2.exceptions.VersioningError) as excinfo: + with pytest.raises(stix2.exceptions.RevokeError) as excinfo: campaign_v2.revoke() str(excinfo.value) == "Cannot revoke an already revoked object"