2018-11-28 22:51:00 +01:00
|
|
|
"""STIX2 Core Objects and Methods."""
|
|
|
|
|
2018-07-10 20:56:31 +02:00
|
|
|
import copy
|
2017-11-02 12:48:37 +01:00
|
|
|
import importlib
|
|
|
|
import pkgutil
|
2018-07-25 20:06:18 +02:00
|
|
|
import re
|
2017-11-02 12:48:37 +01:00
|
|
|
|
|
|
|
import stix2
|
2018-06-26 18:29:20 +02:00
|
|
|
|
2020-03-11 01:24:53 +01:00
|
|
|
from .base import _Observable, _STIXBase
|
2019-11-06 16:11:12 +01:00
|
|
|
from .exceptions import ParseError
|
2018-07-10 20:56:31 +02:00
|
|
|
from .markings import _MarkingsMixin
|
2020-03-11 01:24:53 +01:00
|
|
|
from .utils import SCO21_EXT_REGEX, TYPE_REGEX, _get_dict
|
2017-08-11 22:18:20 +02:00
|
|
|
|
2017-11-02 12:48:37 +01:00
|
|
|
STIX2_OBJ_MAPS = {}
|
|
|
|
|
|
|
|
|
2018-07-10 20:56:31 +02:00
|
|
|
class STIXDomainObject(_STIXBase, _MarkingsMixin):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class STIXRelationshipObject(_STIXBase, _MarkingsMixin):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2017-11-02 12:48:37 +01:00
|
|
|
def parse(data, allow_custom=False, version=None):
|
2018-03-30 19:21:07 +02:00
|
|
|
"""Convert a string, dict or file-like object into a STIX object.
|
2017-08-11 22:18:20 +02:00
|
|
|
|
|
|
|
Args:
|
2017-09-08 18:39:36 +02:00
|
|
|
data (str, dict, file-like object): The STIX 2 content to be parsed.
|
2018-03-29 17:49:30 +02:00
|
|
|
allow_custom (bool): Whether to allow custom properties as well unknown
|
|
|
|
custom objects. Note that unknown custom objects cannot be parsed
|
|
|
|
into STIX objects, and will be returned as is. Default: False.
|
2018-11-01 13:17:34 +01:00
|
|
|
version (str): If present, it forces the parser to use the version
|
|
|
|
provided. Otherwise, the library will make the best effort based
|
2018-12-11 19:22:04 +01:00
|
|
|
on checking the "spec_version" property. If none of the above are
|
|
|
|
possible, it will use the default version specified by the library.
|
2017-08-11 22:18:20 +02:00
|
|
|
|
|
|
|
Returns:
|
|
|
|
An instantiated Python STIX object.
|
|
|
|
|
2018-06-25 16:06:07 +02:00
|
|
|
Warnings:
|
|
|
|
'allow_custom=True' will allow for the return of any supplied STIX
|
|
|
|
dict(s) that cannot be found to map to any known STIX object types
|
|
|
|
(both STIX2 domain objects or defined custom STIX2 objects); NO
|
|
|
|
validation is done. This is done to allow the processing of possibly
|
|
|
|
unknown custom STIX objects (example scenario: I need to query a
|
|
|
|
third-party TAXII endpoint that could provide custom STIX objects that
|
|
|
|
I don't know about ahead of time)
|
2018-03-30 19:21:07 +02:00
|
|
|
|
2018-03-29 17:49:30 +02:00
|
|
|
"""
|
|
|
|
# convert STIX object to dict, if not already
|
2018-04-13 17:08:03 +02:00
|
|
|
obj = _get_dict(data)
|
2018-03-29 17:49:30 +02:00
|
|
|
|
|
|
|
# convert dict to full python-stix2 obj
|
|
|
|
obj = dict_to_stix2(obj, allow_custom, version)
|
|
|
|
|
|
|
|
return obj
|
|
|
|
|
|
|
|
|
2020-02-08 00:17:12 +01:00
|
|
|
def _detect_spec_version(stix_dict):
|
|
|
|
"""
|
|
|
|
Given a dict representing a STIX object, try to detect what spec version
|
|
|
|
it is likely to comply with.
|
|
|
|
|
|
|
|
:param stix_dict: A dict with some STIX content. Must at least have a
|
|
|
|
"type" property.
|
|
|
|
:return: A string in "vXX" format, where "XX" indicates the spec version,
|
|
|
|
e.g. "v20", "v21", etc.
|
|
|
|
"""
|
|
|
|
|
|
|
|
obj_type = stix_dict["type"]
|
|
|
|
|
|
|
|
if 'spec_version' in stix_dict:
|
|
|
|
# For STIX 2.0, applies to bundles only.
|
|
|
|
# For STIX 2.1+, applies to SCOs, SDOs, SROs, and markings only.
|
|
|
|
v = 'v' + stix_dict['spec_version'].replace('.', '')
|
|
|
|
elif "id" not in stix_dict:
|
|
|
|
# Only 2.0 SCOs don't have ID properties
|
|
|
|
v = "v20"
|
|
|
|
elif obj_type == 'bundle':
|
|
|
|
# Bundle without a spec_version property: must be 2.1. But to
|
|
|
|
# future-proof, use max version over all contained SCOs, with 2.1
|
|
|
|
# minimum.
|
|
|
|
v = max(
|
|
|
|
"v21",
|
|
|
|
max(
|
|
|
|
_detect_spec_version(obj) for obj in stix_dict["objects"]
|
2020-02-08 00:58:45 +01:00
|
|
|
),
|
2020-02-08 00:17:12 +01:00
|
|
|
)
|
|
|
|
elif obj_type in STIX2_OBJ_MAPS["v21"]["observables"]:
|
|
|
|
# Non-bundle object with an ID and without spec_version. Could be a
|
|
|
|
# 2.1 SCO or 2.0 SDO/SRO/marking. Check for 2.1 SCO...
|
|
|
|
v = "v21"
|
|
|
|
else:
|
|
|
|
# Not a 2.1 SCO; must be a 2.0 object.
|
|
|
|
v = "v20"
|
|
|
|
|
|
|
|
return v
|
|
|
|
|
|
|
|
|
2018-03-29 17:49:30 +02:00
|
|
|
def dict_to_stix2(stix_dict, allow_custom=False, version=None):
|
|
|
|
"""convert dictionary to full python-stix2 object
|
|
|
|
|
2018-11-01 13:17:34 +01:00
|
|
|
Args:
|
|
|
|
stix_dict (dict): a python dictionary of a STIX object
|
|
|
|
that (presumably) is semantically correct to be parsed
|
|
|
|
into a full python-stix2 obj
|
|
|
|
allow_custom (bool): Whether to allow custom properties as well
|
|
|
|
unknown custom objects. Note that unknown custom objects cannot
|
|
|
|
be parsed into STIX objects, and will be returned as is.
|
|
|
|
Default: False.
|
|
|
|
version (str): If present, it forces the parser to use the version
|
|
|
|
provided. Otherwise, the library will make the best effort based
|
2018-12-11 19:22:04 +01:00
|
|
|
on checking the "spec_version" property. If none of the above are
|
|
|
|
possible, it will use the default version specified by the library.
|
2018-11-01 13:17:34 +01:00
|
|
|
|
|
|
|
Returns:
|
|
|
|
An instantiated Python STIX object
|
|
|
|
|
|
|
|
Warnings:
|
|
|
|
'allow_custom=True' will allow for the return of any supplied STIX
|
|
|
|
dict(s) that cannot be found to map to any known STIX object types
|
|
|
|
(both STIX2 domain objects or defined custom STIX2 objects); NO
|
|
|
|
validation is done. This is done to allow the processing of
|
|
|
|
possibly unknown custom STIX objects (example scenario: I need to
|
|
|
|
query a third-party TAXII endpoint that could provide custom STIX
|
|
|
|
objects that I don't know about ahead of time)
|
2018-03-30 19:21:07 +02:00
|
|
|
|
2017-09-08 18:39:36 +02:00
|
|
|
"""
|
2018-06-14 02:09:07 +02:00
|
|
|
if 'type' not in stix_dict:
|
2018-06-26 18:23:53 +02:00
|
|
|
raise ParseError("Can't parse object with no 'type' property: %s" % str(stix_dict))
|
2018-06-14 02:09:07 +02:00
|
|
|
|
2018-07-10 20:56:31 +02:00
|
|
|
if version:
|
|
|
|
# If the version argument was passed, override other approaches.
|
|
|
|
v = 'v' + version.replace('.', '')
|
2017-11-02 12:48:37 +01:00
|
|
|
else:
|
2020-02-08 00:17:12 +01:00
|
|
|
v = _detect_spec_version(stix_dict)
|
2017-11-02 12:48:37 +01:00
|
|
|
|
2019-11-06 16:11:12 +01:00
|
|
|
OBJ_MAP = dict(STIX2_OBJ_MAPS[v]['objects'], **STIX2_OBJ_MAPS[v]['observables'])
|
2017-08-11 22:18:20 +02:00
|
|
|
|
|
|
|
try:
|
2018-03-29 17:49:30 +02:00
|
|
|
obj_class = OBJ_MAP[stix_dict['type']]
|
2017-08-11 22:18:20 +02:00
|
|
|
except KeyError:
|
2018-03-29 17:49:30 +02:00
|
|
|
if allow_custom:
|
|
|
|
# flag allows for unknown custom objects too, but will not
|
|
|
|
# be parsed into STIX object, returned as is
|
|
|
|
return stix_dict
|
2018-06-26 18:23:53 +02:00
|
|
|
raise ParseError("Can't parse unknown object type '%s'! For custom types, use the CustomObject decorator." % stix_dict['type'])
|
2018-03-29 17:49:30 +02:00
|
|
|
|
|
|
|
return obj_class(allow_custom=allow_custom, **stix_dict)
|
2017-08-11 22:18:20 +02:00
|
|
|
|
|
|
|
|
2018-07-10 20:56:31 +02:00
|
|
|
def parse_observable(data, _valid_refs=None, allow_custom=False, version=None):
|
|
|
|
"""Deserialize a string or file-like object into a STIX Cyber Observable
|
|
|
|
object.
|
|
|
|
|
|
|
|
Args:
|
2018-07-26 02:53:53 +02:00
|
|
|
data (str, dict, file-like object): The STIX2 content to be parsed.
|
2018-07-10 20:56:31 +02:00
|
|
|
_valid_refs: A list of object references valid for the scope of the
|
|
|
|
object being parsed. Use empty list if no valid refs are present.
|
|
|
|
allow_custom (bool): Whether to allow custom properties or not.
|
|
|
|
Default: False.
|
2018-11-01 13:17:34 +01:00
|
|
|
version (str): If present, it forces the parser to use the version
|
2018-12-11 19:22:04 +01:00
|
|
|
provided. Otherwise, the default version specified by the library
|
|
|
|
will be used.
|
2018-07-10 20:56:31 +02:00
|
|
|
|
|
|
|
Returns:
|
|
|
|
An instantiated Python STIX Cyber Observable object.
|
2018-11-01 13:17:34 +01:00
|
|
|
|
2018-07-10 20:56:31 +02:00
|
|
|
"""
|
|
|
|
obj = _get_dict(data)
|
2020-02-08 00:17:12 +01:00
|
|
|
|
|
|
|
if 'type' not in obj:
|
|
|
|
raise ParseError("Can't parse observable with no 'type' property: %s" % str(obj))
|
|
|
|
|
2018-07-10 20:56:31 +02:00
|
|
|
# get deep copy since we are going modify the dict and might
|
|
|
|
# modify the original dict as _get_dict() does not return new
|
|
|
|
# dict when passed a dict
|
|
|
|
obj = copy.deepcopy(obj)
|
|
|
|
|
|
|
|
obj['_valid_refs'] = _valid_refs or []
|
|
|
|
|
|
|
|
if version:
|
|
|
|
# If the version argument was passed, override other approaches.
|
|
|
|
v = 'v' + version.replace('.', '')
|
|
|
|
else:
|
2020-02-08 00:17:12 +01:00
|
|
|
v = _detect_spec_version(obj)
|
2018-07-10 20:56:31 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
OBJ_MAP_OBSERVABLE = STIX2_OBJ_MAPS[v]['observables']
|
|
|
|
obj_class = OBJ_MAP_OBSERVABLE[obj['type']]
|
|
|
|
except KeyError:
|
|
|
|
if allow_custom:
|
|
|
|
# flag allows for unknown custom objects too, but will not
|
|
|
|
# be parsed into STIX observable object, just returned as is
|
|
|
|
return obj
|
2019-12-06 15:40:27 +01:00
|
|
|
raise ParseError("Can't parse unknown observable type '%s'! For custom observables, "
|
|
|
|
"use the CustomObservable decorator." % obj['type'])
|
2018-07-10 20:56:31 +02:00
|
|
|
|
|
|
|
return obj_class(allow_custom=allow_custom, **obj)
|
|
|
|
|
|
|
|
|
|
|
|
def _register_object(new_type, version=None):
|
2017-08-11 22:18:20 +02:00
|
|
|
"""Register a custom STIX Object type.
|
|
|
|
|
2017-11-02 12:48:37 +01:00
|
|
|
Args:
|
|
|
|
new_type (class): A class to register in the Object map.
|
|
|
|
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If
|
|
|
|
None, use latest version.
|
2018-11-01 13:17:34 +01:00
|
|
|
|
2017-09-08 18:39:36 +02:00
|
|
|
"""
|
2018-07-10 20:56:31 +02:00
|
|
|
if version:
|
|
|
|
v = 'v' + version.replace('.', '')
|
|
|
|
else:
|
|
|
|
# Use default version (latest) if no version was provided.
|
2017-11-02 12:48:37 +01:00
|
|
|
v = 'v' + stix2.DEFAULT_VERSION.replace('.', '')
|
2018-07-10 20:56:31 +02:00
|
|
|
|
|
|
|
OBJ_MAP = STIX2_OBJ_MAPS[v]['objects']
|
|
|
|
OBJ_MAP[new_type._type] = new_type
|
|
|
|
|
|
|
|
|
|
|
|
def _register_marking(new_marking, version=None):
|
|
|
|
"""Register a custom STIX Marking Definition type.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
new_marking (class): A class to register in the Marking map.
|
|
|
|
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If
|
|
|
|
None, use latest version.
|
2018-11-01 13:17:34 +01:00
|
|
|
|
2018-07-10 20:56:31 +02:00
|
|
|
"""
|
|
|
|
if version:
|
|
|
|
v = 'v' + version.replace('.', '')
|
2017-11-02 12:48:37 +01:00
|
|
|
else:
|
2018-07-10 20:56:31 +02:00
|
|
|
# Use default version (latest) if no version was provided.
|
|
|
|
v = 'v' + stix2.DEFAULT_VERSION.replace('.', '')
|
|
|
|
|
|
|
|
OBJ_MAP_MARKING = STIX2_OBJ_MAPS[v]['markings']
|
|
|
|
OBJ_MAP_MARKING[new_marking._type] = new_marking
|
|
|
|
|
|
|
|
|
|
|
|
def _register_observable(new_observable, version=None):
|
|
|
|
"""Register a custom STIX Cyber Observable type.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
new_observable (class): A class to register in the Observables map.
|
|
|
|
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If
|
|
|
|
None, use latest version.
|
2018-11-01 13:17:34 +01:00
|
|
|
|
2018-07-10 20:56:31 +02:00
|
|
|
"""
|
|
|
|
if version:
|
2017-11-02 12:48:37 +01:00
|
|
|
v = 'v' + version.replace('.', '')
|
2018-07-10 20:56:31 +02:00
|
|
|
else:
|
|
|
|
# Use default version (latest) if no version was provided.
|
|
|
|
v = 'v' + stix2.DEFAULT_VERSION.replace('.', '')
|
|
|
|
|
|
|
|
OBJ_MAP_OBSERVABLE = STIX2_OBJ_MAPS[v]['observables']
|
|
|
|
OBJ_MAP_OBSERVABLE[new_observable._type] = new_observable
|
2017-11-02 12:48:37 +01:00
|
|
|
|
|
|
|
|
2020-03-11 01:24:53 +01:00
|
|
|
def _register_observable_extension(
|
2020-03-11 02:21:53 +01:00
|
|
|
observable, new_extension, version=stix2.DEFAULT_VERSION,
|
2020-03-11 01:24:53 +01:00
|
|
|
):
|
2018-07-10 20:56:31 +02:00
|
|
|
"""Register a custom extension to a STIX Cyber Observable type.
|
2017-11-02 12:48:37 +01:00
|
|
|
|
2018-07-10 20:56:31 +02:00
|
|
|
Args:
|
2020-03-11 01:24:53 +01:00
|
|
|
observable: An observable class or instance
|
2018-07-10 20:56:31 +02:00
|
|
|
new_extension (class): A class to register in the Observables
|
|
|
|
Extensions map.
|
2020-03-11 01:24:53 +01:00
|
|
|
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1").
|
|
|
|
Defaults to the latest supported version.
|
2018-11-01 13:17:34 +01:00
|
|
|
|
2018-07-10 20:56:31 +02:00
|
|
|
"""
|
2020-03-11 01:24:53 +01:00
|
|
|
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('.', '')
|
2018-07-10 20:56:31 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
observable_type = observable._type
|
|
|
|
except AttributeError:
|
2018-07-13 17:10:05 +02:00
|
|
|
raise ValueError(
|
|
|
|
"Unknown observable type. Custom observables must be "
|
|
|
|
"created with the @CustomObservable decorator.",
|
|
|
|
)
|
2018-07-10 20:56:31 +02:00
|
|
|
|
|
|
|
OBJ_MAP_OBSERVABLE = STIX2_OBJ_MAPS[v]['observables']
|
|
|
|
EXT_MAP = STIX2_OBJ_MAPS[v]['observable-extensions']
|
|
|
|
|
|
|
|
try:
|
2020-03-11 01:24:53 +01:00
|
|
|
EXT_MAP[observable_type][ext_type] = new_extension
|
2018-07-10 20:56:31 +02:00
|
|
|
except KeyError:
|
|
|
|
if observable_type not in OBJ_MAP_OBSERVABLE:
|
2018-07-13 17:10:05 +02:00
|
|
|
raise ValueError(
|
|
|
|
"Unknown observable type '%s'. Custom observables "
|
|
|
|
"must be created with the @CustomObservable decorator."
|
|
|
|
% observable_type,
|
|
|
|
)
|
2018-07-10 20:56:31 +02:00
|
|
|
else:
|
2020-03-11 01:24:53 +01:00
|
|
|
EXT_MAP[observable_type] = {ext_type: new_extension}
|
2018-07-10 20:56:31 +02:00
|
|
|
|
|
|
|
|
|
|
|
def _collect_stix2_mappings():
|
|
|
|
"""Navigate the package once and retrieve all object mapping dicts for each
|
|
|
|
v2X package. Includes OBJ_MAP, OBJ_MAP_OBSERVABLE, EXT_MAP."""
|
2017-11-02 12:48:37 +01:00
|
|
|
if not STIX2_OBJ_MAPS:
|
|
|
|
top_level_module = importlib.import_module('stix2')
|
|
|
|
path = top_level_module.__path__
|
|
|
|
prefix = str(top_level_module.__name__) + '.'
|
|
|
|
|
2018-07-10 20:56:31 +02:00
|
|
|
for module_loader, name, is_pkg in pkgutil.walk_packages(path=path, prefix=prefix):
|
2018-11-29 16:26:20 +01:00
|
|
|
ver = name.split('.')[1]
|
2018-07-25 20:06:18 +02:00
|
|
|
if re.match(r'^stix2\.v2[0-9]$', name) and is_pkg:
|
2017-11-02 12:48:37 +01:00
|
|
|
mod = importlib.import_module(name, str(top_level_module.__name__))
|
2018-11-29 16:26:20 +01:00
|
|
|
STIX2_OBJ_MAPS[ver] = {}
|
|
|
|
STIX2_OBJ_MAPS[ver]['objects'] = mod.OBJ_MAP
|
|
|
|
STIX2_OBJ_MAPS[ver]['observables'] = mod.OBJ_MAP_OBSERVABLE
|
|
|
|
STIX2_OBJ_MAPS[ver]['observable-extensions'] = mod.EXT_MAP
|
2018-07-25 20:06:18 +02:00
|
|
|
elif re.match(r'^stix2\.v2[0-9]\.common$', name) and is_pkg is False:
|
2018-07-10 20:56:31 +02:00
|
|
|
mod = importlib.import_module(name, str(top_level_module.__name__))
|
2018-11-29 16:26:20 +01:00
|
|
|
STIX2_OBJ_MAPS[ver]['markings'] = mod.OBJ_MAP_MARKING
|