diff --git a/stix2/base.py b/stix2/base.py index 4248075..75bb0ab 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -2,6 +2,7 @@ import copy import datetime as dt +import re import uuid import simplejson as json @@ -17,7 +18,7 @@ from .exceptions import ( from .markings.utils import validate from .utils import NOW, find_property_index, format_datetime, get_timestamp from .utils import new_version as _new_version -from .utils import revoke as _revoke +from .utils import revoke as _revoke, PREFIX_21_REGEX try: from collections.abc import Mapping @@ -76,6 +77,11 @@ def get_required_properties(properties): class _STIXBase(Mapping): """Base class for STIX object types""" + def get_class_version(self): + module_name = self.__class__.__module__ + module_parts = module_name.split(".") + return module_parts[1] + def object_properties(self): props = set(self._properties.keys()) custom_props = list(set(self._inner.keys()) - props) @@ -163,6 +169,11 @@ class _STIXBase(Mapping): raise ExtraPropertiesError(cls, extra_kwargs) if custom_props: self._allow_custom = True + if self.get_class_version() == "v21": + for prop_name, prop_value in custom_props.items(): + if not re.match(PREFIX_21_REGEX, prop_name): + raise InvalidValueError(self.__class__, prop_name, + reason="Property names must begin with an alpha character.") # Remove any keyword arguments whose value is None or [] (i.e. empty list) setting_kwargs = {} diff --git a/stix2/core.py b/stix2/core.py index b03e3d7..cf97344 100644 --- a/stix2/core.py +++ b/stix2/core.py @@ -10,7 +10,7 @@ import stix2 from .base import _Observable, _STIXBase from .exceptions import ParseError from .markings import _MarkingsMixin -from .utils import SCO21_EXT_REGEX, TYPE_REGEX, _get_dict +from .utils import _get_dict, SCO21_EXT_REGEX, TYPE_REGEX STIX2_OBJ_MAPS = {} @@ -210,6 +210,7 @@ def _register_object(new_type, version=None): None, use latest version. """ + if version: v = 'v' + version.replace('.', '') else: @@ -248,6 +249,7 @@ def _register_observable(new_observable, version=None): None, use latest version. """ + if version: v = 'v' + version.replace('.', '') else: @@ -289,8 +291,9 @@ def _register_observable_extension( if not re.match(SCO21_EXT_REGEX, ext_type): raise ValueError( "Invalid extension type name '%s': must only contain the " - "characters a-z (lowercase ASCII), 0-9, hyphen (-), and end " - "with '-ext'." % ext_type, + "characters a-z (lowercase ASCII), 0-9, hyphen (-), " + "must begin with an a-z character" + "and end with '-ext'." % ext_type, ) if len(ext_type) < 3 or len(ext_type) > 250: diff --git a/stix2/custom.py b/stix2/custom.py index f3c89cf..e73a16a 100644 --- a/stix2/custom.py +++ b/stix2/custom.py @@ -8,18 +8,28 @@ from .core import ( STIXDomainObject, _register_marking, _register_object, _register_observable, _register_observable_extension, ) -from .utils import TYPE_REGEX, get_class_hierarchy_names +from .utils import get_class_hierarchy_names, SCO21_TYPE_REGEX, TYPE_REGEX, PREFIX_21_REGEX def _custom_object_builder(cls, type, properties, version): class _CustomObject(cls, STIXDomainObject): - if not re.match(TYPE_REGEX, type): - raise ValueError( - "Invalid type name '%s': must only contain the " - "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % type, - ) - elif len(type) < 3 or len(type) > 250: + if version == "2.0": + if not re.match(TYPE_REGEX, type): + raise ValueError( + "Invalid type type name '%s': must only contain the " + "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % + type, + ) + else: # 2.1+ + if not re.match(SCO21_TYPE_REGEX, type): + raise ValueError( + "Invalid type name '%s': must only contain the " + "characters a-z (lowercase ASCII), 0-9, and hyphen (-) " + "and must begin with an a-z character" % type, + ) + + if len(type) < 3 or len(type) > 250: raise ValueError( "Invalid type name '%s': must be between 3 and 250 characters." % type, ) @@ -27,6 +37,11 @@ def _custom_object_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())]") + if version == "2.1": + for prop_name, prop in properties: + if not re.match(r'^[a-z]', prop_name): + raise ValueError("Property name %s must begin with an alpha character" % prop_name) + _type = type _properties = OrderedDict(properties) @@ -41,9 +56,34 @@ def _custom_object_builder(cls, type, properties, version): def _custom_marking_builder(cls, type, properties, version): class _CustomMarking(cls, _STIXBase): + if version == "2.0": + if not re.match(TYPE_REGEX, type): + raise ValueError( + "Invalid type type name '%s': must only contain the " + "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % + type, + ) + else: # 2.1+ + if not re.match(SCO21_TYPE_REGEX, type): + raise ValueError( + "Invalid type name '%s': must only contain the " + "characters a-z (lowercase ASCII), 0-9, and hyphen (-) " + "and must begin with an a-z character" % type, + ) + + if len(type) < 3 or len(type) > 250: + raise ValueError( + "Invalid type name '%s': must be between 3 and 250 characters." % type, + ) + if not properties or not isinstance(properties, list): raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") + if version == "2.1": + for prop_name, prop in properties: + if not re.match(PREFIX_21_REGEX, prop_name): + raise ValueError("Property name %s must begin with an alpha character." % prop_name) + _type = type _properties = OrderedDict(properties) @@ -61,12 +101,22 @@ def _custom_observable_builder(cls, type, properties, version, id_contrib_props= class _CustomObservable(cls, _Observable): - if not re.match(TYPE_REGEX, type): - raise ValueError( - "Invalid observable type name '%s': must only contain the " - "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % type, - ) - elif len(type) < 3 or len(type) > 250: + if version == "2.0": + if not re.match(TYPE_REGEX, type): + raise ValueError( + "Invalid type type name '%s': must only contain the " + "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % + type, + ) + else: # 2.1+ + if not re.match(SCO21_TYPE_REGEX, type): + raise ValueError( + "Invalid observable type name '%s': must only contain the " + "characters a-z (lowercase ASCII), 0-9, and hyphen (-) " + "and must begin with an a-z character" % type, + ) + + if len(type) < 3 or len(type) > 250: raise ValueError("Invalid observable type name '%s': must be between 3 and 250 characters." % type) if not properties or not isinstance(properties, list): @@ -89,7 +139,9 @@ def _custom_observable_builder(cls, type, properties, version, id_contrib_props= 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)): + if not re.match(PREFIX_21_REGEX, prop_name): + raise ValueError("Property name %s must begin with an alpha character." % prop_name) + elif 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, @@ -130,6 +182,11 @@ def _custom_extension_builder(cls, observable, type, properties, version): class _CustomExtension(cls, _Extension): + if version == "2.1": + for prop_name, prop_value in prop_dict.items(): + if not re.match(PREFIX_21_REGEX, prop_name): + raise ValueError("Property name %s must begin with an alpha character." % prop_name) + _type = type _properties = prop_dict diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py index 1e6f629..33cffd5 100644 --- a/stix2/test/v21/test_custom.py +++ b/stix2/test/v21/test_custom.py @@ -43,6 +43,19 @@ def test_identity_custom_property(): ) assert "Unexpected properties for Identity" in str(excinfo.value) + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Identity( + id=IDENTITY_ID, + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + custom_properties={ + "7foo": "bar", + }, + ) + assert "Property names must begin with an alpha character." in str(excinfo.value) + identity = stix2.v21.Identity( id=IDENTITY_ID, created="2015-12-21T19:59:11Z", @@ -184,6 +197,18 @@ def test_custom_property_in_observed_data(): assert '"x_foo": "bar"' in str(observed_data) +def test_invalid_custom_property_in_observed_data(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.File( + custom_properties={"8foo": 1}, + allow_custom=True, + name='test', + x_foo='bar', + ) + + assert "Property names must begin with an alpha character." in str(excinfo.value) + + def test_custom_property_object_in_observable_extension(): ntfs = stix2.v21.NTFSExt( allow_custom=True, @@ -293,6 +318,38 @@ def test_custom_marking_no_init_2(): assert no2.property1 == 'something' +def test_custom_marking_invalid_type_name(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomMarking( + 'x', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj(object): + pass # pragma: no cover + assert "Invalid type name 'x': " in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomMarking( + 'x_new_marking', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj2(object): + pass # pragma: no cover + assert "Invalid type name 'x_new_marking':" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomMarking( + '7x-new-marking', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj3(object): + pass # pragma: no cover + assert "Invalid type name '7x-new-marking':" in str(excinfo.value) + + @stix2.v21.CustomObject( 'x-new-type', [ ('property1', stix2.properties.StringProperty(required=True)), @@ -374,6 +431,17 @@ def test_custom_object_invalid_type_name(): pass # pragma: no cover assert "Invalid type name 'x_new_object':" in str(excinfo.value) + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomObject( + '7x-new-object', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj3(object): + pass # pragma: no cover + assert "Invalid type name '7x-new-object':" in str(excinfo.value) + + def test_parse_custom_object_type(): nt_string = """{ @@ -500,6 +568,17 @@ def test_custom_observable_object_invalid_type_name(): pass # pragma: no cover assert "Invalid observable type name 'x_new_obs':" in str(excinfo.value) + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomObservable( + '7x-new-obs', [ + ('property1', stix2.properties.StringProperty()), + ], + ) + class NewObs3(object): + pass # pragma: no cover + assert "Invalid observable type name '7x-new-obs':" in str(excinfo.value) + + def test_custom_observable_object_invalid_ref_property(): with pytest.raises(ValueError) as excinfo: @@ -874,6 +953,17 @@ def test_custom_extension_invalid_type_name(): pass # pragma: no cover assert "Invalid extension type name 'x_new_ext':" in str(excinfo.value) + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomExtension( + stix2.v21.File, '7x-new-ext', { + 'property1': stix2.properties.StringProperty(required=True), + }, + ) + class Bla2Extension(): + pass # pragma: no cover + assert "Invalid extension type name '7x-new-ext':" in str(excinfo.value) + + def test_custom_extension_no_properties(): with pytest.raises(ValueError): diff --git a/stix2/utils.py b/stix2/utils.py index 7b3b6cf..4d75f8e 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -7,6 +7,7 @@ except ImportError: import copy import datetime as dt import json +import re from dateutil import parser import pytz @@ -25,8 +26,10 @@ NOW = object() # STIX object properties that cannot be modified STIX_UNMOD_PROPERTIES = ['created', 'created_by_ref', 'id', 'type'] -TYPE_REGEX = r'^\-?[a-z0-9]+(-[a-z0-9]+)*\-?$' -SCO21_EXT_REGEX = r'^\-?[a-z0-9]+(-[a-z0-9]+)*\-ext$' +TYPE_REGEX = re.compile(r'^\-?[a-z0-9]+(-[a-z0-9]+)*\-?$') +SCO21_TYPE_REGEX = re.compile(r'^\-?([a-z][a-z0-9]*)+(-[a-z0-9]+)*\-?$') +SCO21_EXT_REGEX = re.compile(r'^\-?([a-z][a-z0-9]*)+(-[a-z0-9]+)*\-ext$') +PREFIX_21_REGEX = re.compile(r'^[a-z].*') class STIXdatetime(dt.datetime):