Add enforcement of a new STIX 2.1 SCO extension name requirement:
that it must end with "-ext".master
parent
380926cff5
commit
d708537b85
|
@ -7,10 +7,10 @@ import re
|
||||||
|
|
||||||
import stix2
|
import stix2
|
||||||
|
|
||||||
from .base import _STIXBase
|
from .base import _Observable, _STIXBase
|
||||||
from .exceptions import ParseError
|
from .exceptions import ParseError
|
||||||
from .markings import _MarkingsMixin
|
from .markings import _MarkingsMixin
|
||||||
from .utils import _get_dict
|
from .utils import SCO21_EXT_REGEX, TYPE_REGEX, _get_dict
|
||||||
|
|
||||||
STIX2_OBJ_MAPS = {}
|
STIX2_OBJ_MAPS = {}
|
||||||
|
|
||||||
|
@ -258,22 +258,54 @@ def _register_observable(new_observable, version=None):
|
||||||
OBJ_MAP_OBSERVABLE[new_observable._type] = new_observable
|
OBJ_MAP_OBSERVABLE[new_observable._type] = new_observable
|
||||||
|
|
||||||
|
|
||||||
def _register_observable_extension(observable, new_extension, version=None):
|
def _register_observable_extension(
|
||||||
|
observable, new_extension, version=stix2.DEFAULT_VERSION
|
||||||
|
):
|
||||||
"""Register a custom extension to a STIX Cyber Observable type.
|
"""Register a custom extension to a STIX Cyber Observable type.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
observable: An observable object
|
observable: An observable class or instance
|
||||||
new_extension (class): A class to register in the Observables
|
new_extension (class): A class to register in the Observables
|
||||||
Extensions map.
|
Extensions map.
|
||||||
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If
|
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1").
|
||||||
None, use latest version.
|
Defaults to the latest supported version.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if version:
|
obs_class = observable if isinstance(observable, type) else \
|
||||||
v = 'v' + version.replace('.', '')
|
type(observable)
|
||||||
else:
|
ext_type = new_extension._type
|
||||||
# Use default version (latest) if no version was provided.
|
|
||||||
v = 'v' + stix2.DEFAULT_VERSION.replace('.', '')
|
if not issubclass(obs_class, _Observable):
|
||||||
|
raise ValueError("'observable' must be a valid Observable class!")
|
||||||
|
|
||||||
|
if version == "2.0":
|
||||||
|
if not re.match(TYPE_REGEX, ext_type):
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid extension type name '%s': must only contain the "
|
||||||
|
"characters a-z (lowercase ASCII), 0-9, and hyphen (-)." %
|
||||||
|
ext_type,
|
||||||
|
)
|
||||||
|
else: # 2.1+
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(ext_type) < 3 or len(ext_type) > 250:
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid extension type name '%s': must be between 3 and 250"
|
||||||
|
" characters." % ext_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not new_extension._properties:
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid extension: must define at least one property: " +
|
||||||
|
ext_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
v = 'v' + version.replace('.', '')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
observable_type = observable._type
|
observable_type = observable._type
|
||||||
|
@ -287,7 +319,7 @@ def _register_observable_extension(observable, new_extension, version=None):
|
||||||
EXT_MAP = STIX2_OBJ_MAPS[v]['observable-extensions']
|
EXT_MAP = STIX2_OBJ_MAPS[v]['observable-extensions']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
EXT_MAP[observable_type][new_extension._type] = new_extension
|
EXT_MAP[observable_type][ext_type] = new_extension
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if observable_type not in OBJ_MAP_OBSERVABLE:
|
if observable_type not in OBJ_MAP_OBSERVABLE:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -296,7 +328,7 @@ def _register_observable_extension(observable, new_extension, version=None):
|
||||||
% observable_type,
|
% observable_type,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
EXT_MAP[observable_type] = {new_extension._type: new_extension}
|
EXT_MAP[observable_type] = {ext_type: new_extension}
|
||||||
|
|
||||||
|
|
||||||
def _collect_stix2_mappings():
|
def _collect_stix2_mappings():
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
from .base import _cls_init, _Extension, _Observable, _STIXBase
|
from .base import _cls_init, _Extension, _Observable, _STIXBase
|
||||||
from .core import (
|
from .core import (
|
||||||
STIXDomainObject, _register_marking, _register_object,
|
STIXDomainObject, _register_marking, _register_object,
|
||||||
|
@ -113,24 +115,23 @@ def _custom_observable_builder(cls, type, properties, version, id_contrib_props=
|
||||||
|
|
||||||
|
|
||||||
def _custom_extension_builder(cls, observable, type, properties, version):
|
def _custom_extension_builder(cls, observable, type, properties, version):
|
||||||
if not observable or not issubclass(observable, _Observable):
|
|
||||||
raise ValueError("'observable' must be a valid Observable class!")
|
try:
|
||||||
|
prop_dict = OrderedDict(properties)
|
||||||
|
except TypeError as e:
|
||||||
|
six.raise_from(
|
||||||
|
ValueError(
|
||||||
|
"Extension properties must be dict-like, e.g. a list "
|
||||||
|
"containing tuples. For example, "
|
||||||
|
"[('property1', IntegerProperty())]",
|
||||||
|
),
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
|
||||||
class _CustomExtension(cls, _Extension):
|
class _CustomExtension(cls, _Extension):
|
||||||
|
|
||||||
if not re.match(TYPE_REGEX, type):
|
|
||||||
raise ValueError(
|
|
||||||
"Invalid extension 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:
|
|
||||||
raise ValueError("Invalid extension 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())]")
|
|
||||||
|
|
||||||
_type = type
|
_type = type
|
||||||
_properties = OrderedDict(properties)
|
_properties = prop_dict
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
_Extension.__init__(self, **kwargs)
|
_Extension.__init__(self, **kwargs)
|
||||||
|
|
|
@ -821,27 +821,24 @@ def test_custom_extension_invalid_type_name():
|
||||||
|
|
||||||
|
|
||||||
def test_custom_extension_no_properties():
|
def test_custom_extension_no_properties():
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError):
|
||||||
@stix2.v20.CustomExtension(stix2.v20.DomainName, 'x-new-ext2', None)
|
@stix2.v20.CustomExtension(stix2.v20.DomainName, 'x-new-ext2', None)
|
||||||
class BarExtension():
|
class BarExtension():
|
||||||
pass
|
pass
|
||||||
assert "Must supply a list, containing tuples." in str(excinfo.value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_custom_extension_empty_properties():
|
def test_custom_extension_empty_properties():
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError):
|
||||||
@stix2.v20.CustomExtension(stix2.v20.DomainName, 'x-new-ext2', [])
|
@stix2.v20.CustomExtension(stix2.v20.DomainName, 'x-new-ext2', [])
|
||||||
class BarExtension():
|
class BarExtension():
|
||||||
pass
|
pass
|
||||||
assert "Must supply a list, containing tuples." in str(excinfo.value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_custom_extension_dict_properties():
|
def test_custom_extension_dict_properties():
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError):
|
||||||
@stix2.v20.CustomExtension(stix2.v20.DomainName, 'x-new-ext2', {})
|
@stix2.v20.CustomExtension(stix2.v20.DomainName, 'x-new-ext2', {})
|
||||||
class BarExtension():
|
class BarExtension():
|
||||||
pass
|
pass
|
||||||
assert "Must supply a list, containing tuples." in str(excinfo.value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_custom_extension_no_init_1():
|
def test_custom_extension_no_init_1():
|
||||||
|
|
|
@ -800,7 +800,7 @@ def test_custom_extension_wrong_observable_type():
|
||||||
)
|
)
|
||||||
def test_custom_extension_with_list_and_dict_properties_observable_type(data):
|
def test_custom_extension_with_list_and_dict_properties_observable_type(data):
|
||||||
@stix2.v21.CustomExtension(
|
@stix2.v21.CustomExtension(
|
||||||
stix2.v21.UserAccount, 'some-extension', [
|
stix2.v21.UserAccount, 'some-extension-ext', [
|
||||||
('keys', stix2.properties.ListProperty(stix2.properties.DictionaryProperty, required=True)),
|
('keys', stix2.properties.ListProperty(stix2.properties.DictionaryProperty, required=True)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -876,32 +876,29 @@ def test_custom_extension_invalid_type_name():
|
||||||
|
|
||||||
|
|
||||||
def test_custom_extension_no_properties():
|
def test_custom_extension_no_properties():
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError):
|
||||||
@stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new-ext2', None)
|
@stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new2-ext', None)
|
||||||
class BarExtension():
|
class BarExtension():
|
||||||
pass
|
pass
|
||||||
assert "Must supply a list, containing tuples." in str(excinfo.value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_custom_extension_empty_properties():
|
def test_custom_extension_empty_properties():
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError):
|
||||||
@stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new-ext2', [])
|
@stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new2-ext', [])
|
||||||
class BarExtension():
|
class BarExtension():
|
||||||
pass
|
pass
|
||||||
assert "Must supply a list, containing tuples." in str(excinfo.value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_custom_extension_dict_properties():
|
def test_custom_extension_dict_properties():
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError):
|
||||||
@stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new-ext2', {})
|
@stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new2-ext', {})
|
||||||
class BarExtension():
|
class BarExtension():
|
||||||
pass
|
pass
|
||||||
assert "Must supply a list, containing tuples." in str(excinfo.value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_custom_extension_no_init_1():
|
def test_custom_extension_no_init_1():
|
||||||
@stix2.v21.CustomExtension(
|
@stix2.v21.CustomExtension(
|
||||||
stix2.v21.DomainName, 'x-new-extension', [
|
stix2.v21.DomainName, 'x-new-extension-ext', [
|
||||||
('property1', stix2.properties.StringProperty(required=True)),
|
('property1', stix2.properties.StringProperty(required=True)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -914,7 +911,7 @@ def test_custom_extension_no_init_1():
|
||||||
|
|
||||||
def test_custom_extension_no_init_2():
|
def test_custom_extension_no_init_2():
|
||||||
@stix2.v21.CustomExtension(
|
@stix2.v21.CustomExtension(
|
||||||
stix2.v21.DomainName, 'x-new-ext2', [
|
stix2.v21.DomainName, 'x-new2-ext', [
|
||||||
('property1', stix2.properties.StringProperty(required=True)),
|
('property1', stix2.properties.StringProperty(required=True)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -949,14 +946,14 @@ def test_custom_and_spec_extension_mix():
|
||||||
file_obs = stix2.v21.File(
|
file_obs = stix2.v21.File(
|
||||||
name="my_file.dat",
|
name="my_file.dat",
|
||||||
extensions={
|
extensions={
|
||||||
"x-custom1": {
|
"custom1-ext": {
|
||||||
"a": 1,
|
"a": 1,
|
||||||
"b": 2,
|
"b": 2,
|
||||||
},
|
},
|
||||||
"ntfs-ext": {
|
"ntfs-ext": {
|
||||||
"sid": "S-1-whatever",
|
"sid": "S-1-whatever",
|
||||||
},
|
},
|
||||||
"x-custom2": {
|
"custom2-ext": {
|
||||||
"z": 99.9,
|
"z": 99.9,
|
||||||
"y": False,
|
"y": False,
|
||||||
},
|
},
|
||||||
|
@ -969,8 +966,8 @@ def test_custom_and_spec_extension_mix():
|
||||||
allow_custom=True,
|
allow_custom=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert file_obs.extensions["x-custom1"] == {"a": 1, "b": 2}
|
assert file_obs.extensions["custom1-ext"] == {"a": 1, "b": 2}
|
||||||
assert file_obs.extensions["x-custom2"] == {"y": False, "z": 99.9}
|
assert file_obs.extensions["custom2-ext"] == {"y": False, "z": 99.9}
|
||||||
assert file_obs.extensions["ntfs-ext"].sid == "S-1-whatever"
|
assert file_obs.extensions["ntfs-ext"].sid == "S-1-whatever"
|
||||||
assert file_obs.extensions["raster-image-ext"].image_height == 1024
|
assert file_obs.extensions["raster-image-ext"].image_height == 1024
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ NOW = object()
|
||||||
STIX_UNMOD_PROPERTIES = ['created', 'created_by_ref', 'id', 'type']
|
STIX_UNMOD_PROPERTIES = ['created', 'created_by_ref', 'id', 'type']
|
||||||
|
|
||||||
TYPE_REGEX = r'^\-?[a-z0-9]+(-[a-z0-9]+)*\-?$'
|
TYPE_REGEX = r'^\-?[a-z0-9]+(-[a-z0-9]+)*\-?$'
|
||||||
|
SCO21_EXT_REGEX = r'^\-?[a-z0-9]+(-[a-z0-9]+)*\-ext$'
|
||||||
|
|
||||||
|
|
||||||
class STIXdatetime(dt.datetime):
|
class STIXdatetime(dt.datetime):
|
||||||
|
|
Loading…
Reference in New Issue