Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New STIX 2.1 SCO extension name requirement: must end with "-ext" #370

Merged
merged 3 commits into from
Mar 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 45 additions & 13 deletions stix2/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

import stix2

from .base import _STIXBase
from .base import _Observable, _STIXBase
from .exceptions import ParseError
from .markings import _MarkingsMixin
from .utils import _get_dict
from .utils import SCO21_EXT_REGEX, TYPE_REGEX, _get_dict

STIX2_OBJ_MAPS = {}

Expand Down Expand Up @@ -258,22 +258,54 @@ def _register_observable(new_observable, version=None):
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.

Args:
observable: An observable object
observable: An observable class or instance
new_extension (class): A class to register in the Observables
Extensions map.
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If
None, use latest version.
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1").
Defaults to the latest supported version.

"""
if version:
v = 'v' + version.replace('.', '')
else:
# Use default version (latest) if no version was provided.
v = 'v' + stix2.DEFAULT_VERSION.replace('.', '')
obs_class = observable if isinstance(observable, type) else \
type(observable)
ext_type = new_extension._type

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:
observable_type = observable._type
Expand All @@ -287,7 +319,7 @@ def _register_observable_extension(observable, new_extension, version=None):
EXT_MAP = STIX2_OBJ_MAPS[v]['observable-extensions']

try:
EXT_MAP[observable_type][new_extension._type] = new_extension
EXT_MAP[observable_type][ext_type] = new_extension
except KeyError:
if observable_type not in OBJ_MAP_OBSERVABLE:
raise ValueError(
Expand All @@ -296,7 +328,7 @@ def _register_observable_extension(observable, new_extension, version=None):
% observable_type,
)
else:
EXT_MAP[observable_type] = {new_extension._type: new_extension}
EXT_MAP[observable_type] = {ext_type: new_extension}


def _collect_stix2_mappings():
Expand Down
29 changes: 15 additions & 14 deletions stix2/custom.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from collections import OrderedDict
import re

import six

from .base import _cls_init, _Extension, _Observable, _STIXBase
from .core import (
STIXDomainObject, _register_marking, _register_object,
Expand Down Expand Up @@ -113,24 +115,23 @@ def __init__(self, **kwargs):


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!")

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)
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,
)

if not properties or not isinstance(properties, list):
raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]")
class _CustomExtension(cls, _Extension):

_type = type
_properties = OrderedDict(properties)
_properties = prop_dict

def __init__(self, **kwargs):
_Extension.__init__(self, **kwargs)
Expand Down
9 changes: 3 additions & 6 deletions stix2/test/v20/test_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,27 +821,24 @@ class BlaExtension():


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)
class BarExtension():
pass
assert "Must supply a list, containing tuples." in str(excinfo.value)


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', [])
class BarExtension():
pass
assert "Must supply a list, containing tuples." in str(excinfo.value)


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', {})
class BarExtension():
pass
assert "Must supply a list, containing tuples." in str(excinfo.value)


def test_custom_extension_no_init_1():
Expand Down
29 changes: 13 additions & 16 deletions stix2/test/v21/test_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,7 @@ def test_custom_extension_wrong_observable_type():
)
def test_custom_extension_with_list_and_dict_properties_observable_type(data):
@stix2.v21.CustomExtension(
stix2.v21.UserAccount, 'some-extension', [
stix2.v21.UserAccount, 'x-some-extension-ext', [
('keys', stix2.properties.ListProperty(stix2.properties.DictionaryProperty, required=True)),
],
)
Expand Down Expand Up @@ -876,32 +876,29 @@ class BlaExtension():


def test_custom_extension_no_properties():
with pytest.raises(ValueError) as excinfo:
@stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new-ext2', None)
with pytest.raises(ValueError):
@stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new2-ext', None)
class BarExtension():
pass
assert "Must supply a list, containing tuples." in str(excinfo.value)


def test_custom_extension_empty_properties():
with pytest.raises(ValueError) as excinfo:
@stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new-ext2', [])
with pytest.raises(ValueError):
@stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new2-ext', [])
class BarExtension():
pass
assert "Must supply a list, containing tuples." in str(excinfo.value)


def test_custom_extension_dict_properties():
with pytest.raises(ValueError) as excinfo:
@stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new-ext2', {})
with pytest.raises(ValueError):
@stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new2-ext', {})
class BarExtension():
pass
assert "Must supply a list, containing tuples." in str(excinfo.value)


def test_custom_extension_no_init_1():
@stix2.v21.CustomExtension(
stix2.v21.DomainName, 'x-new-extension', [
stix2.v21.DomainName, 'x-new-extension-ext', [
('property1', stix2.properties.StringProperty(required=True)),
],
)
Expand All @@ -914,7 +911,7 @@ class NewExt():

def test_custom_extension_no_init_2():
@stix2.v21.CustomExtension(
stix2.v21.DomainName, 'x-new-ext2', [
stix2.v21.DomainName, 'x-new2-ext', [
('property1', stix2.properties.StringProperty(required=True)),
],
)
Expand Down Expand Up @@ -949,14 +946,14 @@ def test_custom_and_spec_extension_mix():
file_obs = stix2.v21.File(
name="my_file.dat",
extensions={
"x-custom1": {
"x-custom1-ext": {
"a": 1,
"b": 2,
},
"ntfs-ext": {
"sid": "S-1-whatever",
},
"x-custom2": {
"x-custom2-ext": {
"z": 99.9,
"y": False,
},
Expand All @@ -969,8 +966,8 @@ def test_custom_and_spec_extension_mix():
allow_custom=True,
)

assert file_obs.extensions["x-custom1"] == {"a": 1, "b": 2}
assert file_obs.extensions["x-custom2"] == {"y": False, "z": 99.9}
assert file_obs.extensions["x-custom1-ext"] == {"a": 1, "b": 2}
assert file_obs.extensions["x-custom2-ext"] == {"y": False, "z": 99.9}
assert file_obs.extensions["ntfs-ext"].sid == "S-1-whatever"
assert file_obs.extensions["raster-image-ext"].image_height == 1024

Expand Down
1 change: 1 addition & 0 deletions stix2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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$'


class STIXdatetime(dt.datetime):
Expand Down