From c63ba8e44797bc2c2b85bd910750f4478bb11d43 Mon Sep 17 00:00:00 2001 From: clenk Date: Wed, 3 May 2017 14:10:10 -0400 Subject: [PATCH 1/4] Add ObservableProperty, DictionaryProperty, HashesProperty, BinaryProperty, and HexProperty --- stix2/base.py | 4 ++ stix2/exceptions.py | 13 ++++++ stix2/properties.py | 85 ++++++++++++++++++++++++++++++++++- stix2/test/test_properties.py | 67 +++++++++++++++++++++++++-- 4 files changed, 165 insertions(+), 4 deletions(-) diff --git a/stix2/base.py b/stix2/base.py index 05bf545..5b58f06 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -99,3 +99,7 @@ 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])) + + +class Observable(_STIXBase): + pass diff --git a/stix2/exceptions.py b/stix2/exceptions.py index 2606796..61cec79 100644 --- a/stix2/exceptions.py +++ b/stix2/exceptions.py @@ -49,3 +49,16 @@ class ImmutableError(STIXError, ValueError): def __init__(self): super(ImmutableError, self).__init__("Cannot modify properties after creation.") + + +class DictionaryKeyError(STIXError, ValueError): + """Dictionary key does not conform to the correct format.""" + + def __init__(self, key, reason): + super(DictionaryKeyError, self).__init__() + self.key = key + self.reason = reason + + def __str__(self): + msg = "Invliad dictionary key {0.key}: ({0.reason})." + return msg.format(self) diff --git a/stix2/properties.py b/stix2/properties.py index 76fe31b..b8ce17b 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -1,3 +1,5 @@ +import base64 +import binascii import collections import datetime as dt import inspect @@ -8,7 +10,8 @@ from dateutil import parser import pytz from six import text_type -from .base import _STIXBase +from .base import _STIXBase, Observable +from .exceptions import DictionaryKeyError class Property(object): @@ -213,6 +216,86 @@ class TimestampProperty(Property): return pytz.utc.localize(parsed) +class ObservableProperty(Property): + + def clean(self, value): + dictified = dict(value) + for obj in dictified: + if not issubclass(type(obj), Observable): + raise ValueError("Objects in an observable property must be " + "Cyber Observable Objects") + return dictified + + +class DictionaryProperty(Property): + + def clean(self, value): + dictified = dict(value) + for k in dictified.keys(): + if len(k) < 3: + raise DictionaryKeyError(k, "shorter than 3 characters") + elif len(k) > 256: + raise DictionaryKeyError(k, "longer than 256 characters") + if not re.match('^[a-zA-Z0-9_-]+$', k): + raise DictionaryKeyError(k, "contains characters other than" + "lowercase a-z, uppercase A-Z, " + "numerals 0-9, hyphen (-), or " + "underscore (_)") + return dictified + + +HASHES_REGEX = { + "MD5": ("^[a-fA-F0-9]{32}$", "MD5"), + "MD6": ("^[a-fA-F0-9]{32}|[a-fA-F0-9]{40}|[a-fA-F0-9]{56}|[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128}$", "MD6"), + "RIPEMD160": ("^[a-fA-F0-9]{40}$", "RIPEMD-160"), + "SHA1": ("^[a-fA-F0-9]{40}$", "SHA-1"), + "SHA224": ("^[a-fA-F0-9]{56}$", "SHA-224"), + "SHA256": ("^[a-fA-F0-9]{64}$", "SHA-256"), + "SHA384": ("^[a-fA-F0-9]{96}$", "SHA-384"), + "SHA512": ("^[a-fA-F0-9]{128}$", "SHA-512"), + "SHA3224": ("^[a-fA-F0-9]{56}$", "SHA3-224"), + "SHA3256": ("^[a-fA-F0-9]{64}$", "SHA3-256"), + "SHA3384": ("^[a-fA-F0-9]{96}$", "SHA3-384"), + "SHA3512": ("^[a-fA-F0-9]{128}$", "SHA3-512"), + "SSDEEP": ("^[a-zA-Z0-9/+:.]{1,128}$", "ssdeep"), + "WHIRLPOOL": ("^[a-fA-F0-9]{128}$", "WHIRLPOOL"), +} + + +class HashesProperty(DictionaryProperty): + + def clean(self, value): + clean_dict = super(HashesProperty, self).clean(value) + for k, v in clean_dict.items(): + key = k.upper().replace('-', '') + if key in HASHES_REGEX: + vocab_key = HASHES_REGEX[key][1] + if not re.match(HASHES_REGEX[key][0], v): + raise ValueError("'%s' is not a valid %s hash" % (v, vocab_key)) + if k != vocab_key: + clean_dict[vocab_key] = clean_dict[k] + del clean_dict[k] + return clean_dict + + +class BinaryProperty(Property): + + def clean(self, value): + try: + base64.b64decode(value) + except (binascii.Error, TypeError): + raise ValueError("must contain a base64 encoded string") + return value + + +class HexProperty(Property): + + def clean(self, value): + if not re.match('^([a-fA-F0-9]{2})+$', value): + raise ValueError("must contain an even number of hexadecimal characters") + return value + + REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}" "-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index e83b2fc..246f349 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -1,8 +1,12 @@ import pytest -from stix2.properties import (BooleanProperty, IDProperty, IntegerProperty, - ListProperty, Property, ReferenceProperty, - StringProperty, TimestampProperty, TypeProperty) +from stix2.exceptions import DictionaryKeyError +from stix2.properties import (BinaryProperty, BooleanProperty, + DictionaryProperty, HashesProperty, HexProperty, + IDProperty, IntegerProperty, ListProperty, + Property, ReferenceProperty, StringProperty, + TimestampProperty, TypeProperty) + from .constants import FAKE_TIME @@ -171,3 +175,60 @@ def test_timestamp_property_invalid(): ts_prop.clean(1) with pytest.raises(ValueError): ts_prop.clean("someday sometime") + + +def test_binary_property(): + bin_prop = BinaryProperty() + + assert bin_prop.clean("TG9yZW0gSXBzdW0=") + with pytest.raises(ValueError): + bin_prop.clean("foobar") + + +def test_hex_property(): + hex_prop = HexProperty() + + assert hex_prop.clean("4c6f72656d20497073756d") + with pytest.raises(ValueError): + hex_prop.clean("foobar") + + +@pytest.mark.parametrize("d", [ + {'description': 'something'}, + [('abc', 1), ('bcd', 2), ('cde', 3)], +]) +def test_dictionary_property_valid(d): + dict_prop = DictionaryProperty() + assert dict_prop.clean(d) + + +@pytest.mark.parametrize("d", [ + {'a': 'something'}, + {'a'*300: 'something'}, + {'Hey!': 'something'}, +]) +def test_dictionary_property_invalid(d): + dict_prop = DictionaryProperty() + + with pytest.raises(DictionaryKeyError): + dict_prop.clean(d) + + +@pytest.mark.parametrize("value", [ + {"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"}, + [('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')], +]) +def test_hashes_property_valid(value): + hash_prop = HashesProperty() + assert hash_prop.clean(value) + + +@pytest.mark.parametrize("value", [ + {"MD5": "a"}, + {"SHA-256": "2dfb1bcc980200c6706feee399d41b3f"}, +]) +def test_hashes_property_invalid(value): + hash_prop = HashesProperty() + + with pytest.raises(ValueError): + hash_prop.clean(value) From 2c67b906385e0391b5f22286badc9610abf2512d Mon Sep 17 00:00:00 2001 From: clenk Date: Wed, 3 May 2017 17:35:33 -0400 Subject: [PATCH 2/4] Add Artifact type --- stix2/__init__.py | 17 +++++++++--- stix2/observables.py | 26 ++++++++++++++++++ stix2/properties.py | 8 ++++-- stix2/sdo.py | 8 +++--- stix2/test/test_observed_data.py | 45 +++++++++++++++++++++++++++++++- 5 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 stix2/observables.py diff --git a/stix2/__init__.py b/stix2/__init__.py index 187d18a..1ec1711 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -3,6 +3,7 @@ # flake8: noqa from .bundle import Bundle +from .observables import Artifact, File from .other import ExternalReference, KillChainPhase, MarkingDefinition, \ GranularMarking, StatementMarking, TLPMarking from .sdo import AttackPattern, Campaign, CourseOfAction, Identity, Indicator, \ @@ -31,8 +32,13 @@ OBJ_MAP = { 'vulnerability': Vulnerability, } +OBJ_MAP_OBSERVABLE = { + 'artifact': Artifact, + 'file': File, +} -def parse(data): + +def parse(data, observable=False): """Deserialize a string or file-like object into a STIX object""" obj = get_dict(data) @@ -42,10 +48,13 @@ def parse(data): pass else: try: - obj_class = OBJ_MAP[obj['type']] - return obj_class(**obj) + if observable: + obj_class = OBJ_MAP_OBSERVABLE[obj['type']] + else: + obj_class = OBJ_MAP[obj['type']] except KeyError: # TODO handle custom objects - raise ValueError("Can't parse unknown object type!") + raise ValueError("Can't parse unknown object type '%s'!" % obj['type']) + return obj_class(**obj) return obj diff --git a/stix2/observables.py b/stix2/observables.py new file mode 100644 index 0000000..33eded2 --- /dev/null +++ b/stix2/observables.py @@ -0,0 +1,26 @@ +"""STIX 2.0 Cyber Observable Objects""" + +from .base import Observable +# from .properties import (BinaryProperty, BooleanProperty, DictionaryProperty, +# HashesProperty, HexProperty, IDProperty, +# IntegerProperty, ListProperty, ReferenceProperty, +# StringProperty, TimestampProperty, TypeProperty) +from .properties import BinaryProperty, HashesProperty, StringProperty, TypeProperty + + +class Artifact(Observable): + _type = 'artifact' + _properties = { + 'type': TypeProperty(_type), + 'mime_type': StringProperty(), + 'payload_bin': BinaryProperty(), + 'url': StringProperty(), + 'hashes': HashesProperty(), + } + + +class File(Observable): + _type = 'file' + _properties = { + 'type': TypeProperty(_type), + } diff --git a/stix2/properties.py b/stix2/properties.py index b8ce17b..4b4da7a 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -220,10 +220,14 @@ class ObservableProperty(Property): def clean(self, value): dictified = dict(value) - for obj in dictified: - if not issubclass(type(obj), Observable): + from .__init__ import parse # avoid circular import + for key, obj in dictified.items(): + parsed_obj = parse(obj, observable=True) + if not issubclass(type(parsed_obj), Observable): raise ValueError("Objects in an observable property must be " "Cyber Observable Objects") + dictified[key] = parsed_obj + return dictified diff --git a/stix2/sdo.py b/stix2/sdo.py index 693b750..ca1e5b4 100644 --- a/stix2/sdo.py +++ b/stix2/sdo.py @@ -3,9 +3,9 @@ from .base import _STIXBase from .common import COMMON_PROPERTIES from .other import KillChainPhase -from .properties import (IDProperty, IntegerProperty, ListProperty, Property, - ReferenceProperty, StringProperty, TimestampProperty, - TypeProperty) +from .properties import (IDProperty, IntegerProperty, ListProperty, + ObservableProperty, ReferenceProperty, + StringProperty, TimestampProperty, TypeProperty) from .utils import NOW @@ -126,7 +126,7 @@ class ObservedData(_STIXBase): 'first_observed': TimestampProperty(required=True), 'last_observed': TimestampProperty(required=True), 'number_observed': IntegerProperty(required=True), - 'objects': Property(), + 'objects': ObservableProperty(), }) diff --git a/stix2/test/test_observed_data.py b/stix2/test/test_observed_data.py index 52dc15b..154b395 100644 --- a/stix2/test/test_observed_data.py +++ b/stix2/test/test_observed_data.py @@ -1,4 +1,5 @@ import datetime as dt +import re import pytest import pytz @@ -70,6 +71,48 @@ def test_parse_observed_data(data): assert odata.first_observed == dt.datetime(2015, 12, 21, 19, 0, 0, tzinfo=pytz.utc) assert odata.last_observed == dt.datetime(2015, 12, 21, 19, 0, 0, tzinfo=pytz.utc) assert odata.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff" - # assert odata.objects["0"].type == "file" # TODO + assert odata.objects["0"].type == "file" + + +@pytest.mark.parametrize("data", [ + """"0": { + "type": "artifact", + "mime_type": "image/jpeg", + "payload_bin": "VBORw0KGgoAAAANSUhEUgAAADI==" + }""", + """"0": { + "type": "artifact", + "mime_type": "image/jpeg", + "url": "https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", + "hashes": { + "MD5": "6826f9a05da08134006557758bb3afbb" + } + }""", +]) +def test_parse_artifact_valid(data): + odata_str = re.compile('"objects".+\},', re.DOTALL).sub('"objects": { %s },' % data, EXPECTED) + odata = stix2.parse(odata_str) + assert odata.objects["0"].type == "artifact" + + +@pytest.mark.parametrize("data", [ + """"0": { + "type": "artifact", + "mime_type": "image/jpeg", + "payload_bin": "abcVBORw0KGgoAAAANSUhEUgAAADI==" + }""", + """"0": { + "type": "artifact", + "mime_type": "image/jpeg", + "url": "https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", + "hashes": { + "MD5": "a" + } + }""", +]) +def test_parse_artifact_invalid(data): + odata_str = re.compile('"objects".+\},', re.DOTALL).sub('"objects": { %s },' % data, EXPECTED) + with pytest.raises(ValueError): + stix2.parse(odata_str) # TODO: Add other examples From 1a75d830bb63d3c8c88caea7c1c92d1fdffb13d1 Mon Sep 17 00:00:00 2001 From: clenk Date: Wed, 3 May 2017 18:19:30 -0400 Subject: [PATCH 3/4] Add Autonomous System --- stix2/__init__.py | 3 ++- stix2/observables.py | 12 +++++++++++- stix2/properties.py | 11 +++++++++++ stix2/test/test_observed_data.py | 18 ++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/stix2/__init__.py b/stix2/__init__.py index 1ec1711..763c4d4 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -3,7 +3,7 @@ # flake8: noqa from .bundle import Bundle -from .observables import Artifact, File +from .observables import Artifact, AutonomousSystem, File from .other import ExternalReference, KillChainPhase, MarkingDefinition, \ GranularMarking, StatementMarking, TLPMarking from .sdo import AttackPattern, Campaign, CourseOfAction, Identity, Indicator, \ @@ -34,6 +34,7 @@ OBJ_MAP = { OBJ_MAP_OBSERVABLE = { 'artifact': Artifact, + 'autonomous-system': AutonomousSystem, 'file': File, } diff --git a/stix2/observables.py b/stix2/observables.py index 33eded2..4e72cae 100644 --- a/stix2/observables.py +++ b/stix2/observables.py @@ -5,7 +5,7 @@ from .base import Observable # HashesProperty, HexProperty, IDProperty, # IntegerProperty, ListProperty, ReferenceProperty, # StringProperty, TimestampProperty, TypeProperty) -from .properties import BinaryProperty, HashesProperty, StringProperty, TypeProperty +from .properties import BinaryProperty, HashesProperty, IntegerProperty, StringProperty, TypeProperty class Artifact(Observable): @@ -19,6 +19,16 @@ class Artifact(Observable): } +class AutonomousSystem(Observable): + _type = 'autonomous-system' + _properties = { + 'type': TypeProperty(_type), + 'number': IntegerProperty(), + 'name': StringProperty(), + 'rir': StringProperty(), + } + + class File(Observable): _type = 'file' _properties = { diff --git a/stix2/properties.py b/stix2/properties.py index 4b4da7a..5aae467 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -335,3 +335,14 @@ class SelectorProperty(Property): if not SELECTOR_REGEX.match(value): raise ValueError("must adhere to selector syntax.") return value + + +class ObjectReferenceProperty(Property): + def _init(self, valid_refs=None): + self.valid_refs = valid_refs + super(ObjectReferenceProperty, self).__init__() + + def clean(self, value): + if value not in self.valid_refs: + raise ValueError("must refer to observable objects in the same " + "Observable Objects container.") diff --git a/stix2/test/test_observed_data.py b/stix2/test/test_observed_data.py index 154b395..5472597 100644 --- a/stix2/test/test_observed_data.py +++ b/stix2/test/test_observed_data.py @@ -115,4 +115,22 @@ def test_parse_artifact_invalid(data): with pytest.raises(ValueError): stix2.parse(odata_str) + +@pytest.mark.parametrize("data", [ + """"0": { + "type": "autonomous-system", + "number": 15139, + "name": "Slime Industries", + "rir": "ARIN" + }""", +]) +def test_parse_autonomous_system_valid(data): + odata_str = re.compile('"objects".+\},', re.DOTALL).sub('"objects": { %s },' % data, EXPECTED) + odata = stix2.parse(odata_str) + assert odata.objects["0"].type == "autonomous-system" + assert odata.objects["0"].number == 15139 + assert odata.objects["0"].name == "Slime Industries" + assert odata.objects["0"].rir == "ARIN" + + # TODO: Add other examples From 04e3a72a7d7dd553f3f8527a3fe208d270cb0b6a Mon Sep 17 00:00:00 2001 From: clenk Date: Fri, 5 May 2017 12:32:02 -0400 Subject: [PATCH 4/4] Add EmailAddress and ObjectReferenceProperty --- stix2/__init__.py | 28 ++++++++++++++++++++++------ stix2/base.py | 20 ++++++++++++++++++-- stix2/exceptions.py | 14 ++++++++++++++ stix2/observables.py | 12 +++++++++++- stix2/properties.py | 15 ++++----------- stix2/test/test_observed_data.py | 19 +++++++++++++++++++ 6 files changed, 88 insertions(+), 20 deletions(-) diff --git a/stix2/__init__.py b/stix2/__init__.py index 763c4d4..4a9ec75 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -3,7 +3,7 @@ # flake8: noqa from .bundle import Bundle -from .observables import Artifact, AutonomousSystem, File +from .observables import Artifact, AutonomousSystem, EmailAddress, File from .other import ExternalReference, KillChainPhase, MarkingDefinition, \ GranularMarking, StatementMarking, TLPMarking from .sdo import AttackPattern, Campaign, CourseOfAction, Identity, Indicator, \ @@ -35,11 +35,12 @@ OBJ_MAP = { OBJ_MAP_OBSERVABLE = { 'artifact': Artifact, 'autonomous-system': AutonomousSystem, + 'email-address': EmailAddress, 'file': File, } -def parse(data, observable=False): +def parse(data): """Deserialize a string or file-like object into a STIX object""" obj = get_dict(data) @@ -49,13 +50,28 @@ def parse(data, observable=False): pass else: try: - if observable: - obj_class = OBJ_MAP_OBSERVABLE[obj['type']] - else: - obj_class = OBJ_MAP[obj['type']] + obj_class = OBJ_MAP[obj['type']] except KeyError: # TODO handle custom objects raise ValueError("Can't parse unknown object type '%s'!" % obj['type']) return obj_class(**obj) return obj + + +def parse_observable(data, _valid_refs): + """Deserialize a string or file-like object into a STIX Cyber Observable + object. + """ + + obj = get_dict(data) + obj['_valid_refs'] = _valid_refs + + if 'type' not in obj: + raise ValueError("'type' is a required field!") + try: + obj_class = OBJ_MAP_OBSERVABLE[obj['type']] + except KeyError: + # TODO handle custom objects + raise ValueError("Can't parse unknown object type '%s'!" % obj['type']) + return obj_class(**obj) diff --git a/stix2/base.py b/stix2/base.py index 5b58f06..eaf317c 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -5,7 +5,7 @@ import datetime as dt import json from .exceptions import ExtraFieldsError, ImmutableError, InvalidValueError, \ - MissingFieldsError + InvalidObjRefError, MissingFieldsError from .utils import format_datetime, get_timestamp, NOW __all__ = ['STIXJSONEncoder', '_STIXBase'] @@ -102,4 +102,20 @@ class _STIXBase(collections.Mapping): class Observable(_STIXBase): - pass + + def __init__(self, **kwargs): + self._STIXBase__valid_refs = kwargs.pop('_valid_refs') + super(Observable, self).__init__(**kwargs) + + def _check_property(self, prop_name, prop, kwargs): + super(Observable, self)._check_property(prop_name, prop, kwargs) + if prop_name.endswith('_ref'): + ref = kwargs[prop_name].split('--', 1)[0] + if ref not in self._STIXBase__valid_refs: + raise InvalidObjRefError(self.__class__, prop_name, "'%s' is not a valid object in local scope" % ref) + if prop_name.endswith('_refs'): + for r in kwargs[prop_name]: + ref = r.split('--', 1)[0] + if ref not in self._STIXBase__valid_refs: + raise InvalidObjRefError(self.__class__, prop_name, "'%s' is not a valid object in local scope" % ref) + # TODO also check the type of the object referenced, not just that the key exists diff --git a/stix2/exceptions.py b/stix2/exceptions.py index 61cec79..c23f20d 100644 --- a/stix2/exceptions.py +++ b/stix2/exceptions.py @@ -62,3 +62,17 @@ class DictionaryKeyError(STIXError, ValueError): def __str__(self): msg = "Invliad dictionary key {0.key}: ({0.reason})." return msg.format(self) + + +class InvalidObjRefError(STIXError, ValueError): + """A STIX Cyber Observable Object contains an invalid object reference.""" + + def __init__(self, cls, prop_name, reason): + super(InvalidObjRefError, self).__init__() + self.cls = cls + self.prop_name = prop_name + self.reason = reason + + def __str__(self): + msg = "Invalid object reference for '{0.cls.__name__}:{0.prop_name}': {0.reason}" + return msg.format(self) diff --git a/stix2/observables.py b/stix2/observables.py index 4e72cae..7cad32a 100644 --- a/stix2/observables.py +++ b/stix2/observables.py @@ -5,7 +5,7 @@ from .base import Observable # HashesProperty, HexProperty, IDProperty, # IntegerProperty, ListProperty, ReferenceProperty, # StringProperty, TimestampProperty, TypeProperty) -from .properties import BinaryProperty, HashesProperty, IntegerProperty, StringProperty, TypeProperty +from .properties import BinaryProperty, HashesProperty, IntegerProperty, ObjectReferenceProperty, StringProperty, TypeProperty class Artifact(Observable): @@ -29,6 +29,16 @@ class AutonomousSystem(Observable): } +class EmailAddress(Observable): + _type = 'email-address' + _properties = { + 'type': TypeProperty(_type), + 'value': StringProperty(required=True), + 'display_name': StringProperty(), + 'belongs_to_ref': ObjectReferenceProperty(), + } + + class File(Observable): _type = 'file' _properties = { diff --git a/stix2/properties.py b/stix2/properties.py index 5aae467..57ebeca 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -220,9 +220,9 @@ class ObservableProperty(Property): def clean(self, value): dictified = dict(value) - from .__init__ import parse # avoid circular import + from .__init__ import parse_observable # avoid circular import for key, obj in dictified.items(): - parsed_obj = parse(obj, observable=True) + parsed_obj = parse_observable(obj, dictified.keys()) if not issubclass(type(parsed_obj), Observable): raise ValueError("Objects in an observable property must be " "Cyber Observable Objects") @@ -337,12 +337,5 @@ class SelectorProperty(Property): return value -class ObjectReferenceProperty(Property): - def _init(self, valid_refs=None): - self.valid_refs = valid_refs - super(ObjectReferenceProperty, self).__init__() - - def clean(self, value): - if value not in self.valid_refs: - raise ValueError("must refer to observable objects in the same " - "Observable Objects container.") +class ObjectReferenceProperty(StringProperty): + pass diff --git a/stix2/test/test_observed_data.py b/stix2/test/test_observed_data.py index 5472597..28388c8 100644 --- a/stix2/test/test_observed_data.py +++ b/stix2/test/test_observed_data.py @@ -6,6 +6,7 @@ import pytz import stix2 from .constants import OBSERVED_DATA_ID +from ..exceptions import InvalidValueError EXPECTED = """{ "created": "2016-04-06T19:58:16Z", @@ -133,4 +134,22 @@ def test_parse_autonomous_system_valid(data): assert odata.objects["0"].rir == "ARIN" +@pytest.mark.parametrize("data", [ + """"1": { + "type": "email-address", + "value": "john@example.com", + "display_name": "John Doe", + "belongs_to_ref": "0" + }""", +]) +def test_parse_email_address(data): + odata_str = re.compile('\}.+\},', re.DOTALL).sub('}, %s},' % data, EXPECTED) + odata = stix2.parse(odata_str) + assert odata.objects["1"].type == "email-address" + + odata_str = re.compile('"belongs_to_ref": "0"', re.DOTALL).sub('"belongs_to_ref": "3"', odata_str) + with pytest.raises(InvalidValueError): + stix2.parse(odata_str) + + # TODO: Add other examples