From 83997b1135f5828f26fb1d84a1ec598b588b665b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Vinot?= Date: Tue, 18 Apr 2017 10:48:04 +0200 Subject: [PATCH 01/11] Add code coverage report --- .travis.yml | 4 ++++ README.md | 3 +++ tox.ini | 22 ++++++++++++++++------ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index ff4730e..1937814 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,16 @@ sudo: false language: python +cache: pip python: - "2.6" - "2.7" - "3.3" - "3.4" - "3.5" + - "3.5-dev" - "3.6" + - "3.6-dev" + - "nightly" install: - pip install -U pip setuptools - pip install tox-travis pre-commit diff --git a/README.md b/README.md index 0f9a7cf..d209911 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +[![Build Status](https://travis-ci.org/oasis-open/cti-python-stix2.svg?branch=master)](https://travis-ci.org/oasis-open/cti-python-stix2) +[![codecov](https://codecov.io/gh/oasis-open/cti-python-stix2/branch/master/graph/badge.svg)](https://codecov.io/gh/oasis-open/cti-python-stix2) + # cti-python-stix2 *This is an [OASIS Open Repository](https://www.oasis-open.org/resources/open-repositories/). See the [Governance](#governance) section for more information.* diff --git a/tox.ini b/tox.ini index fd580f9..0b0a773 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,18 @@ envlist = py26,py27,py33,py34,py35,py36,pycodestyle [testenv] -deps = pytest -commands = pytest +deps = + -U + tox + pytest + pytest-cov + coverage + codecov +commands = + py.test --cov=stix2 stix2/test/ + codecov + +passenv = CI TRAVIS TRAVIS_* [testenv:pycodestyle] deps = @@ -24,7 +34,7 @@ max-line-length=160 python = 2.6: py26 2.7: py27, pycodestyle - 3.3: py33 - 3.4: py34 - 3.5: py35 - 3.6: py36 + 3.3: py33, pycodestyle + 3.4: py34, pycodestyle + 3.5: py35, pycodestyle + 3.6: py36, pycodestyle From 4efe5357b19bf53c353c2c397fe2058f4384c1f1 Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Tue, 2 May 2017 14:06:42 -0400 Subject: [PATCH 02/11] Added versioning api, with tests --- stix2/base.py | 33 ++++++++++- stix2/exceptions.py | 7 +++ stix2/test/test_versioning.py | 107 ++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 stix2/test/test_versioning.py diff --git a/stix2/base.py b/stix2/base.py index 05bf545..4fbd1c6 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -3,9 +3,10 @@ import collections import datetime as dt import json +import copy from .exceptions import ExtraFieldsError, ImmutableError, InvalidValueError, \ - MissingFieldsError + MissingFieldsError, VersioningError from .utils import format_datetime, get_timestamp, NOW __all__ = ['STIXJSONEncoder', '_STIXBase'] @@ -99,3 +100,33 @@ class _STIXBase(collections.Mapping): props = [(k, self[k]) for k in sorted(self._properties) if self.get(k)] return "{0}({1})".format(self.__class__.__name__, ", ".join(["{0!s}={1!r}".format(k, v) for k, v in props])) + +# Versioning API + + def new_version(self, **kwargs): + unchangable_properties = [] + if self.revoked: + raise VersioningError("Cannot create a new version of a revoked object") + 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") + if unchangable_properties: + raise VersioningError("These properties cannot be changed when making a new version: " + ", ".join(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") + return self.new_version(revoked=True) diff --git a/stix2/exceptions.py b/stix2/exceptions.py index 2606796..7e7f39e 100644 --- a/stix2/exceptions.py +++ b/stix2/exceptions.py @@ -49,3 +49,10 @@ class ImmutableError(STIXError, ValueError): def __init__(self): super(ImmutableError, self).__init__("Cannot modify properties after creation.") + + +class VersioningError(STIXError, ValueError): + """Execption while using the Versioning API""" + + def __init__(self, msg): + super(VersioningError, self).__init__(msg) \ No newline at end of file diff --git a/stix2/test/test_versioning.py b/stix2/test/test_versioning.py new file mode 100644 index 0000000..ec51521 --- /dev/null +++ b/stix2/test/test_versioning.py @@ -0,0 +1,107 @@ +import stix2 +import pytest + +EXPECTED = """{ + "created": "2016-04-06T20:03:00.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "Campaign by Green Group against a series of targets in the financial services sector.", + "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "modified": "2016-04-06T20:03:00.000Z", + "name": "Green Group Attacks Against Finance", + "type": "campaign" +}""" + + +def test_making_new_version(): + 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", + description="Campaign by Green Group against a series of targets in the financial services sector." + ) + + campaign_v2 = campaign_v1.new_version(name="fred") + + 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_v2.name == "fred" + assert campaign_v1.description == campaign_v2.description + assert campaign_v1.modified < campaign_v2.modified + + +def test_revoke(): + 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", + description="Campaign by Green Group against a series of targets in the financial services sector." + ) + + campaign_v2 = campaign_v1.revoke() + + 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_v2.revoked + + +def test_versioning_error_invalid_property(): + 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", + description="Campaign by Green Group against a series of targets in the financial services sector." + ) + + with pytest.raises(stix2.exceptions.VersioningError) as excinfo: + campaign_v2 = campaign_v1.new_version(type="threat-actor") + + str(excinfo.value) == "These properties cannot be changed when making a new version: type" + + +def test_versioning_error_new_version_of_revoked(): + 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", + description="Campaign by Green Group against a series of targets in the financial services sector." + ) + + campaign_v2 = campaign_v1.revoke() + + with pytest.raises(stix2.exceptions.VersioningError) as excinfo: + campaign_v3 = campaign_v2.new_version(name="barney") + + str(excinfo.value) == "Cannot create a new version of a revoked object" + + +def test_versioning_error_revoke_of_revoked(): + 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", + description="Campaign by Green Group against a series of targets in the financial services sector." + ) + + campaign_v2 = campaign_v1.revoke() + + with pytest.raises(stix2.exceptions.VersioningError) as excinfo: + campaign_v3 = campaign_v2.revoke() + + str(excinfo.value) == "Cannot revoke an already revoked object" \ No newline at end of file From a70fc2c9528961ae64be981d11e89f62770882ae Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Tue, 2 May 2017 14:17:26 -0400 Subject: [PATCH 03/11] Added versioning api, with tests --- stix2/exceptions.py | 2 +- stix2/test/test_versioning.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/stix2/exceptions.py b/stix2/exceptions.py index 7e7f39e..9c96e03 100644 --- a/stix2/exceptions.py +++ b/stix2/exceptions.py @@ -55,4 +55,4 @@ class VersioningError(STIXError, ValueError): """Execption while using the Versioning API""" def __init__(self, msg): - super(VersioningError, self).__init__(msg) \ No newline at end of file + super(VersioningError, self).__init__(msg) diff --git a/stix2/test/test_versioning.py b/stix2/test/test_versioning.py index ec51521..b369251 100644 --- a/stix2/test/test_versioning.py +++ b/stix2/test/test_versioning.py @@ -66,7 +66,7 @@ def test_versioning_error_invalid_property(): ) with pytest.raises(stix2.exceptions.VersioningError) as excinfo: - campaign_v2 = campaign_v1.new_version(type="threat-actor") + campaign_v2 = campaign_v1.new_version(type="threat-actor") # noqa str(excinfo.value) == "These properties cannot be changed when making a new version: type" @@ -84,7 +84,7 @@ def test_versioning_error_new_version_of_revoked(): campaign_v2 = campaign_v1.revoke() with pytest.raises(stix2.exceptions.VersioningError) as excinfo: - campaign_v3 = campaign_v2.new_version(name="barney") + campaign_v3 = campaign_v2.new_version(name="barney") # noqa str(excinfo.value) == "Cannot create a new version of a revoked object" @@ -102,6 +102,6 @@ def test_versioning_error_revoke_of_revoked(): campaign_v2 = campaign_v1.revoke() with pytest.raises(stix2.exceptions.VersioningError) as excinfo: - campaign_v3 = campaign_v2.revoke() + campaign_v3 = campaign_v2.revoke() # noqa - str(excinfo.value) == "Cannot revoke an already revoked object" \ No newline at end of file + str(excinfo.value) == "Cannot revoke an already revoked object" From 411c087fc175db6b3de4ffddb6469c9ecae3abd5 Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Tue, 2 May 2017 14:25:01 -0400 Subject: [PATCH 04/11] import style errors --- stix2/base.py | 3 ++- stix2/test/test_versioning.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/stix2/base.py b/stix2/base.py index 4fbd1c6..d4d63d8 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -2,8 +2,9 @@ import collections import datetime as dt -import json import copy +import json + from .exceptions import ExtraFieldsError, ImmutableError, InvalidValueError, \ MissingFieldsError, VersioningError diff --git a/stix2/test/test_versioning.py b/stix2/test/test_versioning.py index b369251..d63f0d7 100644 --- a/stix2/test/test_versioning.py +++ b/stix2/test/test_versioning.py @@ -1,5 +1,6 @@ -import stix2 import pytest +import stix2 + EXPECTED = """{ "created": "2016-04-06T20:03:00.000Z", From cda2b476918ddb6f10f95d48d575ef8050a9d3e8 Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Tue, 2 May 2017 14:29:59 -0400 Subject: [PATCH 05/11] import style errors 2 --- stix2/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stix2/base.py b/stix2/base.py index d4d63d8..f387022 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -1,8 +1,8 @@ """Base class for type definitions in the stix2 library.""" import collections -import datetime as dt import copy +import datetime as dt import json From c2d628db502afc79087a2b4a01afd1dc36fe96f9 Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Tue, 2 May 2017 15:53:07 -0400 Subject: [PATCH 06/11] import style errors 2 --- stix2/test/test_versioning.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/stix2/test/test_versioning.py b/stix2/test/test_versioning.py index d63f0d7..296386e 100644 --- a/stix2/test/test_versioning.py +++ b/stix2/test/test_versioning.py @@ -2,17 +2,6 @@ import pytest import stix2 -EXPECTED = """{ - "created": "2016-04-06T20:03:00.000Z", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "description": "Campaign by Green Group against a series of targets in the financial services sector.", - "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "modified": "2016-04-06T20:03:00.000Z", - "name": "Green Group Attacks Against Finance", - "type": "campaign" -}""" - - def test_making_new_version(): campaign_v1 = stix2.Campaign( id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", @@ -67,7 +56,7 @@ def test_versioning_error_invalid_property(): ) with pytest.raises(stix2.exceptions.VersioningError) as excinfo: - campaign_v2 = campaign_v1.new_version(type="threat-actor") # noqa + campaign_v1.new_version(type="threat-actor") str(excinfo.value) == "These properties cannot be changed when making a new version: type" @@ -85,7 +74,7 @@ def test_versioning_error_new_version_of_revoked(): campaign_v2 = campaign_v1.revoke() with pytest.raises(stix2.exceptions.VersioningError) as excinfo: - campaign_v3 = campaign_v2.new_version(name="barney") # noqa + campaign_v2.new_version(name="barney") str(excinfo.value) == "Cannot create a new version of a revoked object" @@ -103,6 +92,6 @@ def test_versioning_error_revoke_of_revoked(): campaign_v2 = campaign_v1.revoke() with pytest.raises(stix2.exceptions.VersioningError) as excinfo: - campaign_v3 = campaign_v2.revoke() # noqa + campaign_v2.revoke() str(excinfo.value) == "Cannot revoke an already revoked object" From 5b8585b3922e53ca78986266cead61eb56f32220 Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Wed, 3 May 2017 12:14:09 -0400 Subject: [PATCH 07/11] 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" From 200bb8556f52b2dcdd8ff24ba4b4546cf7e38835 Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Thu, 4 May 2017 16:34:08 -0400 Subject: [PATCH 08/11] added unsetting capability cleaned up MissingFieldsError tests error when new modified property is earlier than current modified property --- stix2/base.py | 21 ++++++--- stix2/exceptions.py | 2 +- stix2/test/constants.py | 3 ++ stix2/test/test_external_reference.py | 1 - stix2/test/test_indicator.py | 3 +- stix2/test/test_kill_chain_phases.py | 3 -- stix2/test/test_malware.py | 2 - stix2/test/test_relationship.py | 8 ++-- stix2/test/test_versioning.py | 61 +++++++++++++++++++++++++-- stix2/utils.py | 23 ++++++++++ 10 files changed, 107 insertions(+), 20 deletions(-) diff --git a/stix2/base.py b/stix2/base.py index 09b9c39..1c90dab 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -3,12 +3,13 @@ import collections import copy import datetime as dt + import json from .exceptions import ExtraFieldsError, ImmutableError, InvalidValueError, \ MissingFieldsError, RevokeError, UnmodifiablePropertyError -from .utils import format_datetime, get_timestamp, NOW +from .utils import format_datetime, get_timestamp, NOW, parse_into_datetime __all__ = ['STIXJSONEncoder', '_STIXBase'] @@ -58,16 +59,22 @@ class _STIXBase(collections.Mapping): if extra_kwargs: raise ExtraFieldsError(cls, extra_kwargs) + # Remove any keyword arguments whose value is None + setting_kwargs = {} + for prop_name, prop_value in kwargs.items(): + if prop_value: + setting_kwargs[prop_name] = prop_value + # Detect any missing required fields required_fields = get_required_properties(cls._properties) - missing_kwargs = set(required_fields) - set(kwargs) + missing_kwargs = set(required_fields) - set(setting_kwargs) if missing_kwargs: raise MissingFieldsError(cls, missing_kwargs) for prop_name, prop_metadata in cls._properties.items(): - self._check_property(prop_name, prop_metadata, kwargs) + self._check_property(prop_name, prop_metadata, setting_kwargs) - self._inner = kwargs + self._inner = setting_kwargs if self.granular_markings: for m in self.granular_markings: @@ -121,10 +128,14 @@ class _STIXBase(collections.Mapping): unchangable_properties.append(prop) if unchangable_properties: raise UnmodifiablePropertyError(unchangable_properties) + cls = type(self) if 'modified' not in kwargs: kwargs['modified'] = get_timestamp() + else: + new_modified_property = parse_into_datetime(kwargs['modified']) + if new_modified_property < self.modified: + raise InvalidValueError(cls, 'modified', "The new modified datetime cannot be before the current modified datatime.") new_obj_inner.update(kwargs) - cls = type(self) return cls(**new_obj_inner) def revoke(self): diff --git a/stix2/exceptions.py b/stix2/exceptions.py index c04ada4..20e44a7 100644 --- a/stix2/exceptions.py +++ b/stix2/exceptions.py @@ -25,7 +25,7 @@ class MissingFieldsError(STIXError, ValueError): self.fields = sorted(list(fields)) def __str__(self): - msg = "Missing required field(s) for {0}: ({1})." + msg = "No values for required field(s) for {0}: ({1})." return msg.format(self.cls.__name__, ", ".join(x for x in self.fields)) diff --git a/stix2/test/constants.py b/stix2/test/constants.py index 1c8ae2b..5ef599e 100644 --- a/stix2/test/constants.py +++ b/stix2/test/constants.py @@ -39,6 +39,9 @@ RELATIONSHIP_KWARGS = dict( target_ref=MALWARE_ID, ) +# Minimum required args for a Sighting instance SIGHTING_KWARGS = dict( sighting_of_ref=INDICATOR_ID, ) + + diff --git a/stix2/test/test_external_reference.py b/stix2/test/test_external_reference.py index f8d9b66..1e25fd2 100644 --- a/stix2/test/test_external_reference.py +++ b/stix2/test/test_external_reference.py @@ -113,4 +113,3 @@ def test_external_reference_source_required(): assert excinfo.value.cls == stix2.ExternalReference assert excinfo.value.fields == ["source_name"] - assert str(excinfo.value) == "Missing required field(s) for ExternalReference: (source_name)." diff --git a/stix2/test/test_indicator.py b/stix2/test/test_indicator.py index 1c2b610..baf4b38 100644 --- a/stix2/test/test_indicator.py +++ b/stix2/test/test_indicator.py @@ -93,7 +93,7 @@ def test_indicator_required_fields(): assert excinfo.value.cls == stix2.Indicator assert excinfo.value.fields == ["labels", "pattern"] - assert str(excinfo.value) == "Missing required field(s) for Indicator: (labels, pattern)." + assert str(excinfo.value) == "No values for required field(s) for Indicator: (labels, pattern)." def test_indicator_required_field_pattern(): @@ -102,7 +102,6 @@ def test_indicator_required_field_pattern(): assert excinfo.value.cls == stix2.Indicator assert excinfo.value.fields == ["pattern"] - assert str(excinfo.value) == "Missing required field(s) for Indicator: (pattern)." def test_indicator_created_ref_invalid_format(): diff --git a/stix2/test/test_kill_chain_phases.py b/stix2/test/test_kill_chain_phases.py index d2ecc1f..037e930 100644 --- a/stix2/test/test_kill_chain_phases.py +++ b/stix2/test/test_kill_chain_phases.py @@ -41,7 +41,6 @@ def test_kill_chain_required_fields(): assert excinfo.value.cls == stix2.KillChainPhase assert excinfo.value.fields == ["kill_chain_name", "phase_name"] - assert str(excinfo.value) == "Missing required field(s) for KillChainPhase: (kill_chain_name, phase_name)." def test_kill_chain_required_field_chain_name(): @@ -51,7 +50,6 @@ def test_kill_chain_required_field_chain_name(): assert excinfo.value.cls == stix2.KillChainPhase assert excinfo.value.fields == ["kill_chain_name"] - assert str(excinfo.value) == "Missing required field(s) for KillChainPhase: (kill_chain_name)." def test_kill_chain_required_field_phase_name(): @@ -61,4 +59,3 @@ def test_kill_chain_required_field_phase_name(): assert excinfo.value.cls == stix2.KillChainPhase assert excinfo.value.fields == ["phase_name"] - assert str(excinfo.value) == "Missing required field(s) for KillChainPhase: (phase_name)." diff --git a/stix2/test/test_malware.py b/stix2/test/test_malware.py index edc35b6..f3bfccd 100644 --- a/stix2/test/test_malware.py +++ b/stix2/test/test_malware.py @@ -76,7 +76,6 @@ def test_malware_required_fields(): assert excinfo.value.cls == stix2.Malware assert excinfo.value.fields == ["labels", "name"] - assert str(excinfo.value) == "Missing required field(s) for Malware: (labels, name)." def test_malware_required_field_name(): @@ -85,7 +84,6 @@ def test_malware_required_field_name(): assert excinfo.value.cls == stix2.Malware assert excinfo.value.fields == ["name"] - assert str(excinfo.value) == "Missing required field(s) for Malware: (name)." def test_cannot_assign_to_malware_attributes(malware): diff --git a/stix2/test/test_relationship.py b/stix2/test/test_relationship.py index 72e659c..869107d 100644 --- a/stix2/test/test_relationship.py +++ b/stix2/test/test_relationship.py @@ -76,13 +76,16 @@ def test_relationship_id_must_start_with_relationship(): def test_relationship_required_field_relationship_type(): with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo: stix2.Relationship() - assert str(excinfo.value) == "Missing required field(s) for Relationship: (relationship_type, source_ref, target_ref)." + assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.fields == ["relationship_type", "source_ref", "target_ref"] def test_relationship_missing_some_required_fields(): with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo: stix2.Relationship(relationship_type='indicates') - assert str(excinfo.value) == "Missing required field(s) for Relationship: (source_ref, target_ref)." + + assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.fields == ["source_ref", "target_ref"] def test_relationship_required_field_target_ref(): @@ -94,7 +97,6 @@ def test_relationship_required_field_target_ref(): assert excinfo.value.cls == stix2.Relationship assert excinfo.value.fields == ["target_ref"] - assert str(excinfo.value) == "Missing required field(s) for Relationship: (target_ref)." def test_cannot_assign_to_relationship_attributes(relationship): diff --git a/stix2/test/test_versioning.py b/stix2/test/test_versioning.py index cecaa89..0e51abc 100644 --- a/stix2/test/test_versioning.py +++ b/stix2/test/test_versioning.py @@ -23,6 +23,26 @@ def test_making_new_version(): assert campaign_v1.modified < campaign_v2.modified +def test_making_new_version_with_unset(): + 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", + description="Campaign by Green Group against a series of targets in the financial services sector." + ) + + campaign_v2 = campaign_v1.new_version(description=None) + + 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_v2.description == None + 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", @@ -86,7 +106,42 @@ def test_versioning_error_invalid_property(): 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" + assert str(excinfo.value) == "These properties cannot be changed when making a new version: type." + + +def test_versioning_error_bad_modified_value(): + 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", + description="Campaign by Green Group against a series of targets in the financial services sector." + ) + + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + campaign_v1.new_version(modified="2015-04-06T20:03:00.000Z") + + assert excinfo.value.cls == stix2.Campaign + assert excinfo.value.prop_name == "modified" + assert excinfo.value.reason == "The new modified datetime cannot be before the current modified datatime." + + +def test_versioning_error_usetting_required_property(): + 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", + description="Campaign by Green Group against a series of targets in the financial services sector." + ) + + with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo: + campaign_v1.new_version(name=None) + + assert excinfo.value.cls == stix2.Campaign + assert excinfo.value.fields == [ "name" ] def test_versioning_error_new_version_of_revoked(): @@ -104,7 +159,7 @@ def test_versioning_error_new_version_of_revoked(): 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" + assert excinfo.value.called_by == "new_version" def test_versioning_error_revoke_of_revoked(): @@ -122,4 +177,4 @@ def test_versioning_error_revoke_of_revoked(): with pytest.raises(stix2.exceptions.RevokeError) as excinfo: campaign_v2.revoke() - str(excinfo.value) == "Cannot revoke an already revoked object" + assert excinfo.value.called_by == "revoke" diff --git a/stix2/utils.py b/stix2/utils.py index cdc7f54..5feffae 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -1,6 +1,7 @@ """Utility functions and classes for the stix2 library.""" import datetime as dt +from dateutil import parser import json import pytz @@ -34,6 +35,28 @@ def format_datetime(dttm): return ts + "Z" +def parse_into_datetime(value): + if isinstance(value, dt.date): + if hasattr(value, 'hour'): + return value + else: + # Add a time component + return dt.datetime.combine(value, dt.time(), tzinfo=pytz.utc) + + # value isn't a date or datetime object so assume it's a string + try: + parsed = parser.parse(value) + except TypeError: + # Unknown format + raise ValueError("must be a datetime object, date object, or " + "timestamp string in a recognizable format.") + if parsed.tzinfo: + return parsed.astimezone(pytz.utc) + else: + # Doesn't have timezone info in the string; assume UTC + return pytz.utc.localize(parsed) + + def get_dict(data): """Return data as a dictionary. Input can be a dictionary, string, or file-like object. From c9320ad895f374fbe328e7022619751eccd113b7 Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Thu, 4 May 2017 16:41:37 -0400 Subject: [PATCH 09/11] style errors --- stix2/test/constants.py | 2 -- stix2/test/test_versioning.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/stix2/test/constants.py b/stix2/test/constants.py index 5ef599e..b631d08 100644 --- a/stix2/test/constants.py +++ b/stix2/test/constants.py @@ -43,5 +43,3 @@ RELATIONSHIP_KWARGS = dict( SIGHTING_KWARGS = dict( sighting_of_ref=INDICATOR_ID, ) - - diff --git a/stix2/test/test_versioning.py b/stix2/test/test_versioning.py index 0e51abc..4663263 100644 --- a/stix2/test/test_versioning.py +++ b/stix2/test/test_versioning.py @@ -39,7 +39,7 @@ def test_making_new_version_with_unset(): 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_v2.description == None + assert campaign_v2.description is None assert campaign_v1.modified < campaign_v2.modified @@ -141,7 +141,7 @@ def test_versioning_error_usetting_required_property(): campaign_v1.new_version(name=None) assert excinfo.value.cls == stix2.Campaign - assert excinfo.value.fields == [ "name" ] + assert excinfo.value.fields == ["name"] def test_versioning_error_new_version_of_revoked(): From ddecd529042e55ae4e95451386ada69e543695cd Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Thu, 4 May 2017 19:30:09 -0400 Subject: [PATCH 10/11] style errors 2 --- stix2/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stix2/utils.py b/stix2/utils.py index 5feffae..3f0e0aa 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -1,9 +1,8 @@ """Utility functions and classes for the stix2 library.""" +import json import datetime as dt from dateutil import parser -import json - import pytz # Sentinel value for fields that should be set to the current time. From 1e7e87ef440e6338f5989825298810c8d5cd220a Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Fri, 5 May 2017 10:53:28 -0400 Subject: [PATCH 11/11] style errors 3 --- stix2/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stix2/utils.py b/stix2/utils.py index 3f0e0aa..a2493e2 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -1,7 +1,8 @@ """Utility functions and classes for the stix2 library.""" -import json import datetime as dt +import json + from dateutil import parser import pytz