diff --git a/README.rst b/README.rst index 0613a15..0bf30fd 100644 --- a/README.rst +++ b/README.rst @@ -52,6 +52,7 @@ To parse a STIX JSON string into a Python STIX object, use ``parse()``: "indicator_types": [ "malicious-activity" ], + "pattern_type": "stix", "pattern": "[file:hashes.md5 ='d41d8cd98f00b204e9800998ecf8427e']", "valid_from": "2017-09-26T23:33:39.829952Z" }""") diff --git a/stix2/core.py b/stix2/core.py index 993a0f5..03d2f15 100644 --- a/stix2/core.py +++ b/stix2/core.py @@ -8,7 +8,7 @@ import re import stix2 from .base import _STIXBase -from .exceptions import CustomContentError, ParseError +from .exceptions import ParseError from .markings import _MarkingsMixin from .utils import _get_dict @@ -126,7 +126,7 @@ def dict_to_stix2(stix_dict, allow_custom=False, interoperability=False, version # '2.0' representation. v = 'v20' - OBJ_MAP = STIX2_OBJ_MAPS[v]['objects'] + OBJ_MAP = dict(STIX2_OBJ_MAPS[v]['objects'], **STIX2_OBJ_MAPS[v]['observables']) try: obj_class = OBJ_MAP[stix_dict['type']] @@ -183,8 +183,8 @@ def parse_observable(data, _valid_refs=None, allow_custom=False, version=None): # flag allows for unknown custom objects too, but will not # be parsed into STIX observable object, just returned as is return obj - raise CustomContentError("Can't parse unknown observable type '%s'! For custom observables, " - "use the CustomObservable decorator." % obj['type']) + raise ParseError("Can't parse unknown observable type '%s'! For custom observables, " + "use the CustomObservable decorator." % obj['type']) return obj_class(allow_custom=allow_custom, **obj) diff --git a/stix2/custom.py b/stix2/custom.py index 484cbb0..a00498b 100644 --- a/stix2/custom.py +++ b/stix2/custom.py @@ -67,19 +67,34 @@ def _custom_observable_builder(cls, type, properties, version): if not properties or not isinstance(properties, list): raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") - # Check properties ending in "_ref/s" are ObjectReferenceProperties - for prop_name, prop in properties: - if prop_name.endswith('_ref') and ('ObjectReferenceProperty' not in get_class_hierarchy_names(prop)): - raise ValueError( - "'%s' is named like an object reference property but " - "is not an ObjectReferenceProperty." % prop_name, - ) - elif (prop_name.endswith('_refs') and ('ListProperty' not in get_class_hierarchy_names(prop) - or 'ObjectReferenceProperty' not in get_class_hierarchy_names(prop.contained))): - raise ValueError( - "'%s' is named like an object reference list property but " - "is not a ListProperty containing ObjectReferenceProperty." % prop_name, - ) + if version == "2.0": + # If using STIX2.0, check properties ending in "_ref/s" are ObjectReferenceProperties + for prop_name, prop in properties: + if prop_name.endswith('_ref') and ('ObjectReferenceProperty' not in get_class_hierarchy_names(prop)): + raise ValueError( + "'%s' is named like an object reference property but " + "is not an ObjectReferenceProperty." % prop_name, + ) + elif (prop_name.endswith('_refs') and ('ListProperty' not in get_class_hierarchy_names(prop) or + 'ObjectReferenceProperty' not in get_class_hierarchy_names(prop.contained))): + raise ValueError( + "'%s' is named like an object reference list property but " + "is not a ListProperty containing ObjectReferenceProperty." % prop_name, + ) + else: + # If using STIX2.1 (or newer...), check properties ending in "_ref/s" are ReferenceProperties + for prop_name, prop in properties: + if prop_name.endswith('_ref') and ('ReferenceProperty' not in get_class_hierarchy_names(prop)): + raise ValueError( + "'%s' is named like a reference property but " + "is not a ReferenceProperty." % prop_name, + ) + elif (prop_name.endswith('_refs') and ('ListProperty' not in get_class_hierarchy_names(prop) or + 'ReferenceProperty' not in get_class_hierarchy_names(prop.contained))): + raise ValueError( + "'%s' is named like a reference list property but " + "is not a ListProperty containing ReferenceProperty." % prop_name, + ) _type = type _properties = OrderedDict(properties) diff --git a/stix2/patterns.py b/stix2/patterns.py index 9656ff1..2e149be 100644 --- a/stix2/patterns.py +++ b/stix2/patterns.py @@ -135,6 +135,7 @@ _HASH_REGEX = { "SHA3512": ("^[a-fA-F0-9]{128}$", "SHA3-512"), "SSDEEP": ("^[a-zA-Z0-9/+:.]{1,128}$", "ssdeep"), "WHIRLPOOL": ("^[a-fA-F0-9]{128}$", "WHIRLPOOL"), + "TLSH": ("^[a-fA-F0-9]{70}$", "TLSH"), } diff --git a/stix2/properties.py b/stix2/properties.py index 42550f6..3b4ac07 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -373,6 +373,10 @@ class DictionaryProperty(Property): "underscore (_)" ) raise DictionaryKeyError(k, msg) + + if len(dictified) < 1: + raise ValueError("must not be empty.") + return dictified @@ -391,6 +395,7 @@ HASHES_REGEX = { "SHA3512": (r"^[a-fA-F0-9]{128}$", "SHA3-512"), "SSDEEP": (r"^[a-zA-Z0-9/+:.]{1,128}$", "ssdeep"), "WHIRLPOOL": (r"^[a-fA-F0-9]{128}$", "WHIRLPOOL"), + "TLSH": (r"^[a-fA-F0-9]{70}$", "TLSH"), } @@ -459,22 +464,19 @@ class ReferenceProperty(Property): value = value.id value = str(value) - possible_prefix = value[:value.index('--') + 2] + possible_prefix = value[:value.index('--')] if self.valid_types: - if self.valid_types == ["only_SDO"]: - self.valid_types = STIX2_OBJ_MAPS['v21']['objects'].keys() - elif self.valid_types == ["only_SCO"]: - self.valid_types = STIX2_OBJ_MAPS['v21']['observables'].keys() - elif self.valid_types == ["only_SCO_&_SRO"]: - self.valid_types = list(STIX2_OBJ_MAPS['v21']['observables'].keys()) + ['relationship', 'sighting'] + ref_valid_types = enumerate_types(self.valid_types, 'v' + self.spec_version.replace(".", "")) - if possible_prefix[:-2] in self.valid_types: + if possible_prefix in ref_valid_types: required_prefix = possible_prefix else: raise ValueError("The type-specifying prefix '%s' for this property is not valid" % (possible_prefix)) elif self.invalid_types: - if possible_prefix[:-2] not in self.invalid_types: + ref_invalid_types = enumerate_types(self.invalid_types, 'v' + self.spec_version.replace(".", "")) + + if possible_prefix not in ref_invalid_types: required_prefix = possible_prefix else: raise ValueError("An invalid type-specifying prefix '%s' was specified for this property" % (possible_prefix, value)) @@ -484,6 +486,31 @@ class ReferenceProperty(Property): return value +def enumerate_types(types, spec_version): + """ + `types` is meant to be a list; it may contain specific object types and/or + the any of the words "SCO", "SDO", or "SRO" + + Since "SCO", "SDO", and "SRO" are general types that encompass various specific object types, + once each of those words is being processed, that word will be removed from `return_types`, + so as not to mistakenly allow objects to be created of types "SCO", "SDO", or "SRO" + """ + return_types = [] + return_types += types + + if "SDO" in types: + return_types.remove("SDO") + return_types += STIX2_OBJ_MAPS[spec_version]['objects'].keys() + if "SCO" in types: + return_types.remove("SCO") + return_types += STIX2_OBJ_MAPS[spec_version]['observables'].keys() + if "SRO" in types: + return_types.remove("SRO") + return_types += ['relationship', 'sighting'] + + return return_types + + SELECTOR_REGEX = re.compile(r"^[a-z0-9_-]{3,250}(\.(\[\d+\]|[a-z0-9_-]{1,250}))*$") diff --git a/stix2/test/v20/test_custom.py b/stix2/test/v20/test_custom.py index 6d127f2..ce1aac3 100644 --- a/stix2/test/v20/test_custom.py +++ b/stix2/test/v20/test_custom.py @@ -583,7 +583,7 @@ def test_parse_unregistered_custom_observable_object(): "property1": "something" }""" - with pytest.raises(stix2.exceptions.CustomContentError) as excinfo: + with pytest.raises(stix2.exceptions.ParseError) as excinfo: stix2.parse_observable(nt_string, version='2.0') assert "Can't parse unknown observable type" in str(excinfo.value) diff --git a/stix2/test/v21/test_base.py b/stix2/test/v21/test_base.py index d753ab1..325cf4b 100644 --- a/stix2/test/v21/test_base.py +++ b/stix2/test/v21/test_base.py @@ -29,7 +29,7 @@ def test_encode_json_object(): def test_deterministic_id_unicode(): mutex = {'name': u'D*Fl#Ed*\u00a3\u00a8', 'type': 'mutex'} - obs = stix2.parse_observable(mutex, version="2.1") + obs = stix2.parse(mutex, version="2.1") dd_idx = obs.id.index("--") id_uuid = uuid.UUID(obs.id[dd_idx+2:]) diff --git a/stix2/test/v21/test_bundle.py b/stix2/test/v21/test_bundle.py index 54ef318..4e30c84 100644 --- a/stix2/test/v21/test_bundle.py +++ b/stix2/test/v21/test_bundle.py @@ -22,6 +22,7 @@ EXPECTED_BUNDLE = """{ ], "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", "pattern_type": "stix", + "pattern_version": "2.1", "valid_from": "2017-01-01T12:34:56Z" }, { @@ -61,6 +62,7 @@ EXPECTED_BUNDLE_DICT = { "modified": "2017-01-01T12:34:56.000Z", "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", "pattern_type": "stix", + "pattern_version": "2.1", "valid_from": "2017-01-01T12:34:56Z", "indicator_types": [ "malicious-activity", diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py index 47d484a..9c650eb 100644 --- a/stix2/test/v21/test_custom.py +++ b/stix2/test/v21/test_custom.py @@ -508,7 +508,7 @@ def test_custom_observable_object_invalid_ref_property(): ) class NewObs(): pass - assert "is named like an object reference property but is not an ObjectReferenceProperty" in str(excinfo.value) + assert "is named like a reference property but is not a ReferenceProperty" in str(excinfo.value) def test_custom_observable_object_invalid_refs_property(): @@ -520,7 +520,7 @@ def test_custom_observable_object_invalid_refs_property(): ) class NewObs(): pass - assert "is named like an object reference list property but is not a ListProperty containing ObjectReferenceProperty" in str(excinfo.value) + assert "is named like a reference list property but is not a ListProperty containing ReferenceProperty" in str(excinfo.value) def test_custom_observable_object_invalid_refs_list_property(): @@ -532,26 +532,7 @@ def test_custom_observable_object_invalid_refs_list_property(): ) class NewObs(): pass - assert "is named like an object reference list property but is not a ListProperty containing ObjectReferenceProperty" in str(excinfo.value) - - -def test_custom_observable_object_invalid_valid_refs(): - @stix2.v21.CustomObservable( - 'x-new-obs', [ - ('property1', stix2.properties.StringProperty(required=True)), - ('property_ref', stix2.properties.ObjectReferenceProperty(valid_types='email-addr')), - ], - ) - class NewObs(): - pass - - with pytest.raises(Exception) as excinfo: - NewObs( - _valid_refs=['1'], - property1='something', - property_ref='1', - ) - assert "must be created with _valid_refs as a dict, not a list" in str(excinfo.value) + assert "is named like a reference list property but is not a ListProperty containing ReferenceProperty" in str(excinfo.value) def test_custom_no_properties_raises_exception(): @@ -575,8 +556,7 @@ def test_parse_custom_observable_object(): "type": "x-new-observable", "property1": "something" }""" - - nt = stix2.parse_observable(nt_string, [], version='2.1') + nt = stix2.parse(nt_string, [], version='2.1') assert isinstance(nt, stix2.base._STIXBase) assert nt.property1 == 'something' @@ -587,11 +567,10 @@ def test_parse_unregistered_custom_observable_object(): "property1": "something" }""" - with pytest.raises(stix2.exceptions.CustomContentError) as excinfo: - stix2.parse_observable(nt_string, version='2.1') - assert "Can't parse unknown observable type" in str(excinfo.value) - - parsed_custom = stix2.parse_observable(nt_string, allow_custom=True, version='2.1') + with pytest.raises(stix2.exceptions.ParseError) as excinfo: + stix2.parse(nt_string, version='2.1') + assert "Can't parse unknown object type" in str(excinfo.value) + parsed_custom = stix2.parse(nt_string, allow_custom=True, version='2.1') assert parsed_custom['property1'] == 'something' with pytest.raises(AttributeError) as excinfo: assert parsed_custom.property1 == 'something' @@ -604,8 +583,8 @@ def test_parse_unregistered_custom_observable_object_with_no_type(): }""" with pytest.raises(stix2.exceptions.ParseError) as excinfo: - stix2.parse_observable(nt_string, allow_custom=True, version='2.1') - assert "Can't parse observable with no 'type' property" in str(excinfo.value) + stix2.parse(nt_string, allow_custom=True, version='2.1') + assert "Can't parse object with no 'type' property" in str(excinfo.value) def test_parse_observed_data_with_custom_observable(): @@ -634,8 +613,8 @@ def test_parse_invalid_custom_observable_object(): }""" with pytest.raises(stix2.exceptions.ParseError) as excinfo: - stix2.parse_observable(nt_string, version='2.1') - assert "Can't parse observable with no 'type' property" in str(excinfo.value) + stix2.parse(nt_string, version='2.1') + assert "Can't parse object with no 'type' property" in str(excinfo.value) def test_observable_custom_property(): @@ -885,8 +864,7 @@ def test_parse_observable_with_custom_extension(): } } }""" - - parsed = stix2.parse_observable(input_str, version='2.1') + parsed = stix2.parse(input_str, version='2.1') assert parsed.extensions['x-new-ext'].property2 == 12 @@ -961,10 +939,9 @@ def test_custom_and_spec_extension_mix(): ) def test_parse_observable_with_unregistered_custom_extension(data): with pytest.raises(InvalidValueError) as excinfo: - stix2.parse_observable(data, version='2.1') + stix2.parse(data, version='2.1') assert "Can't parse unknown extension type" in str(excinfo.value) - - parsed_ob = stix2.parse_observable(data, allow_custom=True, version='2.1') + parsed_ob = stix2.parse(data, allow_custom=True, version='2.1') assert parsed_ob['extensions']['x-foobar-ext']['property1'] == 'foo' assert not isinstance(parsed_ob['extensions']['x-foobar-ext'], stix2.base._STIXBase) diff --git a/stix2/test/v21/test_indicator.py b/stix2/test/v21/test_indicator.py index ea46d6d..0562dfd 100644 --- a/stix2/test/v21/test_indicator.py +++ b/stix2/test/v21/test_indicator.py @@ -19,6 +19,7 @@ EXPECTED_INDICATOR = """{ ], "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", "pattern_type": "stix", + "pattern_version": "2.1", "valid_from": "1970-01-01T00:00:01Z" }""" @@ -31,6 +32,7 @@ EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join(""" indicator_types=['malicious-activity'], pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", pattern_type='stix', + pattern_version='2.1', valid_from='1970-01-01T00:00:01Z' """.split()) + ")" diff --git a/stix2/test/v21/test_observed_data.py b/stix2/test/v21/test_observed_data.py index 0074bf7..32bd0bf 100644 --- a/stix2/test/v21/test_observed_data.py +++ b/stix2/test/v21/test_observed_data.py @@ -209,7 +209,7 @@ def test_observed_data_example_with_bad_refs(): assert excinfo.value.cls == stix2.v21.Directory assert excinfo.value.prop_name == "contains_refs" - assert "The type-specifying prefix 'monkey--' for this property is not valid" in excinfo.value.reason + assert "The type-specifying prefix 'monkey' for this property is not valid" in excinfo.value.reason def test_observed_data_example_with_non_dictionary(): @@ -369,7 +369,7 @@ def test_parse_autonomous_system_valid(data): ], ) def test_parse_email_address(data): - odata = stix2.parse_observable(data, version='2.1') + odata = stix2.parse(data, version='2.1') assert odata.type == "email-addr" odata_str = re.compile( @@ -378,7 +378,7 @@ def test_parse_email_address(data): '"belongs_to_ref": "mutex--9be6365f-b89c-48c0-9340-6953f6595718"', data, ) with pytest.raises(stix2.exceptions.InvalidValueError): - stix2.parse_observable(odata_str, version='2.1') + stix2.parse(odata_str, version='2.1') @pytest.mark.parametrize( @@ -424,7 +424,7 @@ def test_parse_email_address(data): ], ) def test_parse_email_message(data): - odata = stix2.parse_observable(data, version='2.1') + odata = stix2.parse(data, version='2.1') assert odata.type == "email-message" assert odata.body_multipart[0].content_disposition == "inline" @@ -446,7 +446,7 @@ def test_parse_email_message(data): ) def test_parse_email_message_not_multipart(data): with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: - stix2.parse_observable(data, version='2.1') + stix2.parse(data, version='2.1') assert excinfo.value.cls == stix2.v21.EmailMessage assert excinfo.value.dependencies == [("is_multipart", "body")] @@ -548,7 +548,7 @@ def test_parse_file_archive(data): ) def test_parse_email_message_with_at_least_one_error(data): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.parse_observable(data, version='2.1') + stix2.parse(data, version='2.1') assert excinfo.value.cls == stix2.v21.EmailMessage assert "At least one of the" in str(excinfo.value) @@ -570,7 +570,7 @@ def test_parse_email_message_with_at_least_one_error(data): ], ) def test_parse_basic_tcp_traffic(data): - odata = stix2.parse_observable( + odata = stix2.parse( data, version='2.1', ) @@ -602,7 +602,7 @@ def test_parse_basic_tcp_traffic(data): ) def test_parse_basic_tcp_traffic_with_error(data): with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: - stix2.parse_observable(data, version='2.1') + stix2.parse(data, version='2.1') assert excinfo.value.cls == stix2.v21.NetworkTraffic assert excinfo.value.properties == ["dst_ref", "src_ref"] @@ -1117,6 +1117,28 @@ def test_network_traffic_socket_example(): assert nt.extensions['socket-ext'].socket_type == "SOCK_STREAM" +def test_incorrect_socket_options(): + with pytest.raises(ValueError) as excinfo: + stix2.v21.SocketExt( + is_listening=True, + address_family="AF_INET", + protocol_family="PF_INET", + socket_type="SOCK_STREAM", + options={"RCVTIMEO": 100}, + ) + assert "Incorrect options key" == str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + stix2.v21.SocketExt( + is_listening=True, + address_family="AF_INET", + protocol_family="PF_INET", + socket_type="SOCK_STREAM", + options={"SO_RCVTIMEO": '100'}, + ) + assert "Options value must be an integer" == str(excinfo.value) + + def test_network_traffic_tcp_example(): h = stix2.v21.TCPExt(src_flags_hex="00000002") nt = stix2.v21.NetworkTraffic( @@ -1366,6 +1388,18 @@ def test_x509_certificate_example(): assert x509.subject == "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=baccala@freesoft.org" # noqa +def test_x509_certificate_error(): + + with pytest.raises(stix2.exceptions.PropertyPresenceError) as excinfo: + stix2.v21.X509Certificate( + defanged=True, + ) + + assert excinfo.value.cls == stix2.v21.X509Certificate + assert "At least one of the" in str(excinfo.value) + assert "properties for X509Certificate must be populated." in str(excinfo.value) + + def test_new_version_with_related_objects(): data = stix2.v21.ObservedData( first_observed="2016-03-12T12:00:00Z", diff --git a/stix2/test/v21/test_properties.py b/stix2/test/v21/test_properties.py index 1fb3cc4..50bce17 100644 --- a/stix2/test/v21/test_properties.py +++ b/stix2/test/v21/test_properties.py @@ -72,6 +72,14 @@ def test_list_property(): p.clean([]) +def test_dictionary_property(): + p = DictionaryProperty(StringProperty) + + assert p.clean({'spec_version': '2.1'}) + with pytest.raises(ValueError): + p.clean({}) + + def test_string_property(): prop = StringProperty() @@ -411,6 +419,7 @@ def test_property_list_of_dictionary(): "value", [ {"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"}, [('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')], + [('TLSH', '6FF02BEF718027B0160B4391212923ED7F1A463D563B1549B86CF62973B197AD2731F8')], ], ) def test_hashes_property_valid(value): @@ -422,6 +431,7 @@ def test_hashes_property_valid(value): "value", [ {"MD5": "a"}, {"SHA-256": "2dfb1bcc980200c6706feee399d41b3f"}, + {"TLSH": "6FF02BEF718027B0160B4391212923ED7F1A463D563B1549B86CF62973B197AD2731F"}, ], ) def test_hashes_property_invalid(value): diff --git a/stix2/utils.py b/stix2/utils.py index ffabed0..b23b0e4 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -1,6 +1,9 @@ """Utility functions and classes for the STIX2 library.""" -from collections import Mapping +try: + from collections.abc import Mapping +except ImportError: + from collections import Mapping import copy import datetime as dt import json diff --git a/stix2/v20/sdo.py b/stix2/v20/sdo.py index 1b1ffdf..50f0fd0 100644 --- a/stix2/v20/sdo.py +++ b/stix2/v20/sdo.py @@ -233,7 +233,7 @@ class Report(STIXDomainObject): ('name', StringProperty(required=True)), ('description', StringProperty()), ('published', TimestampProperty(required=True)), - ('object_refs', ListProperty(ReferenceProperty(invalid_types=[""], spec_version='2.0'), required=True)), + ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.0'), required=True)), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty, required=True)), ('external_references', ListProperty(ExternalReference)), diff --git a/stix2/v20/sro.py b/stix2/v20/sro.py index 584c1ee..63f177b 100644 --- a/stix2/v20/sro.py +++ b/stix2/v20/sro.py @@ -70,7 +70,7 @@ class Sighting(STIXRelationshipObject): ('first_seen', TimestampProperty()), ('last_seen', TimestampProperty()), ('count', IntegerProperty(min=0, max=999999999)), - ('sighting_of_ref', ReferenceProperty(valid_types="only_SDO", spec_version='2.0', required=True)), + ('sighting_of_ref', ReferenceProperty(valid_types="SDO", spec_version='2.0', required=True)), ('observed_data_refs', ListProperty(ReferenceProperty(valid_types='observed-data', spec_version='2.0'))), ('where_sighted_refs', ListProperty(ReferenceProperty(valid_types='identity', spec_version='2.0'))), ('summary', BooleanProperty(default=lambda: False)), diff --git a/stix2/v21/common.py b/stix2/v21/common.py index 1255c37..cf59b1a 100644 --- a/stix2/v21/common.py +++ b/stix2/v21/common.py @@ -76,7 +76,7 @@ class LanguageContent(_STIXBase): ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('object_ref', ReferenceProperty(invalid_types=[""], spec_version='2.1', required=True)), + ('object_ref', ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1', required=True)), # TODO: 'object_modified' it MUST be an exact match for the modified time of the STIX Object (SRO or SDO) being referenced. ('object_modified', TimestampProperty(precision='millisecond')), # TODO: 'contents' https://docs.google.com/document/d/1ShNq4c3e1CkfANmD9O--mdZ5H0O_GLnjN28a_yrEaco/edit#heading=h.cfz5hcantmvx diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py index 0d27bb4..b08869e 100644 --- a/stix2/v21/observables.py +++ b/stix2/v21/observables.py @@ -385,7 +385,7 @@ class File(_Observable): ('mtime', TimestampProperty()), ('atime', TimestampProperty()), ('parent_directory_ref', ReferenceProperty(valid_types='directory', spec_version='2.1')), - ('contains_refs', ListProperty(ReferenceProperty(invalid_types="", spec_version='2.1'))), + ('contains_refs', ListProperty(ReferenceProperty(valid_types=["SCO"], spec_version='2.1'))), ('content_ref', ReferenceProperty(valid_types='artifact', spec_version='2.1')), ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ('spec_version', StringProperty(fixed='2.1')), @@ -592,6 +592,18 @@ class SocketExt(_Extension): ('socket_handle', IntegerProperty()), ]) + def _check_object_constraints(self): + super(SocketExt, self)._check_object_constraints() + + options = self.get('options') + + if options is not None: + for key, val in options.items(): + if key[:3] != "SO_": + raise ValueError("Incorrect options key") + if not isinstance(val, int): + raise ValueError("Options value must be an integer") + class TCPExt(_Extension): # TODO: Add link @@ -986,6 +998,18 @@ class X509Certificate(_Observable): ]) _id_contributing_properties = ["hashes", "serial_number"] + def _check_object_constraints(self): + super(X509Certificate, self)._check_object_constraints() + + att_list = [ + 'is_self_signed', 'hashes', 'version', 'serial_number', + 'signature_algorithm', 'issuer', 'validity_not_before', + 'validity_not_after', 'subject', 'subject_public_key_algorithm', + 'subject_public_key_modulus', 'subject_public_key_exponent', + 'x509_v3_extensions', + ] + self._check_at_least_one_property(att_list) + def CustomObservable(type='x-custom-observable', properties=None): """Custom STIX Cyber Observable Object type decorator. @@ -1004,6 +1028,7 @@ def CustomObservable(type='x-custom-observable', properties=None): def wrapper(cls): _properties = list(itertools.chain.from_iterable([ [('type', TypeProperty(type))], + [('id', IDProperty(type, spec_version='2.1'))], properties, [('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=type))], ])) diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py index 93fe990..797c952 100644 --- a/stix2/v21/sdo.py +++ b/stix2/v21/sdo.py @@ -149,7 +149,7 @@ class Grouping(STIXDomainObject): ('name', StringProperty()), ('description', StringProperty()), ('context', StringProperty(required=True)), - ('object_refs', ListProperty(ReferenceProperty(invalid_types=[""], spec_version='2.1'), required=True)), + ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)), ]) @@ -215,6 +215,13 @@ class Indicator(STIXDomainObject): ('granular_markings', ListProperty(GranularMarking)), ]) + def __init__(self, *args, **kwargs): + + if kwargs.get('pattern') and kwargs.get('pattern_type') == 'stix' and not kwargs.get('pattern_version'): + kwargs['pattern_version'] = '2.1' + + super(STIXDomainObject, self).__init__(*args, **kwargs) + def _check_object_constraints(self): super(Indicator, self)._check_object_constraints() @@ -505,7 +512,7 @@ class MalwareAnalysis(STIXDomainObject): ('analysis_started', TimestampProperty()), ('analysis_ended', TimestampProperty()), ('av_result', StringProperty()), - ('analysis_sco_refs', ListProperty(ReferenceProperty(valid_types="only_SCO", spec_version='2.1'))), + ('analysis_sco_refs', ListProperty(ReferenceProperty(valid_types="SCO", spec_version='2.1'))), ]) def _check_object_constraints(self): @@ -531,7 +538,7 @@ class Note(STIXDomainObject): ('abstract', StringProperty()), ('content', StringProperty(required=True)), ('authors', ListProperty(StringProperty)), - ('object_refs', ListProperty(ReferenceProperty(invalid_types=[""], spec_version='2.1'), required=True)), + ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('confidence', IntegerProperty()), @@ -560,7 +567,7 @@ class ObservedData(STIXDomainObject): ('last_observed', TimestampProperty(required=True)), ('number_observed', IntegerProperty(min=1, max=999999999, required=True)), ('objects', ObservableProperty(spec_version='2.1')), - ('object_refs', ListProperty(ReferenceProperty(valid_types="only_SCO_&_SRO", spec_version="2.1"))), + ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SRO"], spec_version="2.1"))), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('confidence', IntegerProperty()), @@ -625,7 +632,7 @@ class Opinion(STIXDomainObject): ], required=True, ), ), - ('object_refs', ListProperty(ReferenceProperty(invalid_types=[""], spec_version='2.1'), required=True)), + ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('confidence', IntegerProperty()), @@ -654,7 +661,7 @@ class Report(STIXDomainObject): ('description', StringProperty()), ('report_types', ListProperty(StringProperty, required=True)), ('published', TimestampProperty(required=True)), - ('object_refs', ListProperty(ReferenceProperty(invalid_types=[""], spec_version='2.1'), required=True)), + ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('confidence', IntegerProperty()), diff --git a/stix2/v21/sro.py b/stix2/v21/sro.py index 9640874..5ba382b 100644 --- a/stix2/v21/sro.py +++ b/stix2/v21/sro.py @@ -89,7 +89,7 @@ class Sighting(STIXRelationshipObject): ('first_seen', TimestampProperty()), ('last_seen', TimestampProperty()), ('count', IntegerProperty(min=0, max=999999999)), - ('sighting_of_ref', ReferenceProperty(valid_types="only_SDO", spec_version='2.1', required=True)), + ('sighting_of_ref', ReferenceProperty(valid_types="SDO", spec_version='2.1', required=True)), ('observed_data_refs', ListProperty(ReferenceProperty(valid_types='observed-data', spec_version='2.1'))), ('where_sighted_refs', ListProperty(ReferenceProperty(valid_types='identity', spec_version='2.1'))), ('summary', BooleanProperty()),