Modify versioning API to work on dictionaries
This includes new_version() and revoke().stix2.1
parent
095c5066d5
commit
15287959a4
|
@ -9,10 +9,11 @@ from .exceptions import (AtLeastOnePropertyError, DependentPropertiesError,
|
|||
ExtraPropertiesError, ImmutableError,
|
||||
InvalidObjRefError, InvalidValueError,
|
||||
MissingPropertiesError,
|
||||
MutuallyExclusivePropertiesError, RevokeError,
|
||||
UnmodifiablePropertyError)
|
||||
MutuallyExclusivePropertiesError)
|
||||
from .markings.utils import validate
|
||||
from .utils import NOW, format_datetime, get_timestamp, parse_into_datetime
|
||||
from .utils import NOW, format_datetime, get_timestamp
|
||||
from .utils import new_version as _new_version
|
||||
from .utils import revoke as _revoke
|
||||
|
||||
__all__ = ['STIXJSONEncoder', '_STIXBase']
|
||||
|
||||
|
@ -162,30 +163,10 @@ class _STIXBase(collections.Mapping):
|
|||
# Versioning API
|
||||
|
||||
def new_version(self, **kwargs):
|
||||
unchangable_properties = []
|
||||
if self.get("revoked"):
|
||||
raise RevokeError("new_version")
|
||||
new_obj_inner = copy.deepcopy(self._inner)
|
||||
properties_to_change = kwargs.keys()
|
||||
for prop in ["created", "created_by_ref", "id", "type"]:
|
||||
if prop in properties_to_change:
|
||||
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'], precision='millisecond')
|
||||
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)
|
||||
return cls(**new_obj_inner)
|
||||
return _new_version(self, **kwargs)
|
||||
|
||||
def revoke(self):
|
||||
if self.get("revoked"):
|
||||
raise RevokeError("revoke")
|
||||
return self.new_version(revoked=True)
|
||||
return _revoke(self)
|
||||
|
||||
|
||||
class _Observable(_STIXBase):
|
||||
|
|
|
@ -29,6 +29,16 @@ MARKING_IDS = [
|
|||
"marking-definition--2802dfb1-1019-40a8-8848-68d0ec0e417f",
|
||||
]
|
||||
|
||||
# All required args for a Campaign instance, plus some optional args
|
||||
CAMPAIGN_MORE_KWARGS = dict(
|
||||
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.",
|
||||
)
|
||||
|
||||
# Minimum required args for an Identity instance
|
||||
IDENTITY_KWARGS = dict(
|
||||
name="John Smith",
|
||||
|
|
|
@ -2,16 +2,11 @@ import pytest
|
|||
|
||||
import stix2
|
||||
|
||||
from .constants import CAMPAIGN_MORE_KWARGS
|
||||
|
||||
|
||||
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_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS)
|
||||
|
||||
campaign_v2 = campaign_v1.new_version(name="fred")
|
||||
|
||||
|
@ -25,14 +20,7 @@ def test_making_new_version():
|
|||
|
||||
|
||||
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_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS)
|
||||
|
||||
campaign_v2 = campaign_v1.new_version(description=None)
|
||||
|
||||
|
@ -47,16 +35,11 @@ def test_making_new_version_with_unset():
|
|||
|
||||
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_MORE_KWARGS
|
||||
)
|
||||
|
||||
campaign_v2 = campaign_v1.new_version(external_references=[{
|
||||
|
@ -74,14 +57,7 @@ def test_making_new_version_with_embedded_object():
|
|||
|
||||
|
||||
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_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS)
|
||||
|
||||
campaign_v2 = campaign_v1.revoke()
|
||||
|
||||
|
@ -96,14 +72,7 @@ def test_revoke():
|
|||
|
||||
|
||||
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."
|
||||
)
|
||||
campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS)
|
||||
|
||||
with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as excinfo:
|
||||
campaign_v1.new_version(type="threat-actor")
|
||||
|
@ -112,14 +81,7 @@ def test_versioning_error_invalid_property():
|
|||
|
||||
|
||||
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."
|
||||
)
|
||||
campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS)
|
||||
|
||||
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
|
||||
campaign_v1.new_version(modified="2015-04-06T20:03:00.000Z")
|
||||
|
@ -130,14 +92,7 @@ def test_versioning_error_bad_modified_value():
|
|||
|
||||
|
||||
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."
|
||||
)
|
||||
campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS)
|
||||
|
||||
with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo:
|
||||
campaign_v1.new_version(name=None)
|
||||
|
@ -147,15 +102,7 @@ def test_versioning_error_usetting_required_property():
|
|||
|
||||
|
||||
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_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS)
|
||||
campaign_v2 = campaign_v1.revoke()
|
||||
|
||||
with pytest.raises(stix2.exceptions.RevokeError) as excinfo:
|
||||
|
@ -165,18 +112,84 @@ def test_versioning_error_new_version_of_revoked():
|
|||
|
||||
|
||||
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_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS)
|
||||
campaign_v2 = campaign_v1.revoke()
|
||||
|
||||
with pytest.raises(stix2.exceptions.RevokeError) as excinfo:
|
||||
campaign_v2.revoke()
|
||||
|
||||
assert excinfo.value.called_by == "revoke"
|
||||
|
||||
|
||||
def test_making_new_version_dict():
|
||||
campaign_v1 = CAMPAIGN_MORE_KWARGS
|
||||
campaign_v2 = stix2.utils.new_version(CAMPAIGN_MORE_KWARGS, 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 stix2.utils.parse_into_datetime(campaign_v1['modified'], precision='millisecond') < campaign_v2['modified']
|
||||
|
||||
|
||||
def test_versioning_error_dict_bad_modified_value():
|
||||
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
|
||||
stix2.utils.new_version(CAMPAIGN_MORE_KWARGS, modified="2015-04-06T20:03:00.000Z")
|
||||
|
||||
assert excinfo.value.cls == dict
|
||||
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_dict_no_modified_value():
|
||||
campaign_v1 = {
|
||||
'type': 'campaign',
|
||||
'id': "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
|
||||
'created': "2016-04-06T20:03:00.000Z",
|
||||
'name': "Green Group Attacks Against Finance",
|
||||
}
|
||||
campaign_v2 = stix2.utils.new_version(campaign_v1, modified="2017-04-06T20:03:00.000Z")
|
||||
|
||||
assert str(campaign_v2['modified']) == "2017-04-06T20:03:00.000Z"
|
||||
|
||||
|
||||
def test_making_new_version_invalid_cls():
|
||||
campaign_v1 = "This is a campaign."
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.utils.new_version(campaign_v1, name="fred")
|
||||
|
||||
assert 'cannot create new version of object of this type' in str(excinfo.value)
|
||||
|
||||
|
||||
def test_revoke_dict():
|
||||
campaign_v1 = CAMPAIGN_MORE_KWARGS
|
||||
campaign_v2 = stix2.utils.revoke(campaign_v1)
|
||||
|
||||
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 stix2.utils.parse_into_datetime(campaign_v1['modified'], precision='millisecond') < campaign_v2['modified']
|
||||
|
||||
assert campaign_v2['revoked']
|
||||
|
||||
|
||||
def test_versioning_error_revoke_of_revoked_dict():
|
||||
campaign_v1 = CAMPAIGN_MORE_KWARGS
|
||||
campaign_v2 = stix2.utils.revoke(campaign_v1)
|
||||
|
||||
with pytest.raises(stix2.exceptions.RevokeError) as excinfo:
|
||||
stix2.utils.revoke(campaign_v2)
|
||||
|
||||
assert excinfo.value.called_by == "revoke"
|
||||
|
||||
|
||||
def test_revoke_invalid_cls():
|
||||
campaign_v1 = "This is a campaign."
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.utils.revoke(campaign_v1)
|
||||
|
||||
assert 'cannot revoke object of this type' in str(excinfo.value)
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
"""Utility functions and classes for the stix2 library."""
|
||||
|
||||
from collections import Mapping
|
||||
import copy
|
||||
import datetime as dt
|
||||
import json
|
||||
|
||||
from dateutil import parser
|
||||
import pytz
|
||||
|
||||
from .exceptions import (InvalidValueError, RevokeError,
|
||||
UnmodifiablePropertyError)
|
||||
|
||||
# Sentinel value for properties that should be set to the current time.
|
||||
# We can't use the standard 'default' approach, since if there are multiple
|
||||
# timestamps in a single object, the timestamps will vary by a few microseconds.
|
||||
|
@ -112,3 +117,50 @@ def get_dict(data):
|
|||
return dict(data)
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError("Cannot convert '%s' to dictionary." % str(data))
|
||||
|
||||
|
||||
def new_version(data, **kwargs):
|
||||
"""Create a new version of a STIX object, by modifying properties and
|
||||
updating the `modified` property.
|
||||
"""
|
||||
|
||||
if not isinstance(data, Mapping):
|
||||
raise ValueError('cannot create new version of object of this type! '
|
||||
'Try a dictionary or instance of an SDO or SRO class.')
|
||||
|
||||
unchangable_properties = []
|
||||
if data.get("revoked"):
|
||||
raise RevokeError("new_version")
|
||||
try:
|
||||
new_obj_inner = copy.deepcopy(data._inner)
|
||||
except AttributeError:
|
||||
new_obj_inner = copy.deepcopy(data)
|
||||
properties_to_change = kwargs.keys()
|
||||
|
||||
# Make sure certain properties aren't trying to change
|
||||
for prop in ["created", "created_by_ref", "id", "type"]:
|
||||
if prop in properties_to_change:
|
||||
unchangable_properties.append(prop)
|
||||
if unchangable_properties:
|
||||
raise UnmodifiablePropertyError(unchangable_properties)
|
||||
|
||||
cls = type(data)
|
||||
if 'modified' not in kwargs:
|
||||
kwargs['modified'] = get_timestamp()
|
||||
elif 'modified' in data:
|
||||
old_modified_property = parse_into_datetime(data.get('modified'), precision='millisecond')
|
||||
new_modified_property = parse_into_datetime(kwargs['modified'], precision='millisecond')
|
||||
if new_modified_property < old_modified_property:
|
||||
raise InvalidValueError(cls, 'modified', "The new modified datetime cannot be before the current modified datatime.")
|
||||
new_obj_inner.update(kwargs)
|
||||
return cls(**new_obj_inner)
|
||||
|
||||
|
||||
def revoke(data):
|
||||
if not isinstance(data, Mapping):
|
||||
raise ValueError('cannot revoke object of this type! Try a dictionary '
|
||||
'or instance of an SDO or SRO class.')
|
||||
|
||||
if data.get("revoked"):
|
||||
raise RevokeError("revoke")
|
||||
return new_version(data, revoked=True)
|
||||
|
|
Loading…
Reference in New Issue