Merge branch 'master' of github.com:oasis-open/cti-python-stix2
parent
bdb91c6ac4
commit
f6f7d0aed8
|
@ -9,7 +9,6 @@ known_third_party =
|
|||
simplejson
|
||||
six,
|
||||
stix2patterns,
|
||||
stix2validator,
|
||||
taxii2client,
|
||||
known_first_party = stix2
|
||||
force_sort_within_sections = 1
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
include LICENSE
|
||||
include CHANGELOG
|
||||
recursive-exclude stix2\test *
|
12
README.rst
12
README.rst
|
@ -13,7 +13,7 @@ including data markings, versioning, and for resolving STIX IDs across
|
|||
multiple data sources.
|
||||
|
||||
For more information, see `the
|
||||
documentation <https://stix2.readthedocs.io/en/latest/>`__ on
|
||||
documentation <https://stix2.readthedocs.io/>`__ on
|
||||
ReadTheDocs.
|
||||
|
||||
Installation
|
||||
|
@ -62,6 +62,16 @@ To parse a STIX JSON string into a Python STIX object, use ``parse()``:
|
|||
|
||||
For more in-depth documentation, please see `https://stix2.readthedocs.io/ <https://stix2.readthedocs.io/>`__.
|
||||
|
||||
STIX 2.X Technical Specification Support
|
||||
----------------------------------------
|
||||
|
||||
This version of python-stix2 supports STIX 2.0 by default. Although, the
|
||||
`stix2` Python library is built to support multiple versions of the STIX
|
||||
Technical Specification. With every major release of stix2 the ``import stix2``
|
||||
statement will automatically load the SDO/SROs equivalent to the most recent
|
||||
supported 2.X Technical Specification. Please see the library documentation
|
||||
for more details.
|
||||
|
||||
Governance
|
||||
----------
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,237 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Technical Specification Support\n",
|
||||
"\n",
|
||||
"### How imports will work\n",
|
||||
"\n",
|
||||
"Imports can be used in different ways depending on the use case and support levels.\n",
|
||||
"\n",
|
||||
"People who want to (in general) support the latest version of STIX 2.X without making changes, implicitly using the latest version"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import stix2\n",
|
||||
"\n",
|
||||
"stix2.Indicator()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"or,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from stix2 import Indicator\n",
|
||||
"\n",
|
||||
"Indicator()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"People who want to use an explicit version"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import stix2.v20\n",
|
||||
"\n",
|
||||
"stix2.v20.Indicator()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"or,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from stix2.v20 import Indicator\n",
|
||||
"\n",
|
||||
"Indicator()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"or even,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import stix2.v20 as stix2\n",
|
||||
"\n",
|
||||
"stix2.Indicator()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The last option makes it easy to update to a new version in one place per file, once you've made the deliberate action to do this.\n",
|
||||
"\n",
|
||||
"People who want to use multiple versions in a single file:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import stix2\n",
|
||||
"\n",
|
||||
"stix2.v20.Indicator()\n",
|
||||
"\n",
|
||||
"stix2.v21.Indicator()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"or,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from stix2 import v20, v21\n",
|
||||
"\n",
|
||||
"v20.Indicator()\n",
|
||||
"v21.Indicator()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"or (less preferred):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from stix2.v20 import Indicator as Indicator_v20\n",
|
||||
"from stix2.v21 import Indicator as Indicator_v21\n",
|
||||
"\n",
|
||||
"Indicator_v20()\n",
|
||||
"Indicator_v21()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### How parsing will work\n",
|
||||
"If the ``version`` positional argument is not provided. The data will be parsed using the latest version of STIX 2.X supported by the `stix2` library.\n",
|
||||
"\n",
|
||||
"You can lock your `parse()` method to a specific STIX version by"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from stix2 import parse\n",
|
||||
"\n",
|
||||
"indicator = parse(\"\"\"{\n",
|
||||
" \"type\": \"indicator\",\n",
|
||||
" \"id\": \"indicator--dbcbd659-c927-4f9a-994f-0a2632274394\",\n",
|
||||
" \"created\": \"2017-09-26T23:33:39.829Z\",\n",
|
||||
" \"modified\": \"2017-09-26T23:33:39.829Z\",\n",
|
||||
" \"labels\": [\n",
|
||||
" \"malicious-activity\"\n",
|
||||
" ],\n",
|
||||
" \"name\": \"File hash for malware variant\",\n",
|
||||
" \"pattern\": \"[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']\",\n",
|
||||
" \"valid_from\": \"2017-09-26T23:33:39.829952Z\"\n",
|
||||
"}\"\"\", version=\"2.0\")\n",
|
||||
"print(indicator)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Keep in mind that if a 2.1 or higher object is parsed, the operation will fail."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### How will custom work\n",
|
||||
"\n",
|
||||
"CustomObject, CustomObservable, CustomMarking and CustomExtension must be registered explicitly by STIX version. This is a design decision since properties or requirements may change as the STIX Technical Specification advances.\n",
|
||||
"\n",
|
||||
"You can perform this by,"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import stix2\n",
|
||||
"\n",
|
||||
"# Make my custom observable available in STIX 2.0\n",
|
||||
"@stix2.v20.CustomObservable('x-new-object-type',\n",
|
||||
" ((\"prop\", stix2.properties.BooleanProperty())))\n",
|
||||
"class NewObject2(object):\n",
|
||||
" pass\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Make my custom observable available in STIX 2.1\n",
|
||||
"@stix2.v21.CustomObservable('x-new-object-type',\n",
|
||||
" ((\"prop\", stix2.properties.BooleanProperty())))\n",
|
||||
"class NewObject2(object):\n",
|
||||
" pass"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
5
setup.py
5
setup.py
|
@ -44,8 +44,8 @@ setup(
|
|||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
],
|
||||
keywords="stix stix2 json cti cyber threat intelligence",
|
||||
packages=find_packages(),
|
||||
keywords='stix stix2 json cti cyber threat intelligence',
|
||||
packages=find_packages(exclude=['*.test']),
|
||||
install_requires=[
|
||||
'python-dateutil',
|
||||
'pytz',
|
||||
|
@ -53,7 +53,6 @@ setup(
|
|||
'simplejson',
|
||||
'six',
|
||||
'stix2-patterns',
|
||||
'stix2-validator',
|
||||
'taxii2-client',
|
||||
],
|
||||
)
|
||||
|
|
|
@ -19,28 +19,10 @@
|
|||
|
||||
# flake8: noqa
|
||||
|
||||
from . import exceptions
|
||||
from .common import (TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, CustomMarking,
|
||||
ExternalReference, GranularMarking, KillChainPhase,
|
||||
LanguageContent, MarkingDefinition, StatementMarking,
|
||||
TLPMarking)
|
||||
from .core import Bundle, _register_type, parse
|
||||
from .core import Bundle, _collect_stix2_obj_maps, _register_type, parse
|
||||
from .environment import Environment, ObjectFactory
|
||||
from .markings import (add_markings, clear_markings, get_markings, is_marked,
|
||||
remove_markings, set_markings)
|
||||
from .observables import (URL, AlternateDataStream, ArchiveExt, Artifact,
|
||||
AutonomousSystem, CustomExtension, CustomObservable,
|
||||
Directory, DomainName, EmailAddress, EmailMessage,
|
||||
EmailMIMEComponent, File, HTTPRequestExt, ICMPExt,
|
||||
IPv4Address, IPv6Address, MACAddress, Mutex,
|
||||
NetworkTraffic, NTFSExt, PDFExt, Process,
|
||||
RasterImageExt, SocketExt, Software, TCPExt,
|
||||
UNIXAccountExt, UserAccount, WindowsPEBinaryExt,
|
||||
WindowsPEOptionalHeaderType, WindowsPESection,
|
||||
WindowsProcessExt, WindowsRegistryKey,
|
||||
WindowsRegistryValueType, WindowsServiceExt,
|
||||
X509Certificate, X509V3ExtenstionsType,
|
||||
parse_observable)
|
||||
from .patterns import (AndBooleanExpression, AndObservationExpression,
|
||||
BasicObjectPathComponent, EqualityComparisonExpression,
|
||||
FloatConstant, FollowedByObservationExpression,
|
||||
|
@ -59,10 +41,6 @@ from .patterns import (AndBooleanExpression, AndObservationExpression,
|
|||
ReferenceObjectPathComponent, RepeatQualifier,
|
||||
StartStopQualifier, StringConstant, TimestampConstant,
|
||||
WithinQualifier)
|
||||
from .sdo import (AttackPattern, Campaign, CourseOfAction, CustomObject,
|
||||
Identity, Indicator, IntrusionSet, Location, Malware, Note,
|
||||
ObservedData, Opinion, Report, ThreatActor, Tool,
|
||||
Vulnerability)
|
||||
from .sources import CompositeDataSource
|
||||
from .sources.filesystem import (FileSystemSink, FileSystemSource,
|
||||
FileSystemStore)
|
||||
|
@ -70,6 +48,10 @@ from .sources.filters import Filter
|
|||
from .sources.memory import MemorySink, MemorySource, MemoryStore
|
||||
from .sources.taxii import (TAXIICollectionSink, TAXIICollectionSource,
|
||||
TAXIICollectionStore)
|
||||
from .sro import Relationship, Sighting
|
||||
from .utils import get_dict, new_version, revoke
|
||||
from .v21 import * # This import will always be the latest STIX 2.X version
|
||||
from .version import __version__
|
||||
|
||||
_collect_stix2_obj_maps()
|
||||
|
||||
DEFAULT_VERSION = "2.1" # Default version will always be the latest STIX 2.X version
|
||||
|
|
|
@ -40,7 +40,14 @@ class _STIXBase(collections.Mapping):
|
|||
"""Base class for STIX object types"""
|
||||
|
||||
def object_properties(self):
|
||||
return list(self._properties.keys())
|
||||
props = set(self._properties.keys())
|
||||
custom_props = list(set(self._inner.keys()) - props)
|
||||
custom_props.sort()
|
||||
|
||||
all_properties = list(self._properties.keys())
|
||||
all_properties.extend(custom_props) # Any custom properties to the bottom
|
||||
|
||||
return all_properties
|
||||
|
||||
def _check_property(self, prop_name, prop, kwargs):
|
||||
if prop_name not in kwargs:
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
"""STIX 2.0 Objects that are neither SDOs nor SROs."""
|
||||
"""STIX 2.X Objects that are neither SDOs nor SROs."""
|
||||
|
||||
from collections import OrderedDict
|
||||
import importlib
|
||||
import pkgutil
|
||||
|
||||
import stix2
|
||||
|
||||
from . import exceptions
|
||||
from .base import _STIXBase
|
||||
from .common import MarkingDefinition
|
||||
from .properties import IDProperty, ListProperty, Property, TypeProperty
|
||||
from .sdo import (AttackPattern, Campaign, CourseOfAction, Identity, Indicator,
|
||||
IntrusionSet, Location, Malware, Note, ObservedData, Opinion,
|
||||
Report, ThreatActor, Tool, Vulnerability)
|
||||
from .sro import Relationship, Sighting
|
||||
from .utils import get_dict
|
||||
from .utils import get_class_hierarchy_names, get_dict
|
||||
|
||||
|
||||
class STIXObjectProperty(Property):
|
||||
|
@ -20,6 +19,11 @@ class STIXObjectProperty(Property):
|
|||
super(STIXObjectProperty, self).__init__()
|
||||
|
||||
def clean(self, value):
|
||||
# Any STIX Object (SDO, SRO, or Marking Definition) can be added to
|
||||
# a bundle with no further checks.
|
||||
if any(x in ('STIXDomainObject', 'STIXRelationshipObject', 'MarkingDefinition')
|
||||
for x in get_class_hierarchy_names(value)):
|
||||
return value
|
||||
try:
|
||||
dictified = get_dict(value)
|
||||
except ValueError:
|
||||
|
@ -62,40 +66,30 @@ class Bundle(_STIXBase):
|
|||
super(Bundle, self).__init__(**kwargs)
|
||||
|
||||
|
||||
OBJ_MAP = {
|
||||
'attack-pattern': AttackPattern,
|
||||
'bundle': Bundle,
|
||||
'campaign': Campaign,
|
||||
'course-of-action': CourseOfAction,
|
||||
'identity': Identity,
|
||||
'indicator': Indicator,
|
||||
'intrusion-set': IntrusionSet,
|
||||
'location': Location,
|
||||
'malware': Malware,
|
||||
'note': Note,
|
||||
'marking-definition': MarkingDefinition,
|
||||
'observed-data': ObservedData,
|
||||
'opinion': Opinion,
|
||||
'report': Report,
|
||||
'relationship': Relationship,
|
||||
'threat-actor': ThreatActor,
|
||||
'tool': Tool,
|
||||
'sighting': Sighting,
|
||||
'vulnerability': Vulnerability,
|
||||
}
|
||||
STIX2_OBJ_MAPS = {}
|
||||
|
||||
|
||||
def parse(data, allow_custom=False):
|
||||
def parse(data, allow_custom=False, version=None):
|
||||
"""Deserialize a string or file-like object into a STIX object.
|
||||
|
||||
Args:
|
||||
data (str, dict, file-like object): The STIX 2 content to be parsed.
|
||||
allow_custom (bool): Whether to allow custom properties or not. Default: False.
|
||||
allow_custom (bool): Whether to allow custom properties or not.
|
||||
Default: False.
|
||||
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If
|
||||
None, use latest version.
|
||||
|
||||
Returns:
|
||||
An instantiated Python STIX object.
|
||||
|
||||
"""
|
||||
if not version:
|
||||
# Use latest version
|
||||
v = 'v' + stix2.DEFAULT_VERSION.replace('.', '')
|
||||
else:
|
||||
v = 'v' + version.replace('.', '')
|
||||
|
||||
OBJ_MAP = STIX2_OBJ_MAPS[v]
|
||||
obj = get_dict(data)
|
||||
|
||||
if 'type' not in obj:
|
||||
|
@ -108,8 +102,34 @@ def parse(data, allow_custom=False):
|
|||
return obj_class(allow_custom=allow_custom, **obj)
|
||||
|
||||
|
||||
def _register_type(new_type):
|
||||
def _register_type(new_type, version=None):
|
||||
"""Register a custom STIX Object type.
|
||||
|
||||
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.
|
||||
"""
|
||||
if not version:
|
||||
# Use latest version
|
||||
v = 'v' + stix2.DEFAULT_VERSION.replace('.', '')
|
||||
else:
|
||||
v = 'v' + version.replace('.', '')
|
||||
|
||||
OBJ_MAP = STIX2_OBJ_MAPS[v]
|
||||
OBJ_MAP[new_type._type] = new_type
|
||||
|
||||
|
||||
def _collect_stix2_obj_maps():
|
||||
"""Navigate the package once and retrieve all OBJ_MAP dicts for each v2X
|
||||
package."""
|
||||
if not STIX2_OBJ_MAPS:
|
||||
top_level_module = importlib.import_module('stix2')
|
||||
path = top_level_module.__path__
|
||||
prefix = str(top_level_module.__name__) + '.'
|
||||
|
||||
for module_loader, name, is_pkg in pkgutil.walk_packages(path=path,
|
||||
prefix=prefix):
|
||||
if name.startswith('stix2.v2') and is_pkg:
|
||||
mod = importlib.import_module(name, str(top_level_module.__name__))
|
||||
STIX2_OBJ_MAPS[name.split('.')[-1]] = mod.OBJ_MAP
|
||||
|
|
|
@ -152,3 +152,22 @@ class Environment(object):
|
|||
def parse(self, *args, **kwargs):
|
||||
return _parse(*args, **kwargs)
|
||||
parse.__doc__ = _parse.__doc__
|
||||
|
||||
def creator_of(self, obj):
|
||||
"""Retrieve the Identity refered to by the object's `created_by_ref`.
|
||||
|
||||
Args:
|
||||
obj: The STIX object whose `created_by_ref` property will be looked
|
||||
up.
|
||||
|
||||
Returns:
|
||||
The STIX object's creator, or
|
||||
None, if the object contains no `created_by_ref` property or the
|
||||
object's creator cannot be found.
|
||||
|
||||
"""
|
||||
creator_id = obj.get('created_by_ref', '')
|
||||
if creator_id:
|
||||
return self.get(creator_id)
|
||||
else:
|
||||
return None
|
||||
|
|
|
@ -44,36 +44,40 @@ class DataStore(object):
|
|||
self.source = source
|
||||
self.sink = sink
|
||||
|
||||
def get(self, stix_id):
|
||||
def get(self, stix_id, allow_custom=False):
|
||||
"""Retrieve the most recent version of a single STIX object by ID.
|
||||
|
||||
Translate get() call to the appropriate DataSource call.
|
||||
|
||||
Args:
|
||||
stix_id (str): the id of the STIX object to retrieve.
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
stix_obj: the single most recent version of the STIX
|
||||
object specified by the "id".
|
||||
|
||||
"""
|
||||
return self.source.get(stix_id)
|
||||
return self.source.get(stix_id, allow_custom=allow_custom)
|
||||
|
||||
def all_versions(self, stix_id):
|
||||
def all_versions(self, stix_id, allow_custom=False):
|
||||
"""Retrieve all versions of a single STIX object by ID.
|
||||
|
||||
Implement: Translate all_versions() call to the appropriate DataSource call
|
||||
|
||||
Args:
|
||||
stix_id (str): the id of the STIX object to retrieve.
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
stix_objs (list): a list of STIX objects
|
||||
|
||||
"""
|
||||
return self.source.all_versions(stix_id)
|
||||
return self.source.all_versions(stix_id, allow_custom=allow_custom)
|
||||
|
||||
def query(self, query):
|
||||
def query(self, query=None, allow_custom=False):
|
||||
"""Retrieve STIX objects matching a set of filters.
|
||||
|
||||
Implement: Specific data source API calls, processing,
|
||||
|
@ -82,6 +86,8 @@ class DataStore(object):
|
|||
Args:
|
||||
query (list): a list of filters (which collectively are the query)
|
||||
to conduct search on.
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
stix_objs (list): a list of STIX objects
|
||||
|
@ -89,15 +95,17 @@ class DataStore(object):
|
|||
"""
|
||||
return self.source.query(query=query)
|
||||
|
||||
def add(self, stix_objs):
|
||||
def add(self, stix_objs, allow_custom=False):
|
||||
"""Store STIX objects.
|
||||
|
||||
Translates add() to the appropriate DataSink call.
|
||||
|
||||
Args:
|
||||
stix_objs (list): a list of STIX objects
|
||||
allow_custom (bool): whether to allow custom objects/properties or
|
||||
not. Default: False.
|
||||
"""
|
||||
return self.sink.add(stix_objs)
|
||||
return self.sink.add(stix_objs, allow_custom=allow_custom)
|
||||
|
||||
|
||||
class DataSink(object):
|
||||
|
@ -111,7 +119,7 @@ class DataSink(object):
|
|||
def __init__(self):
|
||||
self.id = make_id()
|
||||
|
||||
def add(self, stix_objs):
|
||||
def add(self, stix_objs, allow_custom=False):
|
||||
"""Store STIX objects.
|
||||
|
||||
Implement: Specific data sink API calls, processing,
|
||||
|
@ -120,6 +128,8 @@ class DataSink(object):
|
|||
Args:
|
||||
stix_objs (list): a list of STIX objects (where each object is a
|
||||
STIX object)
|
||||
allow_custom (bool): whether to allow custom objects/properties or
|
||||
not. Default: False.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
@ -139,7 +149,7 @@ class DataSource(object):
|
|||
self.id = make_id()
|
||||
self.filters = set()
|
||||
|
||||
def get(self, stix_id, _composite_filters=None):
|
||||
def get(self, stix_id, _composite_filters=None, allow_custom=False):
|
||||
"""
|
||||
Implement: Specific data source API calls, processing,
|
||||
functionality required for retrieving data from the data source
|
||||
|
@ -148,9 +158,10 @@ class DataSource(object):
|
|||
stix_id (str): the id of the STIX 2.0 object to retrieve. Should
|
||||
return a single object, the most recent version of the object
|
||||
specified by the "id".
|
||||
|
||||
_composite_filters (set): set of filters passed from the parent
|
||||
the CompositeDataSource, not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
stix_obj: the STIX object
|
||||
|
@ -158,7 +169,7 @@ class DataSource(object):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def all_versions(self, stix_id, _composite_filters=None):
|
||||
def all_versions(self, stix_id, _composite_filters=None, allow_custom=False):
|
||||
"""
|
||||
Implement: Similar to get() except returns list of all object versions of
|
||||
the specified "id". In addition, implement the specific data
|
||||
|
@ -169,9 +180,10 @@ class DataSource(object):
|
|||
stix_id (str): The id of the STIX 2.0 object to retrieve. Should
|
||||
return a list of objects, all the versions of the object
|
||||
specified by the "id".
|
||||
|
||||
_composite_filters (set): set of filters passed from the parent
|
||||
CompositeDataSource, not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
stix_objs (list): a list of STIX objects
|
||||
|
@ -179,7 +191,7 @@ class DataSource(object):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def query(self, query, _composite_filters=None):
|
||||
def query(self, query=None, _composite_filters=None, allow_custom=False):
|
||||
"""
|
||||
Implement:Implement the specific data source API calls, processing,
|
||||
functionality required for retrieving query from the data source
|
||||
|
@ -187,9 +199,10 @@ class DataSource(object):
|
|||
Args:
|
||||
query (list): a list of filters (which collectively are the query)
|
||||
to conduct search on
|
||||
|
||||
_composite_filters (set): a set of filters passed from the parent
|
||||
CompositeDataSource, not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
stix_objs (list): a list of STIX objects
|
||||
|
@ -224,7 +237,7 @@ class CompositeDataSource(DataSource):
|
|||
super(CompositeDataSource, self).__init__()
|
||||
self.data_sources = []
|
||||
|
||||
def get(self, stix_id, _composite_filters=None):
|
||||
def get(self, stix_id, _composite_filters=None, allow_custom=False):
|
||||
"""Retrieve STIX object by STIX ID
|
||||
|
||||
Federated retrieve method, iterates through all DataSources
|
||||
|
@ -238,10 +251,11 @@ class CompositeDataSource(DataSource):
|
|||
|
||||
Args:
|
||||
stix_id (str): the id of the STIX object to retrieve.
|
||||
|
||||
_composite_filters (list): a list of filters passed from a
|
||||
CompositeDataSource (i.e. if this CompositeDataSource is attached
|
||||
to another parent CompositeDataSource), not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
stix_obj: the STIX object to be returned.
|
||||
|
@ -259,20 +273,22 @@ class CompositeDataSource(DataSource):
|
|||
|
||||
# for every configured Data Source, call its retrieve handler
|
||||
for ds in self.data_sources:
|
||||
data = ds.get(stix_id=stix_id, _composite_filters=all_filters)
|
||||
data = ds.get(stix_id=stix_id, _composite_filters=all_filters, allow_custom=allow_custom)
|
||||
if data:
|
||||
all_data.append(data)
|
||||
|
||||
# remove duplicate versions
|
||||
if len(all_data) > 0:
|
||||
all_data = deduplicate(all_data)
|
||||
else:
|
||||
return None
|
||||
|
||||
# reduce to most recent version
|
||||
stix_obj = sorted(all_data, key=lambda k: k['modified'], reverse=True)[0]
|
||||
|
||||
return stix_obj
|
||||
|
||||
def all_versions(self, stix_id, _composite_filters=None):
|
||||
def all_versions(self, stix_id, _composite_filters=None, allow_custom=False):
|
||||
"""Retrieve STIX objects by STIX ID
|
||||
|
||||
Federated all_versions retrieve method - iterates through all DataSources
|
||||
|
@ -283,10 +299,11 @@ class CompositeDataSource(DataSource):
|
|||
|
||||
Args:
|
||||
stix_id (str): id of the STIX objects to retrieve
|
||||
|
||||
_composite_filters (list): a list of filters passed from a
|
||||
CompositeDataSource (i.e. if this CompositeDataSource is attached
|
||||
to a parent CompositeDataSource), not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
all_data (list): list of STIX objects that have the specified id
|
||||
|
@ -305,7 +322,7 @@ class CompositeDataSource(DataSource):
|
|||
|
||||
# retrieve STIX objects from all configured data sources
|
||||
for ds in self.data_sources:
|
||||
data = ds.all_versions(stix_id=stix_id, _composite_filters=all_filters)
|
||||
data = ds.all_versions(stix_id=stix_id, _composite_filters=all_filters, allow_custom=allow_custom)
|
||||
all_data.extend(data)
|
||||
|
||||
# remove exact duplicates (where duplicates are STIX 2.0 objects
|
||||
|
@ -315,7 +332,7 @@ class CompositeDataSource(DataSource):
|
|||
|
||||
return all_data
|
||||
|
||||
def query(self, query=None, _composite_filters=None):
|
||||
def query(self, query=None, _composite_filters=None, allow_custom=False):
|
||||
"""Retrieve STIX objects that match query
|
||||
|
||||
Federate the query to all DataSources attached to the
|
||||
|
@ -323,10 +340,11 @@ class CompositeDataSource(DataSource):
|
|||
|
||||
Args:
|
||||
query (list): list of filters to search on
|
||||
|
||||
_composite_filters (list): a list of filters passed from a
|
||||
CompositeDataSource (i.e. if this CompositeDataSource is attached
|
||||
to a parent CompositeDataSource), not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
all_data (list): list of STIX objects to be returned
|
||||
|
@ -351,7 +369,7 @@ class CompositeDataSource(DataSource):
|
|||
# federate query to all attached data sources,
|
||||
# pass composite filters to id
|
||||
for ds in self.data_sources:
|
||||
data = ds.query(query=query, _composite_filters=all_filters)
|
||||
data = ds.query(query=query, _composite_filters=all_filters, allow_custom=allow_custom)
|
||||
all_data.extend(data)
|
||||
|
||||
# remove exact duplicates (where duplicates are STIX 2.0
|
||||
|
|
|
@ -8,51 +8,51 @@ TODO:
|
|||
import json
|
||||
import os
|
||||
|
||||
from stix2.base import _STIXBase
|
||||
from stix2.core import Bundle, parse
|
||||
from stix2.sources import DataSink, DataSource, DataStore
|
||||
from stix2.sources.filters import Filter, apply_common_filters
|
||||
from stix2.utils import deduplicate
|
||||
from stix2.utils import deduplicate, get_class_hierarchy_names
|
||||
|
||||
|
||||
class FileSystemStore(DataStore):
|
||||
"""FileSystemStore
|
||||
"""Interface to a file directory of STIX objects.
|
||||
|
||||
Provides an interface to an file directory of STIX objects.
|
||||
FileSystemStore is a wrapper around a paired FileSystemSink
|
||||
and FileSystemSource.
|
||||
|
||||
Args:
|
||||
stix_dir (str): path to directory of STIX objects
|
||||
bundlify (bool): Whether to wrap objects in bundles when saving them.
|
||||
Default: False.
|
||||
|
||||
Attributes:
|
||||
source (FileSystemSource): FuleSystemSource
|
||||
|
||||
sink (FileSystemSink): FileSystemSink
|
||||
|
||||
"""
|
||||
def __init__(self, stix_dir):
|
||||
def __init__(self, stix_dir, bundlify=False):
|
||||
super(FileSystemStore, self).__init__()
|
||||
self.source = FileSystemSource(stix_dir=stix_dir)
|
||||
self.sink = FileSystemSink(stix_dir=stix_dir)
|
||||
self.sink = FileSystemSink(stix_dir=stix_dir, bundlify=bundlify)
|
||||
|
||||
|
||||
class FileSystemSink(DataSink):
|
||||
"""FileSystemSink
|
||||
|
||||
Provides an interface for adding/pushing STIX objects
|
||||
to file directory of STIX objects.
|
||||
"""Interface for adding/pushing STIX objects to file directory of STIX
|
||||
objects.
|
||||
|
||||
Can be paired with a FileSystemSource, together as the two
|
||||
components of a FileSystemStore.
|
||||
|
||||
Args:
|
||||
stix_dir (str): path to directory of STIX objects
|
||||
stix_dir (str): path to directory of STIX objects.
|
||||
bundlify (bool): Whether to wrap objects in bundles when saving them.
|
||||
Default: False.
|
||||
|
||||
"""
|
||||
def __init__(self, stix_dir):
|
||||
def __init__(self, stix_dir, bundlify=False):
|
||||
super(FileSystemSink, self).__init__()
|
||||
self._stix_dir = os.path.abspath(stix_dir)
|
||||
self.bundlify = bundlify
|
||||
|
||||
if not os.path.exists(self._stix_dir):
|
||||
raise ValueError("directory path for STIX data does not exist")
|
||||
|
@ -61,62 +61,72 @@ class FileSystemSink(DataSink):
|
|||
def stix_dir(self):
|
||||
return self._stix_dir
|
||||
|
||||
def add(self, stix_data=None):
|
||||
"""add STIX objects to file directory
|
||||
def _check_path_and_write(self, stix_obj):
|
||||
"""Write the given STIX object to a file in the STIX file directory.
|
||||
"""
|
||||
path = os.path.join(self._stix_dir, stix_obj["type"], stix_obj["id"] + ".json")
|
||||
|
||||
if not os.path.exists(os.path.dirname(path)):
|
||||
os.makedirs(os.path.dirname(path))
|
||||
|
||||
if self.bundlify:
|
||||
stix_obj = Bundle(stix_obj)
|
||||
|
||||
with open(path, "w") as f:
|
||||
f.write(str(stix_obj))
|
||||
|
||||
def add(self, stix_data=None, allow_custom=False, version=None):
|
||||
"""Add STIX objects to file directory.
|
||||
|
||||
Args:
|
||||
stix_data (STIX object OR dict OR str OR list): valid STIX 2.0 content
|
||||
in a STIX object(or list of), dict (or list of), or a STIX 2.0
|
||||
json encoded string
|
||||
in a STIX object (or list of), dict (or list of), or a STIX 2.0
|
||||
json encoded string.
|
||||
allow_custom (bool): whether to allow custom objects/properties or
|
||||
not. Default: False.
|
||||
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If
|
||||
None, use latest version.
|
||||
|
||||
Note:
|
||||
``stix_data`` can be a Bundle object, but each object in it will be
|
||||
saved separately; you will be able to retrieve any of the objects
|
||||
the Bundle contained, but not the Bundle itself.
|
||||
|
||||
TODO: Bundlify STIX content or no? When dumping to disk.
|
||||
"""
|
||||
def _check_path_and_write(stix_dir, stix_obj):
|
||||
path = os.path.join(stix_dir, stix_obj["type"], stix_obj["id"] + ".json")
|
||||
|
||||
if not os.path.exists(os.path.dirname(path)):
|
||||
os.makedirs(os.path.dirname(path))
|
||||
|
||||
with open(path, "w") as f:
|
||||
# Bundle() can take dict or STIX obj as argument
|
||||
f.write(str(Bundle(stix_obj)))
|
||||
|
||||
if isinstance(stix_data, _STIXBase):
|
||||
if any(x in ('STIXDomainObject', 'STIXRelationshipObject', 'MarkingDefinition')
|
||||
for x in get_class_hierarchy_names(stix_data)):
|
||||
# adding python STIX object
|
||||
_check_path_and_write(self._stix_dir, stix_data)
|
||||
self._check_path_and_write(stix_data)
|
||||
|
||||
elif isinstance(stix_data, dict):
|
||||
elif isinstance(stix_data, (str, dict)):
|
||||
stix_data = parse(stix_data, allow_custom, version)
|
||||
if stix_data["type"] == "bundle":
|
||||
# adding json-formatted Bundle - extracting STIX objects
|
||||
for stix_obj in stix_data["objects"]:
|
||||
# extract STIX objects
|
||||
for stix_obj in stix_data.get("objects", []):
|
||||
self.add(stix_obj)
|
||||
else:
|
||||
# adding json-formatted STIX
|
||||
_check_path_and_write(self._stix_dir, stix_data)
|
||||
self._check_path_and_write(stix_data)
|
||||
|
||||
elif isinstance(stix_data, str):
|
||||
# adding json encoded string of STIX content
|
||||
stix_data = parse(stix_data)
|
||||
if stix_data["type"] == "bundle":
|
||||
for stix_obj in stix_data["objects"]:
|
||||
self.add(stix_obj)
|
||||
else:
|
||||
self.add(stix_data)
|
||||
elif isinstance(stix_data, Bundle):
|
||||
# recursively add individual STIX objects
|
||||
for stix_obj in stix_data.get("objects", []):
|
||||
self.add(stix_obj)
|
||||
|
||||
elif isinstance(stix_data, list):
|
||||
# if list, recurse call on individual STIX objects
|
||||
# recursively add individual STIX objects
|
||||
for stix_obj in stix_data:
|
||||
self.add(stix_obj)
|
||||
|
||||
else:
|
||||
raise ValueError("stix_data must be a STIX object(or list of), json formatted STIX(or list of) or a json formatted STIX bundle")
|
||||
raise TypeError("stix_data must be a STIX object (or list of), "
|
||||
"JSON formatted STIX (or list of), "
|
||||
"or a JSON formatted STIX bundle")
|
||||
|
||||
|
||||
class FileSystemSource(DataSource):
|
||||
"""FileSystemSource
|
||||
|
||||
Provides an interface for searching/retrieving
|
||||
STIX objects from a STIX object file directory.
|
||||
"""Interface for searching/retrieving STIX objects from a STIX object file
|
||||
directory.
|
||||
|
||||
Can be paired with a FileSystemSink, together as the two
|
||||
components of a FileSystemStore.
|
||||
|
@ -136,14 +146,17 @@ class FileSystemSource(DataSource):
|
|||
def stix_dir(self):
|
||||
return self._stix_dir
|
||||
|
||||
def get(self, stix_id, _composite_filters=None):
|
||||
"""retrieve STIX object from file directory via STIX ID
|
||||
def get(self, stix_id, _composite_filters=None, allow_custom=False, version=None):
|
||||
"""Retrieve STIX object from file directory via STIX ID.
|
||||
|
||||
Args:
|
||||
stix_id (str): The STIX ID of the STIX object to be retrieved.
|
||||
|
||||
composite_filters (set): set of filters passed from the parent
|
||||
_composite_filters (set): set of filters passed from the parent
|
||||
CompositeDataSource, not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If
|
||||
None, use latest version.
|
||||
|
||||
Returns:
|
||||
(STIX object): STIX object that has the supplied STIX ID.
|
||||
|
@ -153,47 +166,55 @@ class FileSystemSource(DataSource):
|
|||
"""
|
||||
query = [Filter("id", "=", stix_id)]
|
||||
|
||||
all_data = self.query(query=query, _composite_filters=_composite_filters)
|
||||
all_data = self.query(query=query, _composite_filters=_composite_filters,
|
||||
allow_custom=allow_custom, version=version)
|
||||
|
||||
if all_data:
|
||||
stix_obj = sorted(all_data, key=lambda k: k['modified'])[0]
|
||||
stix_obj = parse(stix_obj)
|
||||
else:
|
||||
stix_obj = None
|
||||
|
||||
return stix_obj
|
||||
|
||||
def all_versions(self, stix_id, _composite_filters=None):
|
||||
"""retrieve STIX object from file directory via STIX ID, all versions
|
||||
def all_versions(self, stix_id, _composite_filters=None, allow_custom=False, version=None):
|
||||
"""Retrieve STIX object from file directory via STIX ID, all versions.
|
||||
|
||||
Note: Since FileSystem sources/sinks don't handle multiple versions
|
||||
of a STIX object, this operation is unnecessary. Pass call to get().
|
||||
|
||||
Args:
|
||||
stix_id (str): The STIX ID of the STIX objects to be retrieved.
|
||||
|
||||
composite_filters (set): set of filters passed from the parent
|
||||
_composite_filters (set): set of filters passed from the parent
|
||||
CompositeDataSource, not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If
|
||||
None, use latest version.
|
||||
|
||||
Returns:
|
||||
(list): of STIX objects that has the supplied STIX ID.
|
||||
The STIX objects are loaded from their json files, parsed into
|
||||
a python STIX objects and then returned
|
||||
"""
|
||||
return [self.get(stix_id=stix_id, _composite_filters=_composite_filters)]
|
||||
|
||||
def query(self, query=None, _composite_filters=None):
|
||||
"""search and retrieve STIX objects based on the complete query
|
||||
"""
|
||||
return [self.get(stix_id=stix_id, _composite_filters=_composite_filters,
|
||||
allow_custom=allow_custom, version=version)]
|
||||
|
||||
def query(self, query=None, _composite_filters=None, allow_custom=False, version=None):
|
||||
"""Search and retrieve STIX objects based on the complete query.
|
||||
|
||||
A "complete query" includes the filters from the query, the filters
|
||||
attached to MemorySource, and any filters passed from a
|
||||
CompositeDataSource (i.e. _composite_filters)
|
||||
attached to this FileSystemSource, and any filters passed from a
|
||||
CompositeDataSource (i.e. _composite_filters).
|
||||
|
||||
Args:
|
||||
query (list): list of filters to search on
|
||||
|
||||
composite_filters (set): set of filters passed from the
|
||||
_composite_filters (set): set of filters passed from the
|
||||
CompositeDataSource, not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If
|
||||
None, use latest version.
|
||||
|
||||
Returns:
|
||||
(list): list of STIX objects that matches the supplied
|
||||
|
@ -209,7 +230,7 @@ class FileSystemSource(DataSource):
|
|||
if not isinstance(query, list):
|
||||
# make sure dont make set from a Filter object,
|
||||
# need to make a set from a list of Filter objects (even if just one Filter)
|
||||
query = list(query)
|
||||
query = [query]
|
||||
query = set(query)
|
||||
|
||||
# combine all query filters
|
||||
|
@ -225,14 +246,14 @@ class FileSystemSource(DataSource):
|
|||
file_filters = self._parse_file_filters(query)
|
||||
|
||||
# establish which subdirectories can be avoided in query
|
||||
# by decluding as many as possible. A filter with "type" as the field
|
||||
# by decluding as many as possible. A filter with "type" as the property
|
||||
# means that certain STIX object types can be ruled out, and thus
|
||||
# the corresponding subdirectories as well
|
||||
include_paths = []
|
||||
declude_paths = []
|
||||
if "type" in [filter.field for filter in file_filters]:
|
||||
if "type" in [filter.property for filter in file_filters]:
|
||||
for filter in file_filters:
|
||||
if filter.field == "type":
|
||||
if filter.property == "type":
|
||||
if filter.op == "=":
|
||||
include_paths.append(os.path.join(self._stix_dir, filter.value))
|
||||
elif filter.op == "!=":
|
||||
|
@ -254,14 +275,14 @@ class FileSystemSource(DataSource):
|
|||
# so query will look in all STIX directories that are not
|
||||
# the specified type. Compile correct dir paths
|
||||
for dir in os.listdir(self._stix_dir):
|
||||
if os.path.abspath(dir) not in declude_paths:
|
||||
include_paths.append(os.path.abspath(dir))
|
||||
if os.path.abspath(os.path.join(self._stix_dir, dir)) not in declude_paths:
|
||||
include_paths.append(os.path.abspath(os.path.join(self._stix_dir, dir)))
|
||||
|
||||
# grab stix object ID as well - if present in filters, as
|
||||
# may forgo the loading of STIX content into memory
|
||||
if "id" in [filter.field for filter in file_filters]:
|
||||
if "id" in [filter.property for filter in file_filters]:
|
||||
for filter in file_filters:
|
||||
if filter.field == "id" and filter.op == "=":
|
||||
if filter.property == "id" and filter.op == "=":
|
||||
id_ = filter.value
|
||||
break
|
||||
else:
|
||||
|
@ -273,37 +294,35 @@ class FileSystemSource(DataSource):
|
|||
for path in include_paths:
|
||||
for root, dirs, files in os.walk(path):
|
||||
for file_ in files:
|
||||
if id_:
|
||||
if id_ == file_.split(".")[0]:
|
||||
# since ID is specified in one of filters, can evaluate against filename first without loading
|
||||
stix_obj = json.load(open(os.path.join(root, file_)))["objects"][0]
|
||||
# check against other filters, add if match
|
||||
all_data.extend(apply_common_filters([stix_obj], query))
|
||||
else:
|
||||
if not id_ or id_ == file_.split(".")[0]:
|
||||
# have to load into memory regardless to evaluate other filters
|
||||
stix_obj = json.load(open(os.path.join(root, file_)))["objects"][0]
|
||||
stix_obj = json.load(open(os.path.join(root, file_)))
|
||||
if stix_obj.get('type', '') == 'bundle':
|
||||
stix_obj = stix_obj['objects'][0]
|
||||
# check against other filters, add if match
|
||||
all_data.extend(apply_common_filters([stix_obj], query))
|
||||
|
||||
all_data = deduplicate(all_data)
|
||||
|
||||
# parse python STIX objects from the STIX object dicts
|
||||
stix_objs = [parse(stix_obj_dict) for stix_obj_dict in all_data]
|
||||
stix_objs = [parse(stix_obj_dict, allow_custom, version) for stix_obj_dict in all_data]
|
||||
|
||||
return stix_objs
|
||||
|
||||
def _parse_file_filters(self, query):
|
||||
"""utility method to extract STIX common filters
|
||||
that can used to possibly speed up querying STIX objects
|
||||
from the file system
|
||||
"""Extract STIX common filters.
|
||||
|
||||
Extracts filters that are for the "id" and "type" field of
|
||||
Possibly speeds up querying STIX objects from the file system.
|
||||
|
||||
Extracts filters that are for the "id" and "type" property of
|
||||
a STIX object. As the file directory is organized by STIX
|
||||
object type with filenames that are equivalent to the STIX
|
||||
object ID, these filters can be used first to reduce the
|
||||
search space of a FileSystemStore(or FileSystemSink)
|
||||
search space of a FileSystemStore (or FileSystemSink).
|
||||
|
||||
"""
|
||||
file_filters = set()
|
||||
for filter_ in query:
|
||||
if filter_.field == "id" or filter_.field == "type":
|
||||
if filter_.property == "id" or filter_.property == "type":
|
||||
file_filters.add(filter_)
|
||||
return file_filters
|
||||
|
|
|
@ -4,29 +4,6 @@ Filters for Python STIX 2.0 DataSources, DataSinks, DataStores
|
|||
"""
|
||||
|
||||
import collections
|
||||
import types
|
||||
|
||||
# Currently, only STIX 2.0 common SDO fields (that are not complex objects)
|
||||
# are supported for filtering on
|
||||
|
||||
"""Supported STIX properties"""
|
||||
STIX_COMMON_FIELDS = [
|
||||
"created",
|
||||
"created_by_ref",
|
||||
"external_references.source_name",
|
||||
"external_references.description",
|
||||
"external_references.url",
|
||||
"external_references.hashes",
|
||||
"external_references.external_id",
|
||||
"granular_markings.marking_ref",
|
||||
"granular_markings.selectors",
|
||||
"id",
|
||||
"labels",
|
||||
"modified",
|
||||
"object_marking_refs",
|
||||
"revoked",
|
||||
"type"
|
||||
]
|
||||
|
||||
"""Supported filter operations"""
|
||||
FILTER_OPS = ['=', '!=', 'in', '>', '<', '>=', '<=']
|
||||
|
@ -34,46 +11,40 @@ FILTER_OPS = ['=', '!=', 'in', '>', '<', '>=', '<=']
|
|||
"""Supported filter value types"""
|
||||
FILTER_VALUE_TYPES = [bool, dict, float, int, list, str, tuple]
|
||||
|
||||
# filter lookup map - STIX 2 common fields -> filter method
|
||||
STIX_COMMON_FILTERS_MAP = {}
|
||||
|
||||
def _check_filter_components(prop, op, value):
|
||||
"""Check that filter meets minimum validity.
|
||||
|
||||
Note:
|
||||
Currently can create Filters that are not valid STIX2 object common
|
||||
properties, as filter.prop value is not checked, only filter.op,
|
||||
filter value are checked here. They are just ignored when applied
|
||||
within the DataSource API. For example, a user can add a TAXII Filter,
|
||||
that is extracted and sent to a TAXII endpoint within TAXIICollection
|
||||
and not applied locally (within this API).
|
||||
|
||||
"""
|
||||
if op not in FILTER_OPS:
|
||||
# check filter operator is supported
|
||||
raise ValueError("Filter operator '%s' not supported for specified property: '%s'" % (op, prop))
|
||||
|
||||
if type(value) not in FILTER_VALUE_TYPES:
|
||||
# check filter value type is supported
|
||||
raise TypeError("Filter value type '%s' is not supported. The type must be a Python immutable type or dictionary" % type(value))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _check_filter_components(field, op, value):
|
||||
"""check filter meets minimum validity
|
||||
|
||||
Note: Currently can create Filters that are not valid
|
||||
STIX2 object common properties, as filter.field value
|
||||
is not checked, only filter.op, filter.value are checked
|
||||
here. They are just ignored when
|
||||
applied within the DataSource API. For example, a user
|
||||
can add a TAXII Filter, that is extracted and sent to
|
||||
a TAXII endpoint within TAXIICollection and not applied
|
||||
locally (within this API).
|
||||
"""
|
||||
|
||||
if op not in FILTER_OPS:
|
||||
# check filter operator is supported
|
||||
raise ValueError("Filter operator '%s' not supported for specified field: '%s'" % (op, field))
|
||||
|
||||
if type(value) not in FILTER_VALUE_TYPES:
|
||||
# check filter value type is supported
|
||||
raise TypeError("Filter value type '%s' is not supported. The type must be a python immutable type or dictionary" % type(value))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Filter(collections.namedtuple("Filter", ['field', 'op', 'value'])):
|
||||
class Filter(collections.namedtuple("Filter", ['property', 'op', 'value'])):
|
||||
"""STIX 2 filters that support the querying functionality of STIX 2
|
||||
DataStores and DataSources.
|
||||
|
||||
Initialized like a python tuple
|
||||
Initialized like a Python tuple.
|
||||
|
||||
Args:
|
||||
field (str): filter field name, corresponds to STIX 2 object property
|
||||
|
||||
property (str): filter property name, corresponds to STIX 2 object property
|
||||
op (str): operator of the filter
|
||||
|
||||
value (str): filter field value
|
||||
value (str): filter property value
|
||||
|
||||
Example:
|
||||
Filter("id", "=", "malware--0f862b01-99da-47cc-9bdb-db4a86a95bb1")
|
||||
|
@ -81,235 +52,110 @@ class Filter(collections.namedtuple("Filter", ['field', 'op', 'value'])):
|
|||
"""
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, field, op, value):
|
||||
def __new__(cls, prop, op, value):
|
||||
# If value is a list, convert it to a tuple so it is hashable.
|
||||
if isinstance(value, list):
|
||||
value = tuple(value)
|
||||
|
||||
_check_filter_components(field, op, value)
|
||||
_check_filter_components(prop, op, value)
|
||||
|
||||
self = super(Filter, cls).__new__(cls, field, op, value)
|
||||
self = super(Filter, cls).__new__(cls, prop, op, value)
|
||||
return self
|
||||
|
||||
@property
|
||||
def common(self):
|
||||
"""return whether Filter is valid STIX2 Object common property
|
||||
def _check_property(self, stix_obj_property):
|
||||
"""Check a property of a STIX Object against this filter.
|
||||
|
||||
Note: The Filter operator and Filter value type are checked when
|
||||
the filter is created, thus only leaving the Filter field to be
|
||||
checked to make sure a valid STIX2 Object common property.
|
||||
Args:
|
||||
stix_obj_property: value to check this filter against
|
||||
|
||||
Note: Filters that are not valid STIX2 Object common property
|
||||
Filters are still allowed to be created for extended usage of
|
||||
Filter. (e.g. TAXII specific filters can be created, which are
|
||||
then extracted and sent to TAXII endpoint.)
|
||||
Returns:
|
||||
True if property matches the filter,
|
||||
False otherwise.
|
||||
"""
|
||||
return self.field in STIX_COMMON_FIELDS
|
||||
if self.op == "=":
|
||||
return stix_obj_property == self.value
|
||||
elif self.op == "!=":
|
||||
return stix_obj_property != self.value
|
||||
elif self.op == "in":
|
||||
return stix_obj_property in self.value
|
||||
elif self.op == ">":
|
||||
return stix_obj_property > self.value
|
||||
elif self.op == "<":
|
||||
return stix_obj_property < self.value
|
||||
elif self.op == ">=":
|
||||
return stix_obj_property >= self.value
|
||||
elif self.op == "<=":
|
||||
return stix_obj_property <= self.value
|
||||
else:
|
||||
raise ValueError("Filter operator: {0} not supported for specified property: {1}".format(self.op, self.property))
|
||||
|
||||
|
||||
def apply_common_filters(stix_objs, query):
|
||||
"""Evaluate filters against a set of STIX 2.0 objects.
|
||||
|
||||
Supports only STIX 2.0 common property fields
|
||||
Supports only STIX 2.0 common property properties.
|
||||
|
||||
Args:
|
||||
stix_objs (list): list of STIX objects to apply the query to
|
||||
query (set): set of filters (combined form complete query)
|
||||
|
||||
Returns:
|
||||
(generator): of STIX objects that successfully evaluate against
|
||||
the query.
|
||||
Yields:
|
||||
STIX objects that successfully evaluate against the query.
|
||||
|
||||
"""
|
||||
for stix_obj in stix_objs:
|
||||
clean = True
|
||||
for filter_ in query:
|
||||
if not filter_.common:
|
||||
# skip filter as it is not a STIX2 Object common property filter
|
||||
continue
|
||||
|
||||
if "." in filter_.field:
|
||||
# For properties like granular_markings and external_references
|
||||
# need to extract the first property from the string.
|
||||
field = filter_.field.split(".")[0]
|
||||
else:
|
||||
field = filter_.field
|
||||
|
||||
if field not in stix_obj.keys():
|
||||
# check filter "field" is in STIX object - if cant be
|
||||
# applied to STIX object, STIX object is discarded
|
||||
# (i.e. did not make it through the filter)
|
||||
clean = False
|
||||
break
|
||||
|
||||
match = STIX_COMMON_FILTERS_MAP[filter_.field.split('.')[0]](filter_, stix_obj)
|
||||
match = _check_filter(filter_, stix_obj)
|
||||
|
||||
if not match:
|
||||
clean = False
|
||||
break
|
||||
elif match == -1:
|
||||
raise ValueError("Error, filter operator: {0} not supported for specified field: {1}".format(filter_.op, filter_.field))
|
||||
|
||||
# if object unmarked after all filters, add it
|
||||
if clean:
|
||||
yield stix_obj
|
||||
|
||||
|
||||
"""Base type filters"""
|
||||
def _check_filter(filter_, stix_obj):
|
||||
"""Evaluate a single filter against a single STIX 2.0 object.
|
||||
|
||||
Args:
|
||||
filter_ (Filter): filter to match against
|
||||
stix_obj: STIX object to apply the filter to
|
||||
|
||||
def _all_filter(filter_, stix_obj_field):
|
||||
"""all filter operations (for filters whose value type can be applied to any operation type)"""
|
||||
if filter_.op == "=":
|
||||
return stix_obj_field == filter_.value
|
||||
elif filter_.op == "!=":
|
||||
return stix_obj_field != filter_.value
|
||||
elif filter_.op == "in":
|
||||
return stix_obj_field in filter_.value
|
||||
elif filter_.op == ">":
|
||||
return stix_obj_field > filter_.value
|
||||
elif filter_.op == "<":
|
||||
return stix_obj_field < filter_.value
|
||||
elif filter_.op == ">=":
|
||||
return stix_obj_field >= filter_.value
|
||||
elif filter_.op == "<=":
|
||||
return stix_obj_field <= filter_.value
|
||||
Returns:
|
||||
True if the stix_obj matches the filter,
|
||||
False if not.
|
||||
|
||||
"""
|
||||
# For properties like granular_markings and external_references
|
||||
# need to extract the first property from the string.
|
||||
prop = filter_.property.split(".")[0]
|
||||
|
||||
if prop not in stix_obj.keys():
|
||||
# check filter "property" is in STIX object - if cant be
|
||||
# applied to STIX object, STIX object is discarded
|
||||
# (i.e. did not make it through the filter)
|
||||
return False
|
||||
|
||||
if "." in filter_.property:
|
||||
# Check embedded properties, from e.g. granular_markings or external_references
|
||||
sub_property = filter_.property.split(".", 1)[1]
|
||||
sub_filter = filter_._replace(property=sub_property)
|
||||
if isinstance(stix_obj[prop], list):
|
||||
for elem in stix_obj[prop]:
|
||||
if _check_filter(sub_filter, elem) is True:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return _check_filter(sub_filter, stix_obj[prop])
|
||||
elif isinstance(stix_obj[prop], list):
|
||||
# Check each item in list property to see if it matches
|
||||
for elem in stix_obj[prop]:
|
||||
if filter_._check_property(elem) is True:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return -1
|
||||
|
||||
|
||||
def _id_filter(filter_, stix_obj_id):
|
||||
"""base STIX id filter"""
|
||||
if filter_.op == "=":
|
||||
return stix_obj_id == filter_.value
|
||||
elif filter_.op == "!=":
|
||||
return stix_obj_id != filter_.value
|
||||
else:
|
||||
return -1
|
||||
|
||||
|
||||
def _boolean_filter(filter_, stix_obj_field):
|
||||
"""base boolean filter"""
|
||||
if filter_.op == "=":
|
||||
return stix_obj_field == filter_.value
|
||||
elif filter_.op == "!=":
|
||||
return stix_obj_field != filter_.value
|
||||
else:
|
||||
return -1
|
||||
|
||||
|
||||
def _string_filter(filter_, stix_obj_field):
|
||||
"""base string filter"""
|
||||
return _all_filter(filter_, stix_obj_field)
|
||||
|
||||
|
||||
def _timestamp_filter(filter_, stix_obj_timestamp):
|
||||
"""base STIX 2 timestamp filter"""
|
||||
return _all_filter(filter_, stix_obj_timestamp)
|
||||
|
||||
|
||||
"""STIX 2.0 Common Property Filters
|
||||
|
||||
The naming of these functions is important as
|
||||
they are used to index a mapping dictionary from
|
||||
STIX common field names to these filter functions.
|
||||
|
||||
REQUIRED naming scheme:
|
||||
"check_<STIX field name>_filter"
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def check_created_filter(filter_, stix_obj):
|
||||
return _timestamp_filter(filter_, stix_obj["created"])
|
||||
|
||||
|
||||
def check_created_by_ref_filter(filter_, stix_obj):
|
||||
return _id_filter(filter_, stix_obj["created_by_ref"])
|
||||
|
||||
|
||||
def check_external_references_filter(filter_, stix_obj):
|
||||
"""
|
||||
STIX object's can have a list of external references
|
||||
|
||||
external_references properties supported:
|
||||
external_references.source_name (string)
|
||||
external_references.description (string)
|
||||
external_references.url (string)
|
||||
external_references.external_id (string)
|
||||
|
||||
external_references properties not supported:
|
||||
external_references.hashes
|
||||
|
||||
"""
|
||||
for er in stix_obj["external_references"]:
|
||||
# grab er property name from filter field
|
||||
filter_field = filter_.field.split(".")[1]
|
||||
if filter_field in er:
|
||||
r = _string_filter(filter_, er[filter_field])
|
||||
if r:
|
||||
return r
|
||||
return False
|
||||
|
||||
|
||||
def check_granular_markings_filter(filter_, stix_obj):
|
||||
"""
|
||||
STIX object's can have a list of granular marking references
|
||||
|
||||
granular_markings properties:
|
||||
granular_markings.marking_ref (id)
|
||||
granular_markings.selectors (string)
|
||||
|
||||
"""
|
||||
for gm in stix_obj["granular_markings"]:
|
||||
# grab gm property name from filter field
|
||||
filter_field = filter_.field.split(".")[1]
|
||||
|
||||
if filter_field == "marking_ref":
|
||||
return _id_filter(filter_, gm[filter_field])
|
||||
|
||||
elif filter_field == "selectors":
|
||||
for selector in gm[filter_field]:
|
||||
r = _string_filter(filter_, selector)
|
||||
if r:
|
||||
return r
|
||||
return False
|
||||
|
||||
|
||||
def check_id_filter(filter_, stix_obj):
|
||||
return _id_filter(filter_, stix_obj["id"])
|
||||
|
||||
|
||||
def check_labels_filter(filter_, stix_obj):
|
||||
for label in stix_obj["labels"]:
|
||||
r = _string_filter(filter_, label)
|
||||
if r:
|
||||
return r
|
||||
return False
|
||||
|
||||
|
||||
def check_modified_filter(filter_, stix_obj):
|
||||
return _timestamp_filter(filter_, stix_obj["modified"])
|
||||
|
||||
|
||||
def check_object_marking_refs_filter(filter_, stix_obj):
|
||||
for marking_id in stix_obj["object_marking_refs"]:
|
||||
r = _id_filter(filter_, marking_id)
|
||||
if r:
|
||||
return r
|
||||
return False
|
||||
|
||||
|
||||
def check_revoked_filter(filter_, stix_obj):
|
||||
return _boolean_filter(filter_, stix_obj["revoked"])
|
||||
|
||||
|
||||
def check_type_filter(filter_, stix_obj):
|
||||
return _string_filter(filter_, stix_obj["type"])
|
||||
|
||||
|
||||
# Create mapping of field names to filter functions
|
||||
for name, obj in dict(globals()).items():
|
||||
if "check_" in name and isinstance(obj, types.FunctionType):
|
||||
field_name = "_".join(name.split("_")[1:-1])
|
||||
STIX_COMMON_FILTERS_MAP[field_name] = obj
|
||||
# Check if property matches
|
||||
return filter_._check_property(stix_obj[prop])
|
||||
|
|
|
@ -24,16 +24,18 @@ from stix2.sources import DataSink, DataSource, DataStore
|
|||
from stix2.sources.filters import Filter, apply_common_filters
|
||||
|
||||
|
||||
def _add(store, stix_data=None):
|
||||
"""Adds STIX objects to MemoryStore/Sink.
|
||||
def _add(store, stix_data=None, allow_custom=False):
|
||||
"""Add STIX objects to MemoryStore/Sink.
|
||||
|
||||
Adds STIX objects to an in-memory dictionary for fast lookup.
|
||||
Recursive function, breaks down STIX Bundles and lists.
|
||||
|
||||
Args:
|
||||
stix_data (list OR dict OR STIX object): STIX objects to be added
|
||||
"""
|
||||
allow_custom (bool): whether to allow custom objects/properties or
|
||||
not. Default: False.
|
||||
|
||||
"""
|
||||
if isinstance(stix_data, _STIXBase):
|
||||
# adding a python STIX object
|
||||
store._data[stix_data["id"]] = stix_data
|
||||
|
@ -41,35 +43,35 @@ def _add(store, stix_data=None):
|
|||
elif isinstance(stix_data, dict):
|
||||
if stix_data["type"] == "bundle":
|
||||
# adding a json bundle - so just grab STIX objects
|
||||
for stix_obj in stix_data["objects"]:
|
||||
_add(store, stix_obj)
|
||||
for stix_obj in stix_data.get("objects", []):
|
||||
_add(store, stix_obj, allow_custom=allow_custom)
|
||||
else:
|
||||
# adding a json STIX object
|
||||
store._data[stix_data["id"]] = stix_data
|
||||
|
||||
elif isinstance(stix_data, str):
|
||||
# adding json encoded string of STIX content
|
||||
stix_data = parse(stix_data)
|
||||
stix_data = parse(stix_data, allow_custom=allow_custom)
|
||||
if stix_data["type"] == "bundle":
|
||||
# recurse on each STIX object in bundle
|
||||
for stix_obj in stix_data:
|
||||
_add(store, stix_obj)
|
||||
for stix_obj in stix_data.get("objects", []):
|
||||
_add(store, stix_obj, allow_custom=allow_custom)
|
||||
else:
|
||||
_add(store, stix_data)
|
||||
|
||||
elif isinstance(stix_data, list):
|
||||
# STIX objects are in a list- recurse on each object
|
||||
for stix_obj in stix_data:
|
||||
_add(store, stix_obj)
|
||||
_add(store, stix_obj, allow_custom=allow_custom)
|
||||
|
||||
else:
|
||||
raise TypeError("stix_data must be as STIX object(or list of),json formatted STIX (or list of), or a json formatted STIX bundle")
|
||||
raise TypeError("stix_data must be a STIX object (or list of), JSON formatted STIX (or list of), or a JSON formatted STIX bundle")
|
||||
|
||||
|
||||
class MemoryStore(DataStore):
|
||||
"""Provides an interface to an in-memory dictionary
|
||||
of STIX objects. MemoryStore is a wrapper around a paired
|
||||
MemorySink and MemorySource
|
||||
"""Interface to an in-memory dictionary of STIX objects.
|
||||
|
||||
MemoryStore is a wrapper around a paired MemorySink and MemorySource.
|
||||
|
||||
Note: It doesn't make sense to create a MemoryStore by passing
|
||||
in existing MemorySource and MemorySink because there could
|
||||
|
@ -77,36 +79,54 @@ class MemoryStore(DataStore):
|
|||
|
||||
Args:
|
||||
stix_data (list OR dict OR STIX object): STIX content to be added
|
||||
allow_custom (bool): whether to allow custom objects/properties or
|
||||
not. Default: False.
|
||||
|
||||
Attributes:
|
||||
_data (dict): the in-memory dict that holds STIX objects
|
||||
|
||||
source (MemorySource): MemorySource
|
||||
|
||||
sink (MemorySink): MemorySink
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, stix_data=None):
|
||||
def __init__(self, stix_data=None, allow_custom=False):
|
||||
super(MemoryStore, self).__init__()
|
||||
self._data = {}
|
||||
|
||||
if stix_data:
|
||||
_add(self, stix_data)
|
||||
_add(self, stix_data, allow_custom=allow_custom)
|
||||
|
||||
self.source = MemorySource(stix_data=self._data, _store=True)
|
||||
self.sink = MemorySink(stix_data=self._data, _store=True)
|
||||
self.source = MemorySource(stix_data=self._data, _store=True, allow_custom=allow_custom)
|
||||
self.sink = MemorySink(stix_data=self._data, _store=True, allow_custom=allow_custom)
|
||||
|
||||
def save_to_file(self, file_path):
|
||||
return self.sink.save_to_file(file_path=file_path)
|
||||
def save_to_file(self, file_path, allow_custom=False):
|
||||
"""Write SITX objects from in-memory dictionary to JSON file, as a STIX
|
||||
Bundle.
|
||||
|
||||
def load_from_file(self, file_path):
|
||||
return self.source.load_from_file(file_path=file_path)
|
||||
Args:
|
||||
file_path (str): file path to write STIX data to
|
||||
allow_custom (bool): whether to allow custom objects/properties or
|
||||
not. Default: False.
|
||||
|
||||
"""
|
||||
return self.sink.save_to_file(file_path=file_path, allow_custom=allow_custom)
|
||||
|
||||
def load_from_file(self, file_path, allow_custom=False):
|
||||
"""Load STIX data from JSON file.
|
||||
|
||||
File format is expected to be a single JSON
|
||||
STIX object or JSON STIX bundle.
|
||||
|
||||
Args:
|
||||
file_path (str): file path to load STIX data from
|
||||
allow_custom (bool): whether to allow custom objects/properties or
|
||||
not. Default: False.
|
||||
|
||||
"""
|
||||
return self.source.load_from_file(file_path=file_path, allow_custom=allow_custom)
|
||||
|
||||
|
||||
class MemorySink(DataSink):
|
||||
"""Provides an interface for adding/pushing STIX objects
|
||||
to an in-memory dictionary.
|
||||
"""Interface for adding/pushing STIX objects to an in-memory dictionary.
|
||||
|
||||
Designed to be paired with a MemorySource, together as the two
|
||||
components of a MemoryStore.
|
||||
|
@ -114,51 +134,43 @@ class MemorySink(DataSink):
|
|||
Args:
|
||||
stix_data (dict OR list): valid STIX 2.0 content in
|
||||
bundle or a list.
|
||||
|
||||
_store (bool): if the MemorySink is a part of a DataStore,
|
||||
in which case "stix_data" is a direct reference to
|
||||
shared memory with DataSource. Not user supplied
|
||||
allow_custom (bool): whether to allow custom objects/properties or
|
||||
not. Default: False.
|
||||
|
||||
Attributes:
|
||||
_data (dict): the in-memory dict that holds STIX objects.
|
||||
If apart of a MemoryStore, dict is shared between with
|
||||
a MemorySource
|
||||
"""
|
||||
|
||||
def __init__(self, stix_data=None, _store=False):
|
||||
"""
|
||||
def __init__(self, stix_data=None, _store=False, allow_custom=False):
|
||||
super(MemorySink, self).__init__()
|
||||
self._data = {}
|
||||
|
||||
if _store:
|
||||
self._data = stix_data
|
||||
elif stix_data:
|
||||
_add(self, stix_data)
|
||||
_add(self, stix_data, allow_custom=allow_custom)
|
||||
|
||||
def add(self, stix_data):
|
||||
"""add STIX objects to in-memory dictionary maintained by
|
||||
the MemorySink (MemoryStore)
|
||||
def add(self, stix_data, allow_custom=False):
|
||||
_add(self, stix_data, allow_custom=allow_custom)
|
||||
add.__doc__ = _add.__doc__
|
||||
|
||||
see "_add()" for args documentation
|
||||
"""
|
||||
_add(self, stix_data)
|
||||
|
||||
def save_to_file(self, file_path):
|
||||
"""write SITX objects in in-memory dictionary to json file, as a STIX Bundle
|
||||
|
||||
Args:
|
||||
file_path (str): file path to write STIX data to
|
||||
|
||||
"""
|
||||
def save_to_file(self, file_path, allow_custom=False):
|
||||
file_path = os.path.abspath(file_path)
|
||||
if not os.path.exists(os.path.dirname(file_path)):
|
||||
os.makedirs(os.path.dirname(file_path))
|
||||
with open(file_path, "w") as f:
|
||||
f.write(str(Bundle(self._data.values())))
|
||||
f.write(str(Bundle(self._data.values(), allow_custom=allow_custom)))
|
||||
save_to_file.__doc__ = MemoryStore.save_to_file.__doc__
|
||||
|
||||
|
||||
class MemorySource(DataSource):
|
||||
"""Provides an interface for searching/retrieving
|
||||
STIX objects from an in-memory dictionary.
|
||||
"""Interface for searching/retrieving STIX objects from an in-memory
|
||||
dictionary.
|
||||
|
||||
Designed to be paired with a MemorySink, together as the two
|
||||
components of a MemoryStore.
|
||||
|
@ -166,42 +178,44 @@ class MemorySource(DataSource):
|
|||
Args:
|
||||
stix_data (dict OR list OR STIX object): valid STIX 2.0 content in
|
||||
bundle or list.
|
||||
|
||||
_store (bool): if the MemorySource is a part of a DataStore,
|
||||
in which case "stix_data" is a direct reference to shared
|
||||
memory with DataSink. Not user supplied
|
||||
allow_custom (bool): whether to allow custom objects/properties or
|
||||
not. Default: False.
|
||||
|
||||
Attributes:
|
||||
_data (dict): the in-memory dict that holds STIX objects.
|
||||
If apart of a MemoryStore, dict is shared between with
|
||||
a MemorySink
|
||||
"""
|
||||
|
||||
def __init__(self, stix_data=None, _store=False):
|
||||
"""
|
||||
def __init__(self, stix_data=None, _store=False, allow_custom=False):
|
||||
super(MemorySource, self).__init__()
|
||||
self._data = {}
|
||||
|
||||
if _store:
|
||||
self._data = stix_data
|
||||
elif stix_data:
|
||||
_add(self, stix_data)
|
||||
_add(self, stix_data, allow_custom=allow_custom)
|
||||
|
||||
def get(self, stix_id, _composite_filters=None):
|
||||
"""retrieve STIX object from in-memory dict via STIX ID
|
||||
def get(self, stix_id, _composite_filters=None, allow_custom=False):
|
||||
"""Retrieve STIX object from in-memory dict via STIX ID.
|
||||
|
||||
Args:
|
||||
stix_id (str): The STIX ID of the STIX object to be retrieved.
|
||||
|
||||
composite_filters (set): set of filters passed from the parent
|
||||
CompositeDataSource, not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
(dict OR STIX object): STIX object that has the supplied
|
||||
ID. As the MemoryStore(i.e. MemorySink) adds STIX objects to memory
|
||||
as they are supplied (either as python dictionary or STIX object), it
|
||||
is returned in the same form as it as added
|
||||
"""
|
||||
|
||||
"""
|
||||
if _composite_filters is None:
|
||||
# if get call is only based on 'id', no need to search, just retrieve from dict
|
||||
try:
|
||||
|
@ -213,24 +227,28 @@ class MemorySource(DataSource):
|
|||
# if there are filters from the composite level, process full query
|
||||
query = [Filter("id", "=", stix_id)]
|
||||
|
||||
all_data = self.query(query=query, _composite_filters=_composite_filters)
|
||||
all_data = self.query(query=query, _composite_filters=_composite_filters, allow_custom=allow_custom)
|
||||
|
||||
# reduce to most recent version
|
||||
stix_obj = sorted(all_data, key=lambda k: k['modified'])[0]
|
||||
if all_data:
|
||||
# reduce to most recent version
|
||||
stix_obj = sorted(all_data, key=lambda k: k['modified'])[0]
|
||||
|
||||
return stix_obj
|
||||
return stix_obj
|
||||
else:
|
||||
return None
|
||||
|
||||
def all_versions(self, stix_id, _composite_filters=None):
|
||||
"""retrieve STIX objects from in-memory dict via STIX ID, all versions of it
|
||||
def all_versions(self, stix_id, _composite_filters=None, allow_custom=False):
|
||||
"""Retrieve STIX objects from in-memory dict via STIX ID, all versions of it
|
||||
|
||||
Note: Since Memory sources/sinks don't handle multiple versions of a
|
||||
STIX object, this operation is unnecessary. Translate call to get().
|
||||
|
||||
Args:
|
||||
stix_id (str): The STIX ID of the STIX 2 object to retrieve.
|
||||
|
||||
composite_filters (set): set of filters passed from the parent
|
||||
CompositeDataSource, not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
(list): list of STIX objects that has the supplied ID. As the
|
||||
|
@ -239,26 +257,27 @@ class MemorySource(DataSource):
|
|||
is returned in the same form as it as added
|
||||
|
||||
"""
|
||||
return [self.get(stix_id=stix_id, _composite_filters=_composite_filters)]
|
||||
return [self.get(stix_id=stix_id, _composite_filters=_composite_filters, allow_custom=allow_custom)]
|
||||
|
||||
def query(self, query=None, _composite_filters=None):
|
||||
"""search and retrieve STIX objects based on the complete query
|
||||
def query(self, query=None, _composite_filters=None, allow_custom=False):
|
||||
"""Search and retrieve STIX objects based on the complete query.
|
||||
|
||||
A "complete query" includes the filters from the query, the filters
|
||||
attached to MemorySource, and any filters passed from a
|
||||
CompositeDataSource (i.e. _composite_filters)
|
||||
attached to this MemorySource, and any filters passed from a
|
||||
CompositeDataSource (i.e. _composite_filters).
|
||||
|
||||
Args:
|
||||
query (list): list of filters to search on
|
||||
|
||||
composite_filters (set): set of filters passed from the
|
||||
CompositeDataSource, not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
(list): list of STIX objects that matches the supplied
|
||||
query. As the MemoryStore(i.e. MemorySink) adds STIX objects to memory
|
||||
as they are supplied (either as python dictionary or STIX object), it
|
||||
is returned in the same form as it as added
|
||||
is returned in the same form as it as added.
|
||||
|
||||
"""
|
||||
if query is None:
|
||||
|
@ -267,7 +286,7 @@ class MemorySource(DataSource):
|
|||
if not isinstance(query, list):
|
||||
# make sure dont make set from a Filter object,
|
||||
# need to make a set from a list of Filter objects (even if just one Filter)
|
||||
query = list(query)
|
||||
query = [query]
|
||||
query = set(query)
|
||||
|
||||
# combine all query filters
|
||||
|
@ -281,15 +300,8 @@ class MemorySource(DataSource):
|
|||
|
||||
return all_data
|
||||
|
||||
def load_from_file(self, file_path):
|
||||
"""load STIX data from json file
|
||||
|
||||
File format is expected to be a single json
|
||||
STIX object or json STIX bundle
|
||||
|
||||
Args:
|
||||
file_path (str): file path to load STIX data from
|
||||
"""
|
||||
def load_from_file(self, file_path, allow_custom=False):
|
||||
file_path = os.path.abspath(file_path)
|
||||
stix_data = json.load(open(file_path, "r"))
|
||||
_add(self, stix_data)
|
||||
_add(self, stix_data, allow_custom=allow_custom)
|
||||
load_from_file.__doc__ = MemoryStore.load_from_file.__doc__
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
"""
|
||||
Python STIX 2.0 TAXII Source/Sink
|
||||
|
||||
TODO:
|
||||
Test everything
|
||||
|
||||
Python STIX 2.x TaxiiCollectionStore
|
||||
"""
|
||||
|
||||
from stix2.base import _STIXBase
|
||||
|
@ -21,7 +17,7 @@ class TAXIICollectionStore(DataStore):
|
|||
around a paired TAXIICollectionSink and TAXIICollectionSource.
|
||||
|
||||
Args:
|
||||
collection (taxii2.Collection): TAXII Collection instance
|
||||
collection (taxii2.Collection): TAXII Collection instance
|
||||
"""
|
||||
def __init__(self, collection):
|
||||
super(TAXIICollectionStore, self).__init__()
|
||||
|
@ -41,39 +37,40 @@ class TAXIICollectionSink(DataSink):
|
|||
super(TAXIICollectionSink, self).__init__()
|
||||
self.collection = collection
|
||||
|
||||
def add(self, stix_data):
|
||||
"""add/push STIX content to TAXII Collection endpoint
|
||||
def add(self, stix_data, allow_custom=False):
|
||||
"""Add/push STIX content to TAXII Collection endpoint
|
||||
|
||||
Args:
|
||||
stix_data (STIX object OR dict OR str OR list): valid STIX 2.0 content
|
||||
in a STIX object (or Bundle), STIX onject dict (or Bundle dict), or a STIX 2.0
|
||||
json encoded string, or list of any of the following
|
||||
allow_custom (bool): whether to allow custom objects/properties or
|
||||
not. Default: False.
|
||||
|
||||
"""
|
||||
|
||||
if isinstance(stix_data, _STIXBase):
|
||||
# adding python STIX object
|
||||
bundle = dict(Bundle(stix_data))
|
||||
bundle = dict(Bundle(stix_data, allow_custom=allow_custom))
|
||||
|
||||
elif isinstance(stix_data, dict):
|
||||
# adding python dict (of either Bundle or STIX obj)
|
||||
if stix_data["type"] == "bundle":
|
||||
bundle = stix_data
|
||||
else:
|
||||
bundle = dict(Bundle(stix_data))
|
||||
bundle = dict(Bundle(stix_data, allow_custom=allow_custom))
|
||||
|
||||
elif isinstance(stix_data, list):
|
||||
# adding list of something - recurse on each
|
||||
for obj in stix_data:
|
||||
self.add(obj)
|
||||
self.add(obj, allow_custom=allow_custom)
|
||||
|
||||
elif isinstance(stix_data, str):
|
||||
# adding json encoded string of STIX content
|
||||
stix_data = parse(stix_data)
|
||||
stix_data = parse(stix_data, allow_custom=allow_custom)
|
||||
if stix_data["type"] == "bundle":
|
||||
bundle = dict(stix_data)
|
||||
else:
|
||||
bundle = dict(Bundle(stix_data))
|
||||
bundle = dict(Bundle(stix_data, allow_custom=allow_custom))
|
||||
|
||||
else:
|
||||
raise TypeError("stix_data must be as STIX object(or list of),json formatted STIX (or list of), or a json formatted STIX bundle")
|
||||
|
@ -93,22 +90,22 @@ class TAXIICollectionSource(DataSource):
|
|||
super(TAXIICollectionSource, self).__init__()
|
||||
self.collection = collection
|
||||
|
||||
def get(self, stix_id, _composite_filters=None):
|
||||
"""retrieve STIX object from local/remote STIX Collection
|
||||
def get(self, stix_id, _composite_filters=None, allow_custom=False):
|
||||
"""Retrieve STIX object from local/remote STIX Collection
|
||||
endpoint.
|
||||
|
||||
Args:
|
||||
stix_id (str): The STIX ID of the STIX object to be retrieved.
|
||||
|
||||
composite_filters (set): set of filters passed from the parent
|
||||
CompositeDataSource, not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
(STIX object): STIX object that has the supplied STIX ID.
|
||||
The STIX object is received from TAXII has dict, parsed into
|
||||
a python STIX object and then returned
|
||||
|
||||
|
||||
"""
|
||||
# combine all query filters
|
||||
query = set()
|
||||
|
@ -124,22 +121,25 @@ class TAXIICollectionSource(DataSource):
|
|||
stix_obj = list(apply_common_filters(stix_objs, query))
|
||||
|
||||
if len(stix_obj):
|
||||
stix_obj = stix_obj[0]
|
||||
stix_obj = parse(stix_obj)
|
||||
stix_obj = parse(stix_obj[0], allow_custom=allow_custom)
|
||||
if stix_obj.id != stix_id:
|
||||
# check - was added to handle erroneous TAXII servers
|
||||
stix_obj = None
|
||||
else:
|
||||
stix_obj = None
|
||||
|
||||
return stix_obj
|
||||
|
||||
def all_versions(self, stix_id, _composite_filters=None):
|
||||
"""retrieve STIX object from local/remote TAXII Collection
|
||||
def all_versions(self, stix_id, _composite_filters=None, allow_custom=False):
|
||||
"""Retrieve STIX object from local/remote TAXII Collection
|
||||
endpoint, all versions of it
|
||||
|
||||
Args:
|
||||
stix_id (str): The STIX ID of the STIX objects to be retrieved.
|
||||
|
||||
composite_filters (set): set of filters passed from the parent
|
||||
CompositeDataSource, not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
(see query() as all_versions() is just a wrapper)
|
||||
|
@ -151,12 +151,18 @@ class TAXIICollectionSource(DataSource):
|
|||
Filter("match[version]", "=", "all")
|
||||
]
|
||||
|
||||
all_data = self.query(query=query, _composite_filters=_composite_filters)
|
||||
all_data = self.query(query=query, _composite_filters=_composite_filters, allow_custom=allow_custom)
|
||||
|
||||
return all_data
|
||||
# parse STIX objects from TAXII returned json
|
||||
all_data = [parse(stix_obj) for stix_obj in all_data]
|
||||
|
||||
def query(self, query=None, _composite_filters=None):
|
||||
"""search and retreive STIX objects based on the complete query
|
||||
# check - was added to handle erroneous TAXII servers
|
||||
all_data_clean = [stix_obj for stix_obj in all_data if stix_obj.id == stix_id]
|
||||
|
||||
return all_data_clean
|
||||
|
||||
def query(self, query=None, _composite_filters=None, allow_custom=False):
|
||||
"""Search and retreive STIX objects based on the complete query
|
||||
|
||||
A "complete query" includes the filters from the query, the filters
|
||||
attached to MemorySource, and any filters passed from a
|
||||
|
@ -164,9 +170,10 @@ class TAXIICollectionSource(DataSource):
|
|||
|
||||
Args:
|
||||
query (list): list of filters to search on
|
||||
|
||||
composite_filters (set): set of filters passed from the
|
||||
CompositeDataSource, not user supplied
|
||||
allow_custom (bool): whether to retrieve custom objects/properties
|
||||
or not. Default: False.
|
||||
|
||||
Returns:
|
||||
(list): list of STIX objects that matches the supplied
|
||||
|
@ -174,14 +181,13 @@ class TAXIICollectionSource(DataSource):
|
|||
parsed into python STIX objects and then returned.
|
||||
|
||||
"""
|
||||
|
||||
if query is None:
|
||||
query = set()
|
||||
else:
|
||||
if not isinstance(query, list):
|
||||
# make sure dont make set from a Filter object,
|
||||
# need to make a set from a list of Filter objects (even if just one Filter)
|
||||
query = list(query)
|
||||
query = [query]
|
||||
query = set(query)
|
||||
|
||||
# combine all query filters
|
||||
|
@ -194,7 +200,7 @@ class TAXIICollectionSource(DataSource):
|
|||
taxii_filters = self._parse_taxii_filters(query)
|
||||
|
||||
# query TAXII collection
|
||||
all_data = self.collection.get_objects(filters=taxii_filters)["objects"]
|
||||
all_data = self.collection.get_objects(filters=taxii_filters, allow_custom=allow_custom)["objects"]
|
||||
|
||||
# deduplicate data (before filtering as reduces wasted filtering)
|
||||
all_data = deduplicate(all_data)
|
||||
|
@ -203,7 +209,7 @@ class TAXIICollectionSource(DataSource):
|
|||
all_data = list(apply_common_filters(all_data, query))
|
||||
|
||||
# parse python STIX objects from the STIX object dicts
|
||||
stix_objs = [parse(stix_obj_dict) for stix_obj_dict in all_data]
|
||||
stix_objs = [parse(stix_obj_dict, allow_custom=allow_custom) for stix_obj_dict in all_data]
|
||||
|
||||
return stix_objs
|
||||
|
||||
|
@ -225,14 +231,13 @@ class TAXIICollectionSource(DataSource):
|
|||
for 'requests.get()'.
|
||||
|
||||
"""
|
||||
|
||||
params = {}
|
||||
|
||||
for filter_ in query:
|
||||
if filter_.field in TAXII_FILTERS:
|
||||
if filter_.field == "added_after":
|
||||
params[filter_.field] = filter_.value
|
||||
if filter_.property in TAXII_FILTERS:
|
||||
if filter_.property == "added_after":
|
||||
params[filter_.property] = filter_.value
|
||||
else:
|
||||
taxii_field = "match[%s]" % filter_.field
|
||||
taxii_field = "match[%s]" % filter_.property
|
||||
params[taxii_field] = filter_.value
|
||||
return params
|
||||
|
|
|
@ -1,16 +1,9 @@
|
|||
{
|
||||
"id": "bundle--2ed6ab6a-ca68-414f-8493-e4db8b75dd51",
|
||||
"objects": [
|
||||
{
|
||||
"created": "2017-05-31T21:30:41.022744Z",
|
||||
"created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
|
||||
"description": "Identify unnecessary system utilities or potentially malicious software that may be used to collect data from a network share, and audit and/or block them by using whitelisting[[CiteRef::Beechey 2010]] tools, like AppLocker,[[CiteRef::Windows Commands JPCERT]][[CiteRef::NSA MS AppLocker]] or Software Restriction Policies[[CiteRef::Corio 2008]] where appropriate.[[CiteRef::TechNet Applocker vs SRP]]",
|
||||
"id": "course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd",
|
||||
"modified": "2017-05-31T21:30:41.022744Z",
|
||||
"name": "Data from Network Shared Drive Mitigation",
|
||||
"type": "course-of-action"
|
||||
}
|
||||
],
|
||||
"spec_version": "2.0",
|
||||
"type": "bundle"
|
||||
"created": "2017-05-31T21:30:41.022744Z",
|
||||
"created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
|
||||
"description": "Identify unnecessary system utilities or potentially malicious software that may be used to collect data from a network share, and audit and/or block them by using whitelisting[[CiteRef::Beechey 2010]] tools, like AppLocker,[[CiteRef::Windows Commands JPCERT]][[CiteRef::NSA MS AppLocker]] or Software Restriction Policies[[CiteRef::Corio 2008]] where appropriate.[[CiteRef::TechNet Applocker vs SRP]]",
|
||||
"id": "course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd",
|
||||
"modified": "2017-05-31T21:30:41.022744Z",
|
||||
"name": "Data from Network Shared Drive Mitigation",
|
||||
"type": "course-of-action"
|
||||
}
|
|
@ -132,8 +132,9 @@ def test_create_bundle_invalid(indicator, malware, relationship):
|
|||
assert excinfo.value.reason == 'This property may not contain a Bundle object'
|
||||
|
||||
|
||||
def test_parse_bundle():
|
||||
bundle = stix2.parse(EXPECTED_BUNDLE)
|
||||
@pytest.mark.parametrize("version", ["2.0"])
|
||||
def test_parse_bundle(version):
|
||||
bundle = stix2.parse(EXPECTED_BUNDLE, version=version)
|
||||
|
||||
assert bundle.type == "bundle"
|
||||
assert bundle.id.startswith("bundle--")
|
||||
|
@ -158,3 +159,10 @@ def test_parse_unknown_type():
|
|||
with pytest.raises(stix2.exceptions.ParseError) as excinfo:
|
||||
stix2.parse(unknown)
|
||||
assert str(excinfo.value) == "Can't parse unknown object type 'other'! For custom types, use the CustomObject decorator."
|
||||
|
||||
|
||||
def test_stix_object_property():
|
||||
prop = stix2.core.STIXObjectProperty()
|
||||
|
||||
identity = stix2.Identity(name="test", identity_class="individual")
|
||||
assert prop.clean(identity) is identity
|
||||
|
|
|
@ -91,6 +91,7 @@ def test_custom_property_in_bundled_object():
|
|||
bundle = stix2.Bundle(identity, allow_custom=True)
|
||||
|
||||
assert bundle.objects[0].x_foo == "bar"
|
||||
assert '"x_foo": "bar"' in str(bundle)
|
||||
|
||||
|
||||
@stix2.sdo.CustomObject('x-new-type', [
|
||||
|
@ -483,3 +484,13 @@ def test_parse_observable_with_unregistered_custom_extension():
|
|||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.parse_observable(input_str)
|
||||
assert "Can't parse Unknown extension type" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_register_custom_object():
|
||||
# Not the way to register custom object.
|
||||
class CustomObject2(object):
|
||||
_type = 'awesome-object'
|
||||
|
||||
stix2._register_type(CustomObject2)
|
||||
# Note that we will always check against newest OBJ_MAP.
|
||||
assert (CustomObject2._type, CustomObject2) in stix2.OBJ_MAP.items()
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
import os
|
||||
|
||||
import pytest
|
||||
from taxii2client import Collection
|
||||
|
||||
from stix2 import (Campaign, FileSystemSink, FileSystemSource, FileSystemStore,
|
||||
Filter, MemorySource, MemoryStore)
|
||||
from stix2 import Filter, MemorySource
|
||||
from stix2.sources import (CompositeDataSource, DataSink, DataSource,
|
||||
DataStore, make_id, taxii)
|
||||
from stix2.sources.filters import apply_common_filters
|
||||
from stix2.utils import deduplicate
|
||||
|
||||
COLLECTION_URL = 'https://example.com/api1/collections/91a7b528-80eb-42ed-a74d-c6fbd5a26116/'
|
||||
FS_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "stix2_data")
|
||||
|
||||
|
||||
class MockTAXIIClient(object):
|
||||
|
@ -148,28 +144,6 @@ def test_ds_abstract_class_smoke():
|
|||
ds3.query([Filter("id", "=", "malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111")])
|
||||
|
||||
|
||||
def test_memory_store_smoke():
|
||||
# Initialize MemoryStore with dict
|
||||
ms = MemoryStore(STIX_OBJS1)
|
||||
|
||||
# Add item to sink
|
||||
ms.add(dict(id="bundle--%s" % make_id(),
|
||||
objects=STIX_OBJS2,
|
||||
spec_version="2.0",
|
||||
type="bundle"))
|
||||
|
||||
resp = ms.all_versions("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f")
|
||||
assert len(resp) == 1
|
||||
|
||||
resp = ms.get("indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f")
|
||||
assert resp["id"] == "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f"
|
||||
|
||||
query = [Filter('type', '=', 'malware')]
|
||||
|
||||
resp = ms.query(query)
|
||||
assert len(resp) == 0
|
||||
|
||||
|
||||
def test_ds_taxii(collection):
|
||||
ds = taxii.TAXIICollectionSource(collection)
|
||||
assert ds.collection is not None
|
||||
|
@ -205,7 +179,7 @@ def test_parse_taxii_filters():
|
|||
|
||||
def test_add_get_remove_filter(ds):
|
||||
|
||||
# First 3 filters are valid, remaining fields are erroneous in some way
|
||||
# First 3 filters are valid, remaining properties are erroneous in some way
|
||||
valid_filters = [
|
||||
Filter('type', '=', 'malware'),
|
||||
Filter('id', '!=', 'stix object id'),
|
||||
|
@ -219,14 +193,14 @@ def test_add_get_remove_filter(ds):
|
|||
with pytest.raises(ValueError) as excinfo:
|
||||
# create Filter that has an operator that is not allowed
|
||||
Filter('modified', '*', 'not supported operator - just place holder')
|
||||
assert str(excinfo.value) == "Filter operator '*' not supported for specified field: 'modified'"
|
||||
assert str(excinfo.value) == "Filter operator '*' not supported for specified property: 'modified'"
|
||||
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
# create Filter that has a value type that is not allowed
|
||||
Filter('created', '=', object())
|
||||
# On Python 2, the type of object() is `<type 'object'>` On Python 3, it's `<class 'object'>`.
|
||||
assert str(excinfo.value).startswith("Filter value type")
|
||||
assert str(excinfo.value).endswith("is not supported. The type must be a python immutable type or dictionary")
|
||||
assert str(excinfo.value).endswith("is not supported. The type must be a Python immutable type or dictionary")
|
||||
|
||||
assert len(ds.filters) == 0
|
||||
|
||||
|
@ -433,7 +407,7 @@ def test_filters4(ds):
|
|||
with pytest.raises(ValueError) as excinfo:
|
||||
Filter("modified", "?", "2017-01-27T13:49:53.935Z")
|
||||
assert str(excinfo.value) == ("Filter operator '?' not supported "
|
||||
"for specified field: 'modified'")
|
||||
"for specified property: 'modified'")
|
||||
|
||||
|
||||
def test_filters5(ds):
|
||||
|
@ -443,6 +417,52 @@ def test_filters5(ds):
|
|||
assert len(resp) == 1
|
||||
|
||||
|
||||
def test_filters6(ds):
|
||||
# Test filtering on non-common property
|
||||
resp = list(apply_common_filters(STIX_OBJS2, [Filter("name", "=", "Malicious site hosting downloader")]))
|
||||
assert resp[0]['id'] == STIX_OBJS2[0]['id']
|
||||
assert len(resp) == 3
|
||||
|
||||
|
||||
def test_filters7(ds):
|
||||
# Test filtering on embedded property
|
||||
stix_objects = list(STIX_OBJS2) + [{
|
||||
"type": "observed-data",
|
||||
"id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
|
||||
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
|
||||
"created": "2016-04-06T19:58:16.000Z",
|
||||
"modified": "2016-04-06T19:58:16.000Z",
|
||||
"first_observed": "2015-12-21T19:00:00Z",
|
||||
"last_observed": "2015-12-21T19:00:00Z",
|
||||
"number_observed": 50,
|
||||
"objects": {
|
||||
"0": {
|
||||
"type": "file",
|
||||
"hashes": {
|
||||
"SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f"
|
||||
},
|
||||
"extensions": {
|
||||
"pdf-ext": {
|
||||
"version": "1.7",
|
||||
"document_info_dict": {
|
||||
"Title": "Sample document",
|
||||
"Author": "Adobe Systems Incorporated",
|
||||
"Creator": "Adobe FrameMaker 5.5.3 for Power Macintosh",
|
||||
"Producer": "Acrobat Distiller 3.01 for Power Macintosh",
|
||||
"CreationDate": "20070412090123-02"
|
||||
},
|
||||
"pdfid0": "DFCE52BD827ECF765649852119D",
|
||||
"pdfid1": "57A1E0F9ED2AE523E313C"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
resp = list(apply_common_filters(stix_objects, [Filter("objects.0.extensions.pdf-ext.version", ">", "1.2")]))
|
||||
assert resp[0]['id'] == stix_objects[3]['id']
|
||||
assert len(resp) == 1
|
||||
|
||||
|
||||
def test_deduplicate(ds):
|
||||
unique = deduplicate(STIX_OBJS1)
|
||||
|
||||
|
@ -512,207 +532,3 @@ def test_composite_datasource_operations():
|
|||
# STIX_OBJS2 has indicator with later time, one with different id, one with
|
||||
# original time in STIX_OBJS1
|
||||
assert len(results) == 3
|
||||
|
||||
|
||||
def test_filesytem_source():
|
||||
# creation
|
||||
fs_source = FileSystemSource(FS_PATH)
|
||||
assert fs_source.stix_dir == FS_PATH
|
||||
|
||||
# get object
|
||||
mal = fs_source.get("malware--6b616fc1-1505-48e3-8b2c-0d19337bff38")
|
||||
assert mal.id == "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38"
|
||||
assert mal.name == "Rover"
|
||||
|
||||
# all versions - (currently not a true all versions call as FileSystem cant have multiple versions)
|
||||
id_ = fs_source.get("identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5")
|
||||
assert id_.id == "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5"
|
||||
assert id_.name == "The MITRE Corporation"
|
||||
assert id_.type == "identity"
|
||||
|
||||
# query
|
||||
intrusion_sets = fs_source.query([Filter("type", '=', "intrusion-set")])
|
||||
assert len(intrusion_sets) == 2
|
||||
assert "intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064" in [is_.id for is_ in intrusion_sets]
|
||||
assert "intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a" in [is_.id for is_ in intrusion_sets]
|
||||
|
||||
is_1 = [is_ for is_ in intrusion_sets if is_.id == "intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a"][0]
|
||||
assert "DragonOK" in is_1.aliases
|
||||
assert len(is_1.external_references) == 4
|
||||
|
||||
# query2
|
||||
is_2 = fs_source.query([Filter("external_references.external_id", '=', "T1027")])
|
||||
assert len(is_2) == 1
|
||||
|
||||
is_2 = is_2[0]
|
||||
assert is_2.id == "attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a"
|
||||
assert is_2.type == "attack-pattern"
|
||||
|
||||
|
||||
def test_filesystem_sink():
|
||||
# creation
|
||||
fs_sink = FileSystemSink(FS_PATH)
|
||||
assert fs_sink.stix_dir == FS_PATH
|
||||
|
||||
fs_source = FileSystemSource(FS_PATH)
|
||||
|
||||
# Test all the ways stix objects can be added (via different supplied forms)
|
||||
|
||||
# add python stix object
|
||||
camp1 = Campaign(name="Hannibal",
|
||||
objective="Targeting Italian and Spanish Diplomat internet accounts",
|
||||
aliases=["War Elephant"])
|
||||
|
||||
fs_sink.add(camp1)
|
||||
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", camp1.id + ".json"))
|
||||
|
||||
camp1_r = fs_source.get(camp1.id)
|
||||
assert camp1_r.id == camp1.id
|
||||
assert camp1_r.name == "Hannibal"
|
||||
assert "War Elephant" in camp1_r.aliases
|
||||
|
||||
# add stix object dict
|
||||
camp2 = {
|
||||
"name": "Aurelius",
|
||||
"type": "campaign",
|
||||
"objective": "German and French Intelligence Services",
|
||||
"aliases": ["Purple Robes"],
|
||||
"id": "campaign--111111b6-1112-4fb0-111b-b111107ca70a",
|
||||
"created": "2017-05-31T21:31:53.197755Z"
|
||||
}
|
||||
|
||||
fs_sink.add(camp2)
|
||||
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", camp2["id"] + ".json"))
|
||||
|
||||
camp2_r = fs_source.get(camp2["id"])
|
||||
assert camp2_r.id == camp2["id"]
|
||||
assert camp2_r.name == camp2["name"]
|
||||
assert "Purple Robes" in camp2_r.aliases
|
||||
|
||||
# add stix bundle dict
|
||||
bund = {
|
||||
"type": "bundle",
|
||||
"id": "bundle--112211b6-1112-4fb0-111b-b111107ca70a",
|
||||
"spec_version": "2.0",
|
||||
"objects": [
|
||||
{
|
||||
"name": "Atilla",
|
||||
"type": "campaign",
|
||||
"objective": "Bulgarian, Albanian and Romanian Intelligence Services",
|
||||
"aliases": ["Huns"],
|
||||
"id": "campaign--133111b6-1112-4fb0-111b-b111107ca70a",
|
||||
"created": "2017-05-31T21:31:53.197755Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
fs_sink.add(bund)
|
||||
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", bund["objects"][0]["id"] + ".json"))
|
||||
|
||||
camp3_r = fs_source.get(bund["objects"][0]["id"])
|
||||
assert camp3_r.id == bund["objects"][0]["id"]
|
||||
assert camp3_r.name == bund["objects"][0]["name"]
|
||||
assert "Huns" in camp3_r.aliases
|
||||
|
||||
# add json-encoded stix obj
|
||||
camp4 = '{"type": "campaign", "id":"campaign--144111b6-1112-4fb0-111b-b111107ca70a",'\
|
||||
' "created":"2017-05-31T21:31:53.197755Z", "name": "Ghengis Khan", "objective": "China and Russian infrastructure"}'
|
||||
|
||||
fs_sink.add(camp4)
|
||||
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", "campaign--144111b6-1112-4fb0-111b-b111107ca70a" + ".json"))
|
||||
|
||||
camp4_r = fs_source.get("campaign--144111b6-1112-4fb0-111b-b111107ca70a")
|
||||
assert camp4_r.id == "campaign--144111b6-1112-4fb0-111b-b111107ca70a"
|
||||
assert camp4_r.name == "Ghengis Khan"
|
||||
|
||||
# add json-encoded stix bundle
|
||||
bund2 = '{"type": "bundle", "id": "bundle--332211b6-1132-4fb0-111b-b111107ca70a",' \
|
||||
' "spec_version": "2.0", "objects": [{"type": "campaign", "id": "campaign--155155b6-1112-4fb0-111b-b111107ca70a",' \
|
||||
' "created":"2017-05-31T21:31:53.197755Z", "name": "Spartacus", "objective": "Oppressive regimes of Africa and Middle East"}]}'
|
||||
fs_sink.add(bund2)
|
||||
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", "campaign--155155b6-1112-4fb0-111b-b111107ca70a" + ".json"))
|
||||
|
||||
camp5_r = fs_source.get("campaign--155155b6-1112-4fb0-111b-b111107ca70a")
|
||||
assert camp5_r.id == "campaign--155155b6-1112-4fb0-111b-b111107ca70a"
|
||||
assert camp5_r.name == "Spartacus"
|
||||
|
||||
# add list of objects
|
||||
camp6 = Campaign(name="Comanche",
|
||||
objective="US Midwest manufacturing firms, oil refineries, and businesses",
|
||||
aliases=["Horse Warrior"])
|
||||
|
||||
camp7 = {
|
||||
"name": "Napolean",
|
||||
"type": "campaign",
|
||||
"objective": "Central and Eastern Europe military commands and departments",
|
||||
"aliases": ["The Frenchmen"],
|
||||
"id": "campaign--122818b6-1112-4fb0-111b-b111107ca70a",
|
||||
"created": "2017-05-31T21:31:53.197755Z"
|
||||
}
|
||||
|
||||
fs_sink.add([camp6, camp7])
|
||||
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", camp6.id + ".json"))
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", "campaign--122818b6-1112-4fb0-111b-b111107ca70a" + ".json"))
|
||||
|
||||
camp6_r = fs_source.get(camp6.id)
|
||||
assert camp6_r.id == camp6.id
|
||||
assert "Horse Warrior" in camp6_r.aliases
|
||||
|
||||
camp7_r = fs_source.get(camp7["id"])
|
||||
assert camp7_r.id == camp7["id"]
|
||||
assert "The Frenchmen" in camp7_r.aliases
|
||||
|
||||
# remove all added objects
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp1_r.id + ".json"))
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp2_r.id + ".json"))
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp3_r.id + ".json"))
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp4_r.id + ".json"))
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp5_r.id + ".json"))
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp6_r.id + ".json"))
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp7_r.id + ".json"))
|
||||
|
||||
# remove campaign dir (that was added in course of testing)
|
||||
os.rmdir(os.path.join(FS_PATH, "campaign"))
|
||||
|
||||
|
||||
def test_filesystem_store():
|
||||
# creation
|
||||
fs_store = FileSystemStore(FS_PATH)
|
||||
|
||||
# get()
|
||||
coa = fs_store.get("course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd")
|
||||
assert coa.id == "course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd"
|
||||
assert coa.type == "course-of-action"
|
||||
|
||||
# all versions() - (note at this time, all_versions() is still not applicable to FileSystem, as only one version is ever stored)
|
||||
rel = fs_store.all_versions("relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1")[0]
|
||||
assert rel.id == "relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1"
|
||||
assert rel.type == "relationship"
|
||||
|
||||
# query()
|
||||
tools = fs_store.query([Filter("labels", "in", "tool")])
|
||||
assert len(tools) == 2
|
||||
assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [tool.id for tool in tools]
|
||||
assert "tool--03342581-f790-4f03-ba41-e82e67392e23" in [tool.id for tool in tools]
|
||||
|
||||
# add()
|
||||
camp1 = Campaign(name="Great Heathen Army",
|
||||
objective="Targeting the government of United Kingdom and insitutions affiliated with the Church Of England",
|
||||
aliases=["Ragnar"])
|
||||
fs_store.add(camp1)
|
||||
|
||||
camp1_r = fs_store.get(camp1.id)
|
||||
assert camp1_r.id == camp1.id
|
||||
assert camp1_r.name == camp1.name
|
||||
|
||||
# remove
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp1_r.id + ".json"))
|
||||
|
||||
# remove campaign dir
|
||||
os.rmdir(os.path.join(FS_PATH, "campaign"))
|
||||
|
|
|
@ -184,3 +184,35 @@ def test_parse_malware():
|
|||
assert mal.modified == FAKE_TIME
|
||||
assert mal.labels == ['ransomware']
|
||||
assert mal.name == "Cryptolocker"
|
||||
|
||||
|
||||
def test_created_by():
|
||||
identity = stix2.Identity(**IDENTITY_KWARGS)
|
||||
factory = stix2.ObjectFactory(created_by_ref=identity.id)
|
||||
env = stix2.Environment(store=stix2.MemoryStore(), factory=factory)
|
||||
env.add(identity)
|
||||
|
||||
ind = env.create(stix2.Indicator, **INDICATOR_KWARGS)
|
||||
creator = env.creator_of(ind)
|
||||
assert creator is identity
|
||||
|
||||
|
||||
def test_created_by_no_datasource():
|
||||
identity = stix2.Identity(**IDENTITY_KWARGS)
|
||||
factory = stix2.ObjectFactory(created_by_ref=identity.id)
|
||||
env = stix2.Environment(factory=factory)
|
||||
|
||||
ind = env.create(stix2.Indicator, **INDICATOR_KWARGS)
|
||||
with pytest.raises(AttributeError) as excinfo:
|
||||
env.creator_of(ind)
|
||||
assert 'Environment has no data source' in str(excinfo.value)
|
||||
|
||||
|
||||
def test_created_by_not_found():
|
||||
identity = stix2.Identity(**IDENTITY_KWARGS)
|
||||
factory = stix2.ObjectFactory(created_by_ref=identity.id)
|
||||
env = stix2.Environment(store=stix2.MemoryStore(), factory=factory)
|
||||
|
||||
ind = env.create(stix2.Indicator, **INDICATOR_KWARGS)
|
||||
creator = env.creator_of(ind)
|
||||
assert creator is None
|
||||
|
|
|
@ -0,0 +1,377 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
|
||||
from stix2 import (Bundle, Campaign, CustomObject, FileSystemSink,
|
||||
FileSystemSource, FileSystemStore, Filter, properties)
|
||||
|
||||
FS_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "stix2_data")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fs_store():
|
||||
# create
|
||||
yield FileSystemStore(FS_PATH)
|
||||
|
||||
# remove campaign dir
|
||||
shutil.rmtree(os.path.join(FS_PATH, "campaign"), True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fs_source():
|
||||
# create
|
||||
fs = FileSystemSource(FS_PATH)
|
||||
assert fs.stix_dir == FS_PATH
|
||||
yield fs
|
||||
|
||||
# remove campaign dir
|
||||
shutil.rmtree(os.path.join(FS_PATH, "campaign"), True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fs_sink():
|
||||
# create
|
||||
fs = FileSystemSink(FS_PATH)
|
||||
assert fs.stix_dir == FS_PATH
|
||||
yield fs
|
||||
|
||||
# remove campaign dir
|
||||
shutil.rmtree(os.path.join(FS_PATH, "campaign"), True)
|
||||
|
||||
|
||||
def test_filesystem_source_nonexistent_folder():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
FileSystemSource('nonexistent-folder')
|
||||
assert "for STIX data does not exist" in str(excinfo)
|
||||
|
||||
|
||||
def test_filesystem_sink_nonexistent_folder():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
FileSystemSink('nonexistent-folder')
|
||||
assert "for STIX data does not exist" in str(excinfo)
|
||||
|
||||
|
||||
def test_filesytem_source_get_object(fs_source):
|
||||
# get object
|
||||
mal = fs_source.get("malware--6b616fc1-1505-48e3-8b2c-0d19337bff38")
|
||||
assert mal.id == "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38"
|
||||
assert mal.name == "Rover"
|
||||
|
||||
|
||||
def test_filesytem_source_get_nonexistent_object(fs_source):
|
||||
ind = fs_source.get("indicator--6b616fc1-1505-48e3-8b2c-0d19337bff38")
|
||||
assert ind is None
|
||||
|
||||
|
||||
def test_filesytem_source_all_versions(fs_source):
|
||||
# all versions - (currently not a true all versions call as FileSystem cant have multiple versions)
|
||||
id_ = fs_source.get("identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5")
|
||||
assert id_.id == "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5"
|
||||
assert id_.name == "The MITRE Corporation"
|
||||
assert id_.type == "identity"
|
||||
|
||||
|
||||
def test_filesytem_source_query_single(fs_source):
|
||||
# query2
|
||||
is_2 = fs_source.query([Filter("external_references.external_id", '=', "T1027")])
|
||||
assert len(is_2) == 1
|
||||
|
||||
is_2 = is_2[0]
|
||||
assert is_2.id == "attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a"
|
||||
assert is_2.type == "attack-pattern"
|
||||
|
||||
|
||||
def test_filesytem_source_query_multiple(fs_source):
|
||||
# query
|
||||
intrusion_sets = fs_source.query([Filter("type", '=', "intrusion-set")])
|
||||
assert len(intrusion_sets) == 2
|
||||
assert "intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064" in [is_.id for is_ in intrusion_sets]
|
||||
assert "intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a" in [is_.id for is_ in intrusion_sets]
|
||||
|
||||
is_1 = [is_ for is_ in intrusion_sets if is_.id == "intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a"][0]
|
||||
assert "DragonOK" in is_1.aliases
|
||||
assert len(is_1.external_references) == 4
|
||||
|
||||
|
||||
def test_filesystem_sink_add_python_stix_object(fs_sink, fs_source):
|
||||
# add python stix object
|
||||
camp1 = Campaign(name="Hannibal",
|
||||
objective="Targeting Italian and Spanish Diplomat internet accounts",
|
||||
aliases=["War Elephant"])
|
||||
|
||||
fs_sink.add(camp1)
|
||||
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", camp1.id + ".json"))
|
||||
|
||||
camp1_r = fs_source.get(camp1.id)
|
||||
assert camp1_r.id == camp1.id
|
||||
assert camp1_r.name == "Hannibal"
|
||||
assert "War Elephant" in camp1_r.aliases
|
||||
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp1_r.id + ".json"))
|
||||
|
||||
|
||||
def test_filesystem_sink_add_stix_object_dict(fs_sink, fs_source):
|
||||
# add stix object dict
|
||||
camp2 = {
|
||||
"name": "Aurelius",
|
||||
"type": "campaign",
|
||||
"objective": "German and French Intelligence Services",
|
||||
"aliases": ["Purple Robes"],
|
||||
"id": "campaign--111111b6-1112-4fb0-111b-b111107ca70a",
|
||||
"created": "2017-05-31T21:31:53.197755Z"
|
||||
}
|
||||
|
||||
fs_sink.add(camp2)
|
||||
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", camp2["id"] + ".json"))
|
||||
|
||||
camp2_r = fs_source.get(camp2["id"])
|
||||
assert camp2_r.id == camp2["id"]
|
||||
assert camp2_r.name == camp2["name"]
|
||||
assert "Purple Robes" in camp2_r.aliases
|
||||
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp2_r.id + ".json"))
|
||||
|
||||
|
||||
def test_filesystem_sink_add_stix_bundle_dict(fs_sink, fs_source):
|
||||
# add stix bundle dict
|
||||
bund = {
|
||||
"type": "bundle",
|
||||
"id": "bundle--112211b6-1112-4fb0-111b-b111107ca70a",
|
||||
"spec_version": "2.0",
|
||||
"objects": [
|
||||
{
|
||||
"name": "Atilla",
|
||||
"type": "campaign",
|
||||
"objective": "Bulgarian, Albanian and Romanian Intelligence Services",
|
||||
"aliases": ["Huns"],
|
||||
"id": "campaign--133111b6-1112-4fb0-111b-b111107ca70a",
|
||||
"created": "2017-05-31T21:31:53.197755Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
fs_sink.add(bund)
|
||||
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", bund["objects"][0]["id"] + ".json"))
|
||||
|
||||
camp3_r = fs_source.get(bund["objects"][0]["id"])
|
||||
assert camp3_r.id == bund["objects"][0]["id"]
|
||||
assert camp3_r.name == bund["objects"][0]["name"]
|
||||
assert "Huns" in camp3_r.aliases
|
||||
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp3_r.id + ".json"))
|
||||
|
||||
|
||||
def test_filesystem_sink_add_json_stix_object(fs_sink, fs_source):
|
||||
# add json-encoded stix obj
|
||||
camp4 = '{"type": "campaign", "id":"campaign--144111b6-1112-4fb0-111b-b111107ca70a",'\
|
||||
' "created":"2017-05-31T21:31:53.197755Z", "name": "Ghengis Khan", "objective": "China and Russian infrastructure"}'
|
||||
|
||||
fs_sink.add(camp4)
|
||||
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", "campaign--144111b6-1112-4fb0-111b-b111107ca70a" + ".json"))
|
||||
|
||||
camp4_r = fs_source.get("campaign--144111b6-1112-4fb0-111b-b111107ca70a")
|
||||
assert camp4_r.id == "campaign--144111b6-1112-4fb0-111b-b111107ca70a"
|
||||
assert camp4_r.name == "Ghengis Khan"
|
||||
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp4_r.id + ".json"))
|
||||
|
||||
|
||||
def test_filesystem_sink_json_stix_bundle(fs_sink, fs_source):
|
||||
# add json-encoded stix bundle
|
||||
bund2 = '{"type": "bundle", "id": "bundle--332211b6-1132-4fb0-111b-b111107ca70a",' \
|
||||
' "spec_version": "2.0", "objects": [{"type": "campaign", "id": "campaign--155155b6-1112-4fb0-111b-b111107ca70a",' \
|
||||
' "created":"2017-05-31T21:31:53.197755Z", "name": "Spartacus", "objective": "Oppressive regimes of Africa and Middle East"}]}'
|
||||
fs_sink.add(bund2)
|
||||
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", "campaign--155155b6-1112-4fb0-111b-b111107ca70a" + ".json"))
|
||||
|
||||
camp5_r = fs_source.get("campaign--155155b6-1112-4fb0-111b-b111107ca70a")
|
||||
assert camp5_r.id == "campaign--155155b6-1112-4fb0-111b-b111107ca70a"
|
||||
assert camp5_r.name == "Spartacus"
|
||||
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp5_r.id + ".json"))
|
||||
|
||||
|
||||
def test_filesystem_sink_add_objects_list(fs_sink, fs_source):
|
||||
# add list of objects
|
||||
camp6 = Campaign(name="Comanche",
|
||||
objective="US Midwest manufacturing firms, oil refineries, and businesses",
|
||||
aliases=["Horse Warrior"])
|
||||
|
||||
camp7 = {
|
||||
"name": "Napolean",
|
||||
"type": "campaign",
|
||||
"objective": "Central and Eastern Europe military commands and departments",
|
||||
"aliases": ["The Frenchmen"],
|
||||
"id": "campaign--122818b6-1112-4fb0-111b-b111107ca70a",
|
||||
"created": "2017-05-31T21:31:53.197755Z"
|
||||
}
|
||||
|
||||
fs_sink.add([camp6, camp7])
|
||||
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", camp6.id + ".json"))
|
||||
assert os.path.exists(os.path.join(FS_PATH, "campaign", "campaign--122818b6-1112-4fb0-111b-b111107ca70a" + ".json"))
|
||||
|
||||
camp6_r = fs_source.get(camp6.id)
|
||||
assert camp6_r.id == camp6.id
|
||||
assert "Horse Warrior" in camp6_r.aliases
|
||||
|
||||
camp7_r = fs_source.get(camp7["id"])
|
||||
assert camp7_r.id == camp7["id"]
|
||||
assert "The Frenchmen" in camp7_r.aliases
|
||||
|
||||
# remove all added objects
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp6_r.id + ".json"))
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp7_r.id + ".json"))
|
||||
|
||||
|
||||
def test_filesystem_store_get_stored_as_bundle(fs_store):
|
||||
coa = fs_store.get("course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f")
|
||||
assert coa.id == "course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f"
|
||||
assert coa.type == "course-of-action"
|
||||
|
||||
|
||||
def test_filesystem_store_get_stored_as_object(fs_store):
|
||||
coa = fs_store.get("course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd")
|
||||
assert coa.id == "course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd"
|
||||
assert coa.type == "course-of-action"
|
||||
|
||||
|
||||
def test_filesystem_store_all_versions(fs_store):
|
||||
# all versions() - (note at this time, all_versions() is still not applicable to FileSystem, as only one version is ever stored)
|
||||
rel = fs_store.all_versions("relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1")[0]
|
||||
assert rel.id == "relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1"
|
||||
assert rel.type == "relationship"
|
||||
|
||||
|
||||
def test_filesystem_store_query(fs_store):
|
||||
# query()
|
||||
tools = fs_store.query([Filter("labels", "in", "tool")])
|
||||
assert len(tools) == 2
|
||||
assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [tool.id for tool in tools]
|
||||
assert "tool--03342581-f790-4f03-ba41-e82e67392e23" in [tool.id for tool in tools]
|
||||
|
||||
|
||||
def test_filesystem_store_query_single_filter(fs_store):
|
||||
query = Filter("labels", "in", "tool")
|
||||
tools = fs_store.query(query)
|
||||
assert len(tools) == 2
|
||||
assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [tool.id for tool in tools]
|
||||
assert "tool--03342581-f790-4f03-ba41-e82e67392e23" in [tool.id for tool in tools]
|
||||
|
||||
|
||||
def test_filesystem_store_empty_query(fs_store):
|
||||
results = fs_store.query() # returns all
|
||||
assert len(results) == 26
|
||||
assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [obj.id for obj in results]
|
||||
assert "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" in [obj.id for obj in results]
|
||||
|
||||
|
||||
def test_filesystem_store_query_multiple_filters(fs_store):
|
||||
fs_store.source.filters.add(Filter("labels", "in", "tool"))
|
||||
tools = fs_store.query(Filter("id", "=", "tool--242f3da3-4425-4d11-8f5c-b842886da966"))
|
||||
assert len(tools) == 1
|
||||
assert tools[0].id == "tool--242f3da3-4425-4d11-8f5c-b842886da966"
|
||||
|
||||
|
||||
def test_filesystem_store_query_dont_include_type_folder(fs_store):
|
||||
results = fs_store.query(Filter("type", "!=", "tool"))
|
||||
assert len(results) == 24
|
||||
|
||||
|
||||
def test_filesystem_store_add(fs_store):
|
||||
# add()
|
||||
camp1 = Campaign(name="Great Heathen Army",
|
||||
objective="Targeting the government of United Kingdom and insitutions affiliated with the Church Of England",
|
||||
aliases=["Ragnar"])
|
||||
fs_store.add(camp1)
|
||||
|
||||
camp1_r = fs_store.get(camp1.id)
|
||||
assert camp1_r.id == camp1.id
|
||||
assert camp1_r.name == camp1.name
|
||||
|
||||
# remove
|
||||
os.remove(os.path.join(FS_PATH, "campaign", camp1_r.id + ".json"))
|
||||
|
||||
|
||||
def test_filesystem_store_add_as_bundle():
|
||||
fs_store = FileSystemStore(FS_PATH, bundlify=True)
|
||||
|
||||
camp1 = Campaign(name="Great Heathen Army",
|
||||
objective="Targeting the government of United Kingdom and insitutions affiliated with the Church Of England",
|
||||
aliases=["Ragnar"])
|
||||
fs_store.add(camp1)
|
||||
|
||||
with open(os.path.join(FS_PATH, "campaign", camp1.id + ".json")) as bundle_file:
|
||||
assert '"type": "bundle"' in bundle_file.read()
|
||||
|
||||
camp1_r = fs_store.get(camp1.id)
|
||||
assert camp1_r.id == camp1.id
|
||||
assert camp1_r.name == camp1.name
|
||||
|
||||
shutil.rmtree(os.path.join(FS_PATH, "campaign"), True)
|
||||
|
||||
|
||||
def test_filesystem_add_bundle_object(fs_store):
|
||||
bundle = Bundle()
|
||||
fs_store.add(bundle)
|
||||
|
||||
|
||||
def test_filesystem_store_add_invalid_object(fs_store):
|
||||
ind = ('campaign', 'campaign--111111b6-1112-4fb0-111b-b111107ca70a') # tuple isn't valid
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
fs_store.add(ind)
|
||||
assert 'stix_data must be' in str(excinfo.value)
|
||||
assert 'a STIX object' in str(excinfo.value)
|
||||
assert 'JSON formatted STIX' in str(excinfo.value)
|
||||
assert 'JSON formatted STIX bundle' in str(excinfo.value)
|
||||
|
||||
|
||||
def test_filesystem_object_with_custom_property(fs_store):
|
||||
camp = Campaign(name="Scipio Africanus",
|
||||
objective="Defeat the Carthaginians",
|
||||
x_empire="Roman",
|
||||
allow_custom=True)
|
||||
|
||||
fs_store.add(camp, True)
|
||||
|
||||
camp_r = fs_store.get(camp.id, True)
|
||||
assert camp_r.id == camp.id
|
||||
assert camp_r.x_empire == camp.x_empire
|
||||
|
||||
|
||||
def test_filesystem_object_with_custom_property_in_bundle(fs_store):
|
||||
camp = Campaign(name="Scipio Africanus",
|
||||
objective="Defeat the Carthaginians",
|
||||
x_empire="Roman",
|
||||
allow_custom=True)
|
||||
|
||||
bundle = Bundle(camp, allow_custom=True)
|
||||
fs_store.add(bundle, True)
|
||||
|
||||
camp_r = fs_store.get(camp.id, True)
|
||||
assert camp_r.id == camp.id
|
||||
assert camp_r.x_empire == camp.x_empire
|
||||
|
||||
|
||||
def test_filesystem_custom_object(fs_store):
|
||||
@CustomObject('x-new-obj', [
|
||||
('property1', properties.StringProperty(required=True)),
|
||||
])
|
||||
class NewObj():
|
||||
pass
|
||||
|
||||
newobj = NewObj(property1='something')
|
||||
fs_store.add(newobj, True)
|
||||
|
||||
newobj_r = fs_store.get(newobj.id, True)
|
||||
assert newobj_r.id == newobj.id
|
||||
assert newobj_r.property1 == 'something'
|
||||
|
||||
# remove dir
|
||||
shutil.rmtree(os.path.join(FS_PATH, "x-new-obj"), True)
|
|
@ -0,0 +1,270 @@
|
|||
import pytest
|
||||
|
||||
from stix2 import (Bundle, Campaign, CustomObject, Filter, MemorySource,
|
||||
MemoryStore, properties)
|
||||
from stix2.sources import make_id
|
||||
|
||||
IND1 = {
|
||||
"created": "2017-01-27T13:49:53.935Z",
|
||||
"id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f",
|
||||
"labels": [
|
||||
"url-watchlist"
|
||||
],
|
||||
"modified": "2017-01-27T13:49:53.935Z",
|
||||
"name": "Malicious site hosting downloader",
|
||||
"pattern": "[url:value = 'http://x4z9arb.cn/4712']",
|
||||
"type": "indicator",
|
||||
"valid_from": "2017-01-27T13:49:53.935382Z"
|
||||
}
|
||||
IND2 = {
|
||||
"created": "2017-01-27T13:49:53.935Z",
|
||||
"id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f",
|
||||
"labels": [
|
||||
"url-watchlist"
|
||||
],
|
||||
"modified": "2017-01-27T13:49:53.935Z",
|
||||
"name": "Malicious site hosting downloader",
|
||||
"pattern": "[url:value = 'http://x4z9arb.cn/4712']",
|
||||
"type": "indicator",
|
||||
"valid_from": "2017-01-27T13:49:53.935382Z"
|
||||
}
|
||||
IND3 = {
|
||||
"created": "2017-01-27T13:49:53.935Z",
|
||||
"id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f",
|
||||
"labels": [
|
||||
"url-watchlist"
|
||||
],
|
||||
"modified": "2017-01-27T13:49:53.936Z",
|
||||
"name": "Malicious site hosting downloader",
|
||||
"pattern": "[url:value = 'http://x4z9arb.cn/4712']",
|
||||
"type": "indicator",
|
||||
"valid_from": "2017-01-27T13:49:53.935382Z"
|
||||
}
|
||||
IND4 = {
|
||||
"created": "2017-01-27T13:49:53.935Z",
|
||||
"id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f",
|
||||
"labels": [
|
||||
"url-watchlist"
|
||||
],
|
||||
"modified": "2017-01-27T13:49:53.935Z",
|
||||
"name": "Malicious site hosting downloader",
|
||||
"pattern": "[url:value = 'http://x4z9arb.cn/4712']",
|
||||
"type": "indicator",
|
||||
"valid_from": "2017-01-27T13:49:53.935382Z"
|
||||
}
|
||||
IND5 = {
|
||||
"created": "2017-01-27T13:49:53.935Z",
|
||||
"id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f",
|
||||
"labels": [
|
||||
"url-watchlist"
|
||||
],
|
||||
"modified": "2017-01-27T13:49:53.935Z",
|
||||
"name": "Malicious site hosting downloader",
|
||||
"pattern": "[url:value = 'http://x4z9arb.cn/4712']",
|
||||
"type": "indicator",
|
||||
"valid_from": "2017-01-27T13:49:53.935382Z"
|
||||
}
|
||||
IND6 = {
|
||||
"created": "2017-01-27T13:49:53.935Z",
|
||||
"id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f",
|
||||
"labels": [
|
||||
"url-watchlist"
|
||||
],
|
||||
"modified": "2017-01-31T13:49:53.935Z",
|
||||
"name": "Malicious site hosting downloader",
|
||||
"pattern": "[url:value = 'http://x4z9arb.cn/4712']",
|
||||
"type": "indicator",
|
||||
"valid_from": "2017-01-27T13:49:53.935382Z"
|
||||
}
|
||||
IND7 = {
|
||||
"created": "2017-01-27T13:49:53.935Z",
|
||||
"id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f",
|
||||
"labels": [
|
||||
"url-watchlist"
|
||||
],
|
||||
"modified": "2017-01-27T13:49:53.935Z",
|
||||
"name": "Malicious site hosting downloader",
|
||||
"pattern": "[url:value = 'http://x4z9arb.cn/4712']",
|
||||
"type": "indicator",
|
||||
"valid_from": "2017-01-27T13:49:53.935382Z"
|
||||
}
|
||||
IND8 = {
|
||||
"created": "2017-01-27T13:49:53.935Z",
|
||||
"id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f",
|
||||
"labels": [
|
||||
"url-watchlist"
|
||||
],
|
||||
"modified": "2017-01-27T13:49:53.935Z",
|
||||
"name": "Malicious site hosting downloader",
|
||||
"pattern": "[url:value = 'http://x4z9arb.cn/4712']",
|
||||
"type": "indicator",
|
||||
"valid_from": "2017-01-27T13:49:53.935382Z"
|
||||
}
|
||||
|
||||
STIX_OBJS2 = [IND6, IND7, IND8]
|
||||
STIX_OBJS1 = [IND1, IND2, IND3, IND4, IND5]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mem_store():
|
||||
yield MemoryStore(STIX_OBJS1)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mem_source():
|
||||
yield MemorySource(STIX_OBJS1)
|
||||
|
||||
|
||||
def test_memory_source_get(mem_source):
|
||||
resp = mem_source.get("indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f")
|
||||
assert resp["id"] == "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f"
|
||||
|
||||
|
||||
def test_memory_source_get_nonexistant_object(mem_source):
|
||||
resp = mem_source.get("tool--d81f86b8-975b-bc0b-775e-810c5ad45a4f")
|
||||
assert resp is None
|
||||
|
||||
|
||||
def test_memory_store_all_versions(mem_store):
|
||||
# Add bundle of items to sink
|
||||
mem_store.add(dict(id="bundle--%s" % make_id(),
|
||||
objects=STIX_OBJS2,
|
||||
spec_version="2.0",
|
||||
type="bundle"))
|
||||
|
||||
resp = mem_store.all_versions("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f")
|
||||
assert len(resp) == 1 # MemoryStore can only store 1 version of each object
|
||||
|
||||
|
||||
def test_memory_store_query(mem_store):
|
||||
query = [Filter('type', '=', 'malware')]
|
||||
resp = mem_store.query(query)
|
||||
assert len(resp) == 0
|
||||
|
||||
|
||||
def test_memory_store_query_single_filter(mem_store):
|
||||
query = Filter('id', '=', 'indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f')
|
||||
resp = mem_store.query(query)
|
||||
assert len(resp) == 1
|
||||
|
||||
|
||||
def test_memory_store_query_empty_query(mem_store):
|
||||
resp = mem_store.query()
|
||||
# sort since returned in random order
|
||||
resp = sorted(resp, key=lambda k: k['id'])
|
||||
assert len(resp) == 2
|
||||
assert resp[0]['id'] == 'indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f'
|
||||
assert resp[0]['modified'] == '2017-01-27T13:49:53.935Z'
|
||||
assert resp[1]['id'] == 'indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f'
|
||||
assert resp[1]['modified'] == '2017-01-27T13:49:53.936Z'
|
||||
|
||||
|
||||
def test_memory_store_query_multiple_filters(mem_store):
|
||||
mem_store.source.filters.add(Filter('type', '=', 'indicator'))
|
||||
query = Filter('id', '=', 'indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f')
|
||||
resp = mem_store.query(query)
|
||||
assert len(resp) == 1
|
||||
|
||||
|
||||
def test_memory_store_add_stix_object_str(mem_store):
|
||||
# add stix object string
|
||||
camp_id = "campaign--111111b6-1112-4fb0-111b-b111107ca70a"
|
||||
camp_name = "Aurelius"
|
||||
camp_alias = "Purple Robes"
|
||||
camp = """{
|
||||
"name": "%s",
|
||||
"type": "campaign",
|
||||
"objective": "German and French Intelligence Services",
|
||||
"aliases": ["%s"],
|
||||
"id": "%s",
|
||||
"created": "2017-05-31T21:31:53.197755Z"
|
||||
}""" % (camp_name, camp_alias, camp_id)
|
||||
|
||||
mem_store.add(camp)
|
||||
|
||||
camp_r = mem_store.get(camp_id)
|
||||
assert camp_r["id"] == camp_id
|
||||
assert camp_r["name"] == camp_name
|
||||
assert camp_alias in camp_r["aliases"]
|
||||
|
||||
|
||||
def test_memory_store_add_stix_bundle_str(mem_store):
|
||||
# add stix bundle string
|
||||
camp_id = "campaign--133111b6-1112-4fb0-111b-b111107ca70a"
|
||||
camp_name = "Atilla"
|
||||
camp_alias = "Huns"
|
||||
bund = """{
|
||||
"type": "bundle",
|
||||
"id": "bundle--112211b6-1112-4fb0-111b-b111107ca70a",
|
||||
"spec_version": "2.0",
|
||||
"objects": [
|
||||
{
|
||||
"name": "%s",
|
||||
"type": "campaign",
|
||||
"objective": "Bulgarian, Albanian and Romanian Intelligence Services",
|
||||
"aliases": ["%s"],
|
||||
"id": "%s",
|
||||
"created": "2017-05-31T21:31:53.197755Z"
|
||||
}
|
||||
]
|
||||
}""" % (camp_name, camp_alias, camp_id)
|
||||
|
||||
mem_store.add(bund)
|
||||
|
||||
camp_r = mem_store.get(camp_id)
|
||||
assert camp_r["id"] == camp_id
|
||||
assert camp_r["name"] == camp_name
|
||||
assert camp_alias in camp_r["aliases"]
|
||||
|
||||
|
||||
def test_memory_store_add_invalid_object(mem_store):
|
||||
ind = ('indicator', IND1) # tuple isn't valid
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
mem_store.add(ind)
|
||||
assert 'stix_data must be' in str(excinfo.value)
|
||||
assert 'a STIX object' in str(excinfo.value)
|
||||
assert 'JSON formatted STIX' in str(excinfo.value)
|
||||
assert 'JSON formatted STIX bundle' in str(excinfo.value)
|
||||
|
||||
|
||||
def test_memory_store_object_with_custom_property(mem_store):
|
||||
camp = Campaign(name="Scipio Africanus",
|
||||
objective="Defeat the Carthaginians",
|
||||
x_empire="Roman",
|
||||
allow_custom=True)
|
||||
|
||||
mem_store.add(camp, True)
|
||||
|
||||
camp_r = mem_store.get(camp.id, True)
|
||||
assert camp_r.id == camp.id
|
||||
assert camp_r.x_empire == camp.x_empire
|
||||
|
||||
|
||||
def test_memory_store_object_with_custom_property_in_bundle(mem_store):
|
||||
camp = Campaign(name="Scipio Africanus",
|
||||
objective="Defeat the Carthaginians",
|
||||
x_empire="Roman",
|
||||
allow_custom=True)
|
||||
|
||||
bundle = Bundle(camp, allow_custom=True)
|
||||
mem_store.add(bundle, True)
|
||||
|
||||
bundle_r = mem_store.get(bundle.id, True)
|
||||
camp_r = bundle_r['objects'][0]
|
||||
assert camp_r.id == camp.id
|
||||
assert camp_r.x_empire == camp.x_empire
|
||||
|
||||
|
||||
def test_memory_store_custom_object(mem_store):
|
||||
@CustomObject('x-new-obj', [
|
||||
('property1', properties.StringProperty(required=True)),
|
||||
])
|
||||
class NewObj():
|
||||
pass
|
||||
|
||||
newobj = NewObj(property1='something')
|
||||
mem_store.add(newobj, True)
|
||||
|
||||
newobj_r = mem_store.get(newobj.id, True)
|
||||
assert newobj_r.id == newobj.id
|
||||
assert newobj_r.property1 == 'something'
|
|
@ -1,8 +1,7 @@
|
|||
import pytest
|
||||
|
||||
from stix2 import TCPExt
|
||||
from stix2 import EmailMIMEComponent, ExtensionsProperty, TCPExt
|
||||
from stix2.exceptions import AtLeastOnePropertyError, DictionaryKeyError
|
||||
from stix2.observables import EmailMIMEComponent, ExtensionsProperty
|
||||
from stix2.properties import (BinaryProperty, BooleanProperty,
|
||||
DictionaryProperty, EmbeddedObjectProperty,
|
||||
EnumProperty, FloatProperty, HashesProperty,
|
||||
|
|
|
@ -34,7 +34,7 @@ class STIXdatetime(dt.datetime):
|
|||
|
||||
|
||||
def deduplicate(stix_obj_list):
|
||||
"""Deduplicate a list of STIX objects to a unique set
|
||||
"""Deduplicate a list of STIX objects to a unique set.
|
||||
|
||||
Reduces a set of STIX objects to unique set by looking
|
||||
at 'id' and 'modified' fields - as a unique object version
|
||||
|
@ -44,7 +44,6 @@ def deduplicate(stix_obj_list):
|
|||
of deduplicate(),that if the "stix_obj_list" argument has
|
||||
multiple STIX objects of the same version, the last object
|
||||
version found in the list will be the one that is returned.
|
||||
()
|
||||
|
||||
Args:
|
||||
stix_obj_list (list): list of STIX objects (dicts)
|
||||
|
@ -56,7 +55,11 @@ def deduplicate(stix_obj_list):
|
|||
unique_objs = {}
|
||||
|
||||
for obj in stix_obj_list:
|
||||
unique_objs[(obj['id'], obj['modified'])] = obj
|
||||
try:
|
||||
unique_objs[(obj['id'], obj['modified'])] = obj
|
||||
except KeyError:
|
||||
# Handle objects with no `modified` property, e.g. marking-definition
|
||||
unique_objs[(obj['id'], obj['created'])] = obj
|
||||
|
||||
return list(unique_objs.values())
|
||||
|
||||
|
@ -248,3 +251,11 @@ def revoke(data):
|
|||
if data.get("revoked"):
|
||||
raise RevokeError("revoke")
|
||||
return new_version(data, revoked=True)
|
||||
|
||||
|
||||
def get_class_hierarchy_names(obj):
|
||||
"""Given an object, return the names of the class hierarchy."""
|
||||
names = []
|
||||
for cls in obj.__class__.__mro__:
|
||||
names.append(cls.__name__)
|
||||
return names
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
|
||||
# flake8: noqa
|
||||
|
||||
from ..core import Bundle
|
||||
from .common import (TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, CustomMarking,
|
||||
ExternalReference, GranularMarking, KillChainPhase,
|
||||
MarkingDefinition, StatementMarking, TLPMarking)
|
||||
from .observables import (URL, AlternateDataStream, ArchiveExt, Artifact,
|
||||
AutonomousSystem, CustomExtension, CustomObservable,
|
||||
Directory, DomainName, EmailAddress, EmailMessage,
|
||||
EmailMIMEComponent, ExtensionsProperty, File,
|
||||
HTTPRequestExt, ICMPExt, IPv4Address, IPv6Address,
|
||||
MACAddress, Mutex, NetworkTraffic, NTFSExt, PDFExt,
|
||||
Process, RasterImageExt, SocketExt, Software, TCPExt,
|
||||
UNIXAccountExt, UserAccount, WindowsPEBinaryExt,
|
||||
WindowsPEOptionalHeaderType, WindowsPESection,
|
||||
WindowsProcessExt, WindowsRegistryKey,
|
||||
WindowsRegistryValueType, WindowsServiceExt,
|
||||
X509Certificate, X509V3ExtenstionsType,
|
||||
parse_observable)
|
||||
from .sdo import (AttackPattern, Campaign, CourseOfAction, CustomObject,
|
||||
Identity, Indicator, IntrusionSet, Malware, ObservedData,
|
||||
Report, ThreatActor, Tool, Vulnerability)
|
||||
from .sro import Relationship, Sighting
|
||||
|
||||
OBJ_MAP = {
|
||||
'attack-pattern': AttackPattern,
|
||||
'bundle': Bundle,
|
||||
'campaign': Campaign,
|
||||
'course-of-action': CourseOfAction,
|
||||
'identity': Identity,
|
||||
'indicator': Indicator,
|
||||
'intrusion-set': IntrusionSet,
|
||||
'malware': Malware,
|
||||
'marking-definition': MarkingDefinition,
|
||||
'observed-data': ObservedData,
|
||||
'report': Report,
|
||||
'relationship': Relationship,
|
||||
'threat-actor': ThreatActor,
|
||||
'tool': Tool,
|
||||
'sighting': Sighting,
|
||||
'vulnerability': Vulnerability,
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
"""STIX 2 Common Data Types and Properties."""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from ..base import _STIXBase
|
||||
from ..markings import _MarkingsMixin
|
||||
from ..properties import (HashesProperty, IDProperty, ListProperty, Property,
|
||||
ReferenceProperty, SelectorProperty, StringProperty,
|
||||
TimestampProperty, TypeProperty)
|
||||
from ..utils import NOW, get_dict
|
||||
|
||||
|
||||
class ExternalReference(_STIXBase):
|
||||
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('source_name', StringProperty(required=True)),
|
||||
('description', StringProperty()),
|
||||
('url', StringProperty()),
|
||||
('hashes', HashesProperty()),
|
||||
('external_id', StringProperty()),
|
||||
])
|
||||
|
||||
def _check_object_constraints(self):
|
||||
super(ExternalReference, self)._check_object_constraints()
|
||||
self._check_at_least_one_property(["description", "external_id", "url"])
|
||||
|
||||
|
||||
class KillChainPhase(_STIXBase):
|
||||
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('kill_chain_name', StringProperty(required=True)),
|
||||
('phase_name', StringProperty(required=True)),
|
||||
])
|
||||
|
||||
|
||||
class GranularMarking(_STIXBase):
|
||||
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('marking_ref', ReferenceProperty(required=True, type="marking-definition")),
|
||||
('selectors', ListProperty(SelectorProperty, required=True)),
|
||||
])
|
||||
|
||||
|
||||
class TLPMarking(_STIXBase):
|
||||
|
||||
_type = 'tlp'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('tlp', Property(required=True))
|
||||
])
|
||||
|
||||
|
||||
class StatementMarking(_STIXBase):
|
||||
|
||||
_type = 'statement'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('statement', StringProperty(required=True))
|
||||
])
|
||||
|
||||
def __init__(self, statement=None, **kwargs):
|
||||
# Allow statement as positional args.
|
||||
if statement and not kwargs.get('statement'):
|
||||
kwargs['statement'] = statement
|
||||
|
||||
super(StatementMarking, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class MarkingProperty(Property):
|
||||
"""Represent the marking objects in the ``definition`` property of
|
||||
marking-definition objects.
|
||||
"""
|
||||
|
||||
def clean(self, value):
|
||||
if type(value) in OBJ_MAP_MARKING.values():
|
||||
return value
|
||||
else:
|
||||
raise ValueError("must be a Statement, TLP Marking or a registered marking.")
|
||||
|
||||
|
||||
class MarkingDefinition(_STIXBase, _MarkingsMixin):
|
||||
|
||||
_type = 'marking-definition'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
('definition_type', StringProperty(required=True)),
|
||||
('definition', MarkingProperty(required=True)),
|
||||
])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if set(('definition_type', 'definition')).issubset(kwargs.keys()):
|
||||
# Create correct marking type object
|
||||
try:
|
||||
marking_type = OBJ_MAP_MARKING[kwargs['definition_type']]
|
||||
except KeyError:
|
||||
raise ValueError("definition_type must be a valid marking type")
|
||||
|
||||
if not isinstance(kwargs['definition'], marking_type):
|
||||
defn = get_dict(kwargs['definition'])
|
||||
kwargs['definition'] = marking_type(**defn)
|
||||
|
||||
super(MarkingDefinition, self).__init__(**kwargs)
|
||||
|
||||
|
||||
OBJ_MAP_MARKING = {
|
||||
'tlp': TLPMarking,
|
||||
'statement': StatementMarking,
|
||||
}
|
||||
|
||||
|
||||
def _register_marking(cls):
|
||||
"""Register a custom STIX Marking Definition type.
|
||||
"""
|
||||
OBJ_MAP_MARKING[cls._type] = cls
|
||||
return cls
|
||||
|
||||
|
||||
def CustomMarking(type='x-custom-marking', properties=None):
|
||||
"""Custom STIX Marking decorator.
|
||||
|
||||
Example:
|
||||
>>> @CustomMarking('x-custom-marking', [
|
||||
... ('property1', StringProperty(required=True)),
|
||||
... ('property2', IntegerProperty()),
|
||||
... ])
|
||||
... class MyNewMarkingObjectType():
|
||||
... pass
|
||||
|
||||
"""
|
||||
def custom_builder(cls):
|
||||
|
||||
class _Custom(cls, _STIXBase):
|
||||
_type = type
|
||||
_properties = OrderedDict()
|
||||
|
||||
if not properties or not isinstance(properties, list):
|
||||
raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]")
|
||||
|
||||
_properties.update(properties)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
_STIXBase.__init__(self, **kwargs)
|
||||
cls.__init__(self, **kwargs)
|
||||
|
||||
_register_marking(_Custom)
|
||||
return _Custom
|
||||
|
||||
return custom_builder
|
||||
|
||||
|
||||
# TODO: don't allow the creation of any other TLPMarkings than the ones below
|
||||
|
||||
TLP_WHITE = MarkingDefinition(
|
||||
id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
|
||||
created="2017-01-20T00:00:00.000Z",
|
||||
definition_type="tlp",
|
||||
definition=TLPMarking(tlp="white")
|
||||
)
|
||||
|
||||
TLP_GREEN = MarkingDefinition(
|
||||
id="marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da",
|
||||
created="2017-01-20T00:00:00.000Z",
|
||||
definition_type="tlp",
|
||||
definition=TLPMarking(tlp="green")
|
||||
)
|
||||
|
||||
TLP_AMBER = MarkingDefinition(
|
||||
id="marking-definition--f88d31f6-486f-44da-b317-01333bde0b82",
|
||||
created="2017-01-20T00:00:00.000Z",
|
||||
definition_type="tlp",
|
||||
definition=TLPMarking(tlp="amber")
|
||||
)
|
||||
|
||||
TLP_RED = MarkingDefinition(
|
||||
id="marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed",
|
||||
created="2017-01-20T00:00:00.000Z",
|
||||
definition_type="tlp",
|
||||
definition=TLPMarking(tlp="red")
|
||||
)
|
|
@ -0,0 +1,948 @@
|
|||
"""STIX 2.0 Cyber Observable Objects.
|
||||
|
||||
Embedded observable object types, such as Email MIME Component, which is
|
||||
embedded in Email Message objects, inherit from ``_STIXBase`` instead of
|
||||
Observable and do not have a ``_type`` attribute.
|
||||
"""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from ..base import _Extension, _Observable, _STIXBase
|
||||
from ..exceptions import (AtLeastOnePropertyError, DependentPropertiesError,
|
||||
ParseError)
|
||||
from ..properties import (BinaryProperty, BooleanProperty, DictionaryProperty,
|
||||
EmbeddedObjectProperty, EnumProperty, FloatProperty,
|
||||
HashesProperty, HexProperty, IntegerProperty,
|
||||
ListProperty, ObjectReferenceProperty, Property,
|
||||
StringProperty, TimestampProperty, TypeProperty)
|
||||
from ..utils import get_dict
|
||||
|
||||
|
||||
class ObservableProperty(Property):
|
||||
"""Property for holding Cyber Observable Objects.
|
||||
"""
|
||||
|
||||
def clean(self, value):
|
||||
try:
|
||||
dictified = get_dict(value)
|
||||
except ValueError:
|
||||
raise ValueError("The observable property must contain a dictionary")
|
||||
if dictified == {}:
|
||||
raise ValueError("The observable property must contain a non-empty dictionary")
|
||||
|
||||
valid_refs = dict((k, v['type']) for (k, v) in dictified.items())
|
||||
|
||||
for key, obj in dictified.items():
|
||||
parsed_obj = parse_observable(obj, valid_refs)
|
||||
dictified[key] = parsed_obj
|
||||
|
||||
return dictified
|
||||
|
||||
|
||||
class ExtensionsProperty(DictionaryProperty):
|
||||
"""Property for representing extensions on Observable objects.
|
||||
"""
|
||||
|
||||
def __init__(self, enclosing_type=None, required=False):
|
||||
self.enclosing_type = enclosing_type
|
||||
super(ExtensionsProperty, self).__init__(required)
|
||||
|
||||
def clean(self, value):
|
||||
try:
|
||||
dictified = get_dict(value)
|
||||
except ValueError:
|
||||
raise ValueError("The extensions property must contain a dictionary")
|
||||
if dictified == {}:
|
||||
raise ValueError("The extensions property must contain a non-empty dictionary")
|
||||
|
||||
if self.enclosing_type in EXT_MAP:
|
||||
specific_type_map = EXT_MAP[self.enclosing_type]
|
||||
for key, subvalue in dictified.items():
|
||||
if key in specific_type_map:
|
||||
cls = specific_type_map[key]
|
||||
if type(subvalue) is dict:
|
||||
dictified[key] = cls(**subvalue)
|
||||
elif type(subvalue) is cls:
|
||||
dictified[key] = subvalue
|
||||
else:
|
||||
raise ValueError("Cannot determine extension type.")
|
||||
else:
|
||||
raise ValueError("The key used in the extensions dictionary is not an extension type name")
|
||||
else:
|
||||
raise ValueError("The enclosing type '%s' has no extensions defined" % self.enclosing_type)
|
||||
return dictified
|
||||
|
||||
|
||||
class Artifact(_Observable):
|
||||
|
||||
_type = 'artifact'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('mime_type', StringProperty()),
|
||||
('payload_bin', BinaryProperty()),
|
||||
('url', StringProperty()),
|
||||
('hashes', HashesProperty()),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
def _check_object_constraints(self):
|
||||
super(Artifact, self)._check_object_constraints()
|
||||
self._check_mutually_exclusive_properties(["payload_bin", "url"])
|
||||
self._check_properties_dependency(["hashes"], ["url"])
|
||||
|
||||
|
||||
class AutonomousSystem(_Observable):
|
||||
|
||||
_type = 'autonomous-system'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('number', IntegerProperty(required=True)),
|
||||
('name', StringProperty()),
|
||||
('rir', StringProperty()),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
|
||||
class Directory(_Observable):
|
||||
|
||||
_type = 'directory'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('path', StringProperty(required=True)),
|
||||
('path_enc', StringProperty()),
|
||||
# these are not the created/modified timestamps of the object itself
|
||||
('created', TimestampProperty()),
|
||||
('modified', TimestampProperty()),
|
||||
('accessed', TimestampProperty()),
|
||||
('contains_refs', ListProperty(ObjectReferenceProperty(valid_types=['file', 'directory']))),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
|
||||
class DomainName(_Observable):
|
||||
|
||||
_type = 'domain-name'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('value', StringProperty(required=True)),
|
||||
('resolves_to_refs', ListProperty(ObjectReferenceProperty(valid_types=['ipv4-addr', 'ipv6-addr', 'domain-name']))),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
|
||||
class EmailAddress(_Observable):
|
||||
|
||||
_type = 'email-addr'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('value', StringProperty(required=True)),
|
||||
('display_name', StringProperty()),
|
||||
('belongs_to_ref', ObjectReferenceProperty(valid_types='user-account')),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
|
||||
class EmailMIMEComponent(_STIXBase):
|
||||
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('body', StringProperty()),
|
||||
('body_raw_ref', ObjectReferenceProperty(valid_types=['artifact', 'file'])),
|
||||
('content_type', StringProperty()),
|
||||
('content_disposition', StringProperty()),
|
||||
])
|
||||
|
||||
def _check_object_constraints(self):
|
||||
super(EmailMIMEComponent, self)._check_object_constraints()
|
||||
self._check_at_least_one_property(["body", "body_raw_ref"])
|
||||
|
||||
|
||||
class EmailMessage(_Observable):
|
||||
|
||||
_type = 'email-message'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('is_multipart', BooleanProperty(required=True)),
|
||||
('date', TimestampProperty()),
|
||||
('content_type', StringProperty()),
|
||||
('from_ref', ObjectReferenceProperty(valid_types='email-addr')),
|
||||
('sender_ref', ObjectReferenceProperty(valid_types='email-addr')),
|
||||
('to_refs', ListProperty(ObjectReferenceProperty(valid_types='email-addr'))),
|
||||
('cc_refs', ListProperty(ObjectReferenceProperty(valid_types='email-addr'))),
|
||||
('bcc_refs', ListProperty(ObjectReferenceProperty(valid_types='email-addr'))),
|
||||
('subject', StringProperty()),
|
||||
('received_lines', ListProperty(StringProperty)),
|
||||
('additional_header_fields', DictionaryProperty()),
|
||||
('body', StringProperty()),
|
||||
('body_multipart', ListProperty(EmbeddedObjectProperty(type=EmailMIMEComponent))),
|
||||
('raw_email_ref', ObjectReferenceProperty(valid_types='artifact')),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
def _check_object_constraints(self):
|
||||
super(EmailMessage, self)._check_object_constraints()
|
||||
self._check_properties_dependency(["is_multipart"], ["body_multipart"])
|
||||
if self.get("is_multipart") is True and self.get("body"):
|
||||
# 'body' MAY only be used if is_multipart is false.
|
||||
raise DependentPropertiesError(self.__class__, [("is_multipart", "body")])
|
||||
|
||||
|
||||
class ArchiveExt(_Extension):
|
||||
|
||||
_type = 'archive-ext'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('contains_refs', ListProperty(ObjectReferenceProperty(valid_types='file'), required=True)),
|
||||
('version', StringProperty()),
|
||||
('comment', StringProperty()),
|
||||
])
|
||||
|
||||
|
||||
class AlternateDataStream(_STIXBase):
|
||||
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('name', StringProperty(required=True)),
|
||||
('hashes', HashesProperty()),
|
||||
('size', IntegerProperty()),
|
||||
])
|
||||
|
||||
|
||||
class NTFSExt(_Extension):
|
||||
|
||||
_type = 'ntfs-ext'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('sid', StringProperty()),
|
||||
('alternate_data_streams', ListProperty(EmbeddedObjectProperty(type=AlternateDataStream))),
|
||||
])
|
||||
|
||||
|
||||
class PDFExt(_Extension):
|
||||
|
||||
_type = 'pdf-ext'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('version', StringProperty()),
|
||||
('is_optimized', BooleanProperty()),
|
||||
('document_info_dict', DictionaryProperty()),
|
||||
('pdfid0', StringProperty()),
|
||||
('pdfid1', StringProperty()),
|
||||
])
|
||||
|
||||
|
||||
class RasterImageExt(_Extension):
|
||||
|
||||
_type = 'raster-image-ext'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('image_height', IntegerProperty()),
|
||||
('image_weight', IntegerProperty()),
|
||||
('bits_per_pixel', IntegerProperty()),
|
||||
('image_compression_algorithm', StringProperty()),
|
||||
('exif_tags', DictionaryProperty()),
|
||||
])
|
||||
|
||||
|
||||
class WindowsPEOptionalHeaderType(_STIXBase):
|
||||
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('magic_hex', HexProperty()),
|
||||
('major_linker_version', IntegerProperty()),
|
||||
('minor_linker_version', IntegerProperty()),
|
||||
('size_of_code', IntegerProperty()),
|
||||
('size_of_initialized_data', IntegerProperty()),
|
||||
('size_of_uninitialized_data', IntegerProperty()),
|
||||
('address_of_entry_point', IntegerProperty()),
|
||||
('base_of_code', IntegerProperty()),
|
||||
('base_of_data', IntegerProperty()),
|
||||
('image_base', IntegerProperty()),
|
||||
('section_alignment', IntegerProperty()),
|
||||
('file_alignment', IntegerProperty()),
|
||||
('major_os_version', IntegerProperty()),
|
||||
('minor_os_version', IntegerProperty()),
|
||||
('major_image_version', IntegerProperty()),
|
||||
('minor_image_version', IntegerProperty()),
|
||||
('major_subsystem_version', IntegerProperty()),
|
||||
('minor_subsystem_version', IntegerProperty()),
|
||||
('win32_version_value_hex', HexProperty()),
|
||||
('size_of_image', IntegerProperty()),
|
||||
('size_of_headers', IntegerProperty()),
|
||||
('checksum_hex', HexProperty()),
|
||||
('subsystem_hex', HexProperty()),
|
||||
('dll_characteristics_hex', HexProperty()),
|
||||
('size_of_stack_reserve', IntegerProperty()),
|
||||
('size_of_stack_commit', IntegerProperty()),
|
||||
('size_of_heap_reserve', IntegerProperty()),
|
||||
('size_of_heap_commit', IntegerProperty()),
|
||||
('loader_flags_hex', HexProperty()),
|
||||
('number_of_rva_and_sizes', IntegerProperty()),
|
||||
('hashes', HashesProperty()),
|
||||
])
|
||||
|
||||
def _check_object_constraints(self):
|
||||
super(WindowsPEOptionalHeaderType, self)._check_object_constraints()
|
||||
self._check_at_least_one_property()
|
||||
|
||||
|
||||
class WindowsPESection(_STIXBase):
|
||||
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('name', StringProperty(required=True)),
|
||||
('size', IntegerProperty()),
|
||||
('entropy', FloatProperty()),
|
||||
('hashes', HashesProperty()),
|
||||
])
|
||||
|
||||
|
||||
class WindowsPEBinaryExt(_Extension):
|
||||
|
||||
_type = 'windows-pebinary-ext'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('pe_type', StringProperty(required=True)), # open_vocab
|
||||
('imphash', StringProperty()),
|
||||
('machine_hex', HexProperty()),
|
||||
('number_of_sections', IntegerProperty()),
|
||||
('time_date_stamp', TimestampProperty(precision='second')),
|
||||
('pointer_to_symbol_table_hex', HexProperty()),
|
||||
('number_of_symbols', IntegerProperty()),
|
||||
('size_of_optional_header', IntegerProperty()),
|
||||
('characteristics_hex', HexProperty()),
|
||||
('file_header_hashes', HashesProperty()),
|
||||
('optional_header', EmbeddedObjectProperty(type=WindowsPEOptionalHeaderType)),
|
||||
('sections', ListProperty(EmbeddedObjectProperty(type=WindowsPESection))),
|
||||
])
|
||||
|
||||
|
||||
class File(_Observable):
|
||||
|
||||
_type = 'file'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('hashes', HashesProperty()),
|
||||
('size', IntegerProperty()),
|
||||
('name', StringProperty()),
|
||||
('name_enc', StringProperty()),
|
||||
('magic_number_hex', HexProperty()),
|
||||
('mime_type', StringProperty()),
|
||||
# these are not the created/modified timestamps of the object itself
|
||||
('created', TimestampProperty()),
|
||||
('modified', TimestampProperty()),
|
||||
('accessed', TimestampProperty()),
|
||||
('parent_directory_ref', ObjectReferenceProperty(valid_types='directory')),
|
||||
('is_encrypted', BooleanProperty()),
|
||||
('encryption_algorithm', StringProperty()),
|
||||
('decryption_key', StringProperty()),
|
||||
('contains_refs', ListProperty(ObjectReferenceProperty)),
|
||||
('content_ref', ObjectReferenceProperty(valid_types='artifact')),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
def _check_object_constraints(self):
|
||||
super(File, self)._check_object_constraints()
|
||||
self._check_properties_dependency(["is_encrypted"], ["encryption_algorithm", "decryption_key"])
|
||||
self._check_at_least_one_property(["hashes", "name"])
|
||||
|
||||
|
||||
class IPv4Address(_Observable):
|
||||
|
||||
_type = 'ipv4-addr'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('value', StringProperty(required=True)),
|
||||
('resolves_to_refs', ListProperty(ObjectReferenceProperty(valid_types='mac-addr'))),
|
||||
('belongs_to_refs', ListProperty(ObjectReferenceProperty(valid_types='autonomous-system'))),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
|
||||
class IPv6Address(_Observable):
|
||||
|
||||
_type = 'ipv6-addr'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('value', StringProperty(required=True)),
|
||||
('resolves_to_refs', ListProperty(ObjectReferenceProperty(valid_types='mac-addr'))),
|
||||
('belongs_to_refs', ListProperty(ObjectReferenceProperty(valid_types='autonomous-system'))),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
|
||||
class MACAddress(_Observable):
|
||||
|
||||
_type = 'mac-addr'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('value', StringProperty(required=True)),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
|
||||
class Mutex(_Observable):
|
||||
|
||||
_type = 'mutex'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('name', StringProperty(required=True)),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
|
||||
class HTTPRequestExt(_Extension):
|
||||
|
||||
_type = 'http-request-ext'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('request_method', StringProperty(required=True)),
|
||||
('request_value', StringProperty(required=True)),
|
||||
('request_version', StringProperty()),
|
||||
('request_header', DictionaryProperty()),
|
||||
('message_body_length', IntegerProperty()),
|
||||
('message_body_data_ref', ObjectReferenceProperty(valid_types='artifact')),
|
||||
])
|
||||
|
||||
|
||||
class ICMPExt(_Extension):
|
||||
|
||||
_type = 'icmp-ext'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('icmp_type_hex', HexProperty(required=True)),
|
||||
('icmp_code_hex', HexProperty(required=True)),
|
||||
])
|
||||
|
||||
|
||||
class SocketExt(_Extension):
|
||||
|
||||
_type = 'socket-ext'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('address_family', EnumProperty(allowed=[
|
||||
"AF_UNSPEC",
|
||||
"AF_INET",
|
||||
"AF_IPX",
|
||||
"AF_APPLETALK",
|
||||
"AF_NETBIOS",
|
||||
"AF_INET6",
|
||||
"AF_IRDA",
|
||||
"AF_BTH",
|
||||
], required=True)),
|
||||
('is_blocking', BooleanProperty()),
|
||||
('is_listening', BooleanProperty()),
|
||||
('protocol_family', EnumProperty(allowed=[
|
||||
"PF_INET",
|
||||
"PF_IPX",
|
||||
"PF_APPLETALK",
|
||||
"PF_INET6",
|
||||
"PF_AX25",
|
||||
"PF_NETROM"
|
||||
])),
|
||||
('options', DictionaryProperty()),
|
||||
('socket_type', EnumProperty(allowed=[
|
||||
"SOCK_STREAM",
|
||||
"SOCK_DGRAM",
|
||||
"SOCK_RAW",
|
||||
"SOCK_RDM",
|
||||
"SOCK_SEQPACKET",
|
||||
])),
|
||||
('socket_descriptor', IntegerProperty()),
|
||||
('socket_handle', IntegerProperty()),
|
||||
])
|
||||
|
||||
|
||||
class TCPExt(_Extension):
|
||||
|
||||
_type = 'tcp-ext'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('src_flags_hex', HexProperty()),
|
||||
('dst_flags_hex', HexProperty()),
|
||||
])
|
||||
|
||||
|
||||
class NetworkTraffic(_Observable):
|
||||
|
||||
_type = 'network-traffic'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('start', TimestampProperty()),
|
||||
('end', TimestampProperty()),
|
||||
('is_active', BooleanProperty()),
|
||||
('src_ref', ObjectReferenceProperty(valid_types=['ipv4-addr', 'ipv6-addr', 'mac-addr', 'domain-name'])),
|
||||
('dst_ref', ObjectReferenceProperty(valid_types=['ipv4-addr', 'ipv6-addr', 'mac-addr', 'domain-name'])),
|
||||
('src_port', IntegerProperty()),
|
||||
('dst_port', IntegerProperty()),
|
||||
('protocols', ListProperty(StringProperty, required=True)),
|
||||
('src_byte_count', IntegerProperty()),
|
||||
('dst_byte_count', IntegerProperty()),
|
||||
('src_packets', IntegerProperty()),
|
||||
('dst_packets', IntegerProperty()),
|
||||
('ipfix', DictionaryProperty()),
|
||||
('src_payload_ref', ObjectReferenceProperty(valid_types='artifact')),
|
||||
('dst_payload_ref', ObjectReferenceProperty(valid_types='artifact')),
|
||||
('encapsulates_refs', ListProperty(ObjectReferenceProperty(valid_types='network-traffic'))),
|
||||
('encapsulates_by_ref', ObjectReferenceProperty(valid_types='network-traffic')),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
def _check_object_constraints(self):
|
||||
super(NetworkTraffic, self)._check_object_constraints()
|
||||
self._check_at_least_one_property(["src_ref", "dst_ref"])
|
||||
|
||||
|
||||
class WindowsProcessExt(_Extension):
|
||||
|
||||
_type = 'windows-process-ext'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('aslr_enabled', BooleanProperty()),
|
||||
('dep_enabled', BooleanProperty()),
|
||||
('priority', StringProperty()),
|
||||
('owner_sid', StringProperty()),
|
||||
('window_title', StringProperty()),
|
||||
('startup_info', DictionaryProperty()),
|
||||
])
|
||||
|
||||
|
||||
class WindowsServiceExt(_Extension):
|
||||
|
||||
_type = 'windows-service-ext'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('service_name', StringProperty(required=True)),
|
||||
('descriptions', ListProperty(StringProperty)),
|
||||
('display_name', StringProperty()),
|
||||
('group_name', StringProperty()),
|
||||
('start_type', EnumProperty(allowed=[
|
||||
"SERVICE_AUTO_START",
|
||||
"SERVICE_BOOT_START",
|
||||
"SERVICE_DEMAND_START",
|
||||
"SERVICE_DISABLED",
|
||||
"SERVICE_SYSTEM_ALERT",
|
||||
])),
|
||||
('service_dll_refs', ListProperty(ObjectReferenceProperty(valid_types='file'))),
|
||||
('service_type', EnumProperty(allowed=[
|
||||
"SERVICE_KERNEL_DRIVER",
|
||||
"SERVICE_FILE_SYSTEM_DRIVER",
|
||||
"SERVICE_WIN32_OWN_PROCESS",
|
||||
"SERVICE_WIN32_SHARE_PROCESS",
|
||||
])),
|
||||
('service_status', EnumProperty(allowed=[
|
||||
"SERVICE_CONTINUE_PENDING",
|
||||
"SERVICE_PAUSE_PENDING",
|
||||
"SERVICE_PAUSED",
|
||||
"SERVICE_RUNNING",
|
||||
"SERVICE_START_PENDING",
|
||||
"SERVICE_STOP_PENDING",
|
||||
"SERVICE_STOPPED",
|
||||
])),
|
||||
])
|
||||
|
||||
|
||||
class Process(_Observable):
|
||||
|
||||
_type = 'process'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('is_hidden', BooleanProperty()),
|
||||
('pid', IntegerProperty()),
|
||||
('name', StringProperty()),
|
||||
# this is not the created timestamps of the object itself
|
||||
('created', TimestampProperty()),
|
||||
('cwd', StringProperty()),
|
||||
('arguments', ListProperty(StringProperty)),
|
||||
('command_line', StringProperty()),
|
||||
('environment_variables', DictionaryProperty()),
|
||||
('opened_connection_refs', ListProperty(ObjectReferenceProperty(valid_types='network-traffic'))),
|
||||
('creator_user_ref', ObjectReferenceProperty(valid_types='user-account')),
|
||||
('binary_ref', ObjectReferenceProperty(valid_types='file')),
|
||||
('parent_ref', ObjectReferenceProperty(valid_types='process')),
|
||||
('child_refs', ListProperty(ObjectReferenceProperty('process'))),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
def _check_object_constraints(self):
|
||||
# no need to check windows-service-ext, since it has a required property
|
||||
super(Process, self)._check_object_constraints()
|
||||
try:
|
||||
self._check_at_least_one_property()
|
||||
if "windows-process-ext" in self.get('extensions', {}):
|
||||
self.extensions["windows-process-ext"]._check_at_least_one_property()
|
||||
except AtLeastOnePropertyError as enclosing_exc:
|
||||
if 'extensions' not in self:
|
||||
raise enclosing_exc
|
||||
else:
|
||||
if "windows-process-ext" in self.get('extensions', {}):
|
||||
self.extensions["windows-process-ext"]._check_at_least_one_property()
|
||||
|
||||
|
||||
class Software(_Observable):
|
||||
|
||||
_type = 'software'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('name', StringProperty(required=True)),
|
||||
('cpe', StringProperty()),
|
||||
('languages', ListProperty(StringProperty)),
|
||||
('vendor', StringProperty()),
|
||||
('version', StringProperty()),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
|
||||
class URL(_Observable):
|
||||
|
||||
_type = 'url'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('value', StringProperty(required=True)),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
|
||||
class UNIXAccountExt(_Extension):
|
||||
|
||||
_type = 'unix-account-ext'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('gid', IntegerProperty()),
|
||||
('groups', ListProperty(StringProperty)),
|
||||
('home_dir', StringProperty()),
|
||||
('shell', StringProperty()),
|
||||
])
|
||||
|
||||
|
||||
class UserAccount(_Observable):
|
||||
|
||||
_type = 'user-account'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('user_id', StringProperty(required=True)),
|
||||
('account_login', StringProperty()),
|
||||
('account_type', StringProperty()), # open vocab
|
||||
('display_name', StringProperty()),
|
||||
('is_service_account', BooleanProperty()),
|
||||
('is_privileged', BooleanProperty()),
|
||||
('can_escalate_privs', BooleanProperty()),
|
||||
('is_disabled', BooleanProperty()),
|
||||
('account_created', TimestampProperty()),
|
||||
('account_expires', TimestampProperty()),
|
||||
('password_last_changed', TimestampProperty()),
|
||||
('account_first_login', TimestampProperty()),
|
||||
('account_last_login', TimestampProperty()),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
|
||||
class WindowsRegistryValueType(_STIXBase):
|
||||
|
||||
_type = 'windows-registry-value-type'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('name', StringProperty(required=True)),
|
||||
('data', StringProperty()),
|
||||
('data_type', EnumProperty(allowed=[
|
||||
'REG_NONE',
|
||||
'REG_SZ',
|
||||
'REG_EXPAND_SZ',
|
||||
'REG_BINARY',
|
||||
'REG_DWORD',
|
||||
'REG_DWORD_BIG_ENDIAN',
|
||||
'REG_LINK',
|
||||
'REG_MULTI_SZ',
|
||||
'REG_RESOURCE_LIST',
|
||||
'REG_FULL_RESOURCE_DESCRIPTION',
|
||||
'REG_RESOURCE_REQUIREMENTS_LIST',
|
||||
'REG_QWORD',
|
||||
'REG_INVALID_TYPE',
|
||||
])),
|
||||
])
|
||||
|
||||
|
||||
class WindowsRegistryKey(_Observable):
|
||||
|
||||
_type = 'windows-registry-key'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('key', StringProperty(required=True)),
|
||||
('values', ListProperty(EmbeddedObjectProperty(type=WindowsRegistryValueType))),
|
||||
# this is not the modified timestamps of the object itself
|
||||
('modified', TimestampProperty()),
|
||||
('creator_user_ref', ObjectReferenceProperty(valid_types='user-account')),
|
||||
('number_of_subkeys', IntegerProperty()),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
# Needed because 'values' is a property on collections.Mapping objects
|
||||
return self._inner['values']
|
||||
|
||||
|
||||
class X509V3ExtenstionsType(_STIXBase):
|
||||
|
||||
_type = 'x509-v3-extensions-type'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('basic_constraints', StringProperty()),
|
||||
('name_constraints', StringProperty()),
|
||||
('policy_constraints', StringProperty()),
|
||||
('key_usage', StringProperty()),
|
||||
('extended_key_usage', StringProperty()),
|
||||
('subject_key_identifier', StringProperty()),
|
||||
('authority_key_identifier', StringProperty()),
|
||||
('subject_alternative_name', StringProperty()),
|
||||
('issuer_alternative_name', StringProperty()),
|
||||
('subject_directory_attributes', StringProperty()),
|
||||
('crl_distribution_points', StringProperty()),
|
||||
('inhibit_any_policy', StringProperty()),
|
||||
('private_key_usage_period_not_before', TimestampProperty()),
|
||||
('private_key_usage_period_not_after', TimestampProperty()),
|
||||
('certificate_policies', StringProperty()),
|
||||
('policy_mappings', StringProperty()),
|
||||
])
|
||||
|
||||
|
||||
class X509Certificate(_Observable):
|
||||
|
||||
_type = 'x509-certificate'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('is_self_signed', BooleanProperty()),
|
||||
('hashes', HashesProperty()),
|
||||
('version', StringProperty()),
|
||||
('serial_number', StringProperty()),
|
||||
('signature_algorithm', StringProperty()),
|
||||
('issuer', StringProperty()),
|
||||
('validity_not_before', TimestampProperty()),
|
||||
('validity_not_after', TimestampProperty()),
|
||||
('subject', StringProperty()),
|
||||
('subject_public_key_algorithm', StringProperty()),
|
||||
('subject_public_key_modulus', StringProperty()),
|
||||
('subject_public_key_exponent', IntegerProperty()),
|
||||
('x509_v3_extensions', EmbeddedObjectProperty(type=X509V3ExtenstionsType)),
|
||||
('extensions', ExtensionsProperty(enclosing_type=_type)),
|
||||
])
|
||||
|
||||
|
||||
OBJ_MAP_OBSERVABLE = {
|
||||
'artifact': Artifact,
|
||||
'autonomous-system': AutonomousSystem,
|
||||
'directory': Directory,
|
||||
'domain-name': DomainName,
|
||||
'email-addr': EmailAddress,
|
||||
'email-message': EmailMessage,
|
||||
'file': File,
|
||||
'ipv4-addr': IPv4Address,
|
||||
'ipv6-addr': IPv6Address,
|
||||
'mac-addr': MACAddress,
|
||||
'mutex': Mutex,
|
||||
'network-traffic': NetworkTraffic,
|
||||
'process': Process,
|
||||
'software': Software,
|
||||
'url': URL,
|
||||
'user-account': UserAccount,
|
||||
'windows-registry-key': WindowsRegistryKey,
|
||||
'x509-certificate': X509Certificate,
|
||||
}
|
||||
|
||||
|
||||
EXT_MAP = {
|
||||
'file': {
|
||||
'archive-ext': ArchiveExt,
|
||||
'ntfs-ext': NTFSExt,
|
||||
'pdf-ext': PDFExt,
|
||||
'raster-image-ext': RasterImageExt,
|
||||
'windows-pebinary-ext': WindowsPEBinaryExt
|
||||
},
|
||||
'network-traffic': {
|
||||
'http-request-ext': HTTPRequestExt,
|
||||
'icmp-ext': ICMPExt,
|
||||
'socket-ext': SocketExt,
|
||||
'tcp-ext': TCPExt,
|
||||
},
|
||||
'process': {
|
||||
'windows-process-ext': WindowsProcessExt,
|
||||
'windows-service-ext': WindowsServiceExt,
|
||||
},
|
||||
'user-account': {
|
||||
'unix-account-ext': UNIXAccountExt,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def parse_observable(data, _valid_refs=None, allow_custom=False):
|
||||
"""Deserialize a string or file-like object into a STIX Cyber Observable
|
||||
object.
|
||||
|
||||
Args:
|
||||
data: The STIX 2 string to be parsed.
|
||||
_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: Whether to allow custom properties or not.
|
||||
Default: False.
|
||||
|
||||
Returns:
|
||||
An instantiated Python STIX Cyber Observable object.
|
||||
"""
|
||||
|
||||
obj = get_dict(data)
|
||||
obj['_valid_refs'] = _valid_refs or []
|
||||
|
||||
if 'type' not in obj:
|
||||
raise ParseError("Can't parse observable with no 'type' property: %s" % str(obj))
|
||||
try:
|
||||
obj_class = OBJ_MAP_OBSERVABLE[obj['type']]
|
||||
except KeyError:
|
||||
raise ParseError("Can't parse unknown observable type '%s'! For custom observables, "
|
||||
"use the CustomObservable decorator." % obj['type'])
|
||||
|
||||
if 'extensions' in obj and obj['type'] in EXT_MAP:
|
||||
for name, ext in obj['extensions'].items():
|
||||
if name not in EXT_MAP[obj['type']]:
|
||||
raise ParseError("Can't parse Unknown extension type '%s' for observable type '%s'!" % (name, obj['type']))
|
||||
ext_class = EXT_MAP[obj['type']][name]
|
||||
obj['extensions'][name] = ext_class(allow_custom=allow_custom, **obj['extensions'][name])
|
||||
|
||||
return obj_class(allow_custom=allow_custom, **obj)
|
||||
|
||||
|
||||
def _register_observable(new_observable):
|
||||
"""Register a custom STIX Cyber Observable type.
|
||||
"""
|
||||
|
||||
OBJ_MAP_OBSERVABLE[new_observable._type] = new_observable
|
||||
|
||||
|
||||
def CustomObservable(type='x-custom-observable', properties=None):
|
||||
"""Custom STIX Cyber Observable Object type decorator.
|
||||
|
||||
Example:
|
||||
>>> @CustomObservable('x-custom-observable', [
|
||||
... ('property1', StringProperty(required=True)),
|
||||
... ('property2', IntegerProperty()),
|
||||
... ])
|
||||
... class MyNewObservableType():
|
||||
... pass
|
||||
"""
|
||||
|
||||
def custom_builder(cls):
|
||||
|
||||
class _Custom(cls, _Observable):
|
||||
|
||||
_type = type
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
])
|
||||
|
||||
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 not isinstance(prop, ObjectReferenceProperty):
|
||||
raise ValueError("'%s' is named like an object reference property but "
|
||||
"is not an ObjectReferenceProperty." % prop_name)
|
||||
elif (prop_name.endswith('_refs') and (not isinstance(prop, ListProperty)
|
||||
or not isinstance(prop.contained, ObjectReferenceProperty))):
|
||||
raise ValueError("'%s' is named like an object reference list property but "
|
||||
"is not a ListProperty containing ObjectReferenceProperty." % prop_name)
|
||||
|
||||
_properties.update(properties)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
_Observable.__init__(self, **kwargs)
|
||||
try:
|
||||
cls.__init__(self, **kwargs)
|
||||
except (AttributeError, TypeError) as e:
|
||||
# Don't accidentally catch errors raised in a custom __init__()
|
||||
if ("has no attribute '__init__'" in str(e) or
|
||||
str(e) == "object.__init__() takes no parameters"):
|
||||
return
|
||||
raise e
|
||||
|
||||
_register_observable(_Custom)
|
||||
return _Custom
|
||||
|
||||
return custom_builder
|
||||
|
||||
|
||||
def _register_extension(observable, new_extension):
|
||||
"""Register a custom extension to a STIX Cyber Observable type.
|
||||
"""
|
||||
|
||||
try:
|
||||
observable_type = observable._type
|
||||
except AttributeError:
|
||||
raise ValueError("Unknown observable type. Custom observables must be "
|
||||
"created with the @CustomObservable decorator.")
|
||||
|
||||
try:
|
||||
EXT_MAP[observable_type][new_extension._type] = new_extension
|
||||
except KeyError:
|
||||
if observable_type not in OBJ_MAP_OBSERVABLE:
|
||||
raise ValueError("Unknown observable type '%s'. Custom observables "
|
||||
"must be created with the @CustomObservable decorator."
|
||||
% observable_type)
|
||||
else:
|
||||
EXT_MAP[observable_type] = {new_extension._type: new_extension}
|
||||
|
||||
|
||||
def CustomExtension(observable=None, type='x-custom-observable', properties=None):
|
||||
"""Decorator for custom extensions to STIX Cyber Observables.
|
||||
"""
|
||||
|
||||
if not observable or not issubclass(observable, _Observable):
|
||||
raise ValueError("'observable' must be a valid Observable class!")
|
||||
|
||||
def custom_builder(cls):
|
||||
|
||||
class _Custom(cls, _Extension):
|
||||
|
||||
_type = type
|
||||
_properties = {
|
||||
'extensions': ExtensionsProperty(enclosing_type=_type),
|
||||
}
|
||||
|
||||
if not isinstance(properties, dict) or not properties:
|
||||
raise ValueError("'properties' must be a dict!")
|
||||
|
||||
_properties.update(properties)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
_Extension.__init__(self, **kwargs)
|
||||
try:
|
||||
cls.__init__(self, **kwargs)
|
||||
except (AttributeError, TypeError) as e:
|
||||
# Don't accidentally catch errors raised in a custom __init__()
|
||||
if ("has no attribute '__init__'" in str(e) or
|
||||
str(e) == "object.__init__() takes no parameters"):
|
||||
return
|
||||
raise e
|
||||
|
||||
_register_extension(observable, _Custom)
|
||||
return _Custom
|
||||
|
||||
return custom_builder
|
|
@ -0,0 +1,364 @@
|
|||
"""STIX 2.0 Domain Objects"""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import stix2
|
||||
|
||||
from ..base import _STIXBase
|
||||
from ..markings import _MarkingsMixin
|
||||
from ..properties import (BooleanProperty, IDProperty, IntegerProperty,
|
||||
ListProperty, PatternProperty, ReferenceProperty,
|
||||
StringProperty, TimestampProperty, TypeProperty)
|
||||
from ..utils import NOW
|
||||
from .common import ExternalReference, GranularMarking, KillChainPhase
|
||||
from .observables import ObservableProperty
|
||||
|
||||
|
||||
class STIXDomainObject(_STIXBase, _MarkingsMixin):
|
||||
pass
|
||||
|
||||
|
||||
class AttackPattern(STIXDomainObject):
|
||||
|
||||
_type = 'attack-pattern'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('name', StringProperty(required=True)),
|
||||
('description', StringProperty()),
|
||||
('kill_chain_phases', ListProperty(KillChainPhase)),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
|
||||
class Campaign(STIXDomainObject):
|
||||
|
||||
_type = 'campaign'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('name', StringProperty(required=True)),
|
||||
('description', StringProperty()),
|
||||
('aliases', ListProperty(StringProperty)),
|
||||
('first_seen', TimestampProperty()),
|
||||
('last_seen', TimestampProperty()),
|
||||
('objective', StringProperty()),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
|
||||
class CourseOfAction(STIXDomainObject):
|
||||
|
||||
_type = 'course-of-action'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('name', StringProperty(required=True)),
|
||||
('description', StringProperty()),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
|
||||
class Identity(STIXDomainObject):
|
||||
|
||||
_type = 'identity'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('name', StringProperty(required=True)),
|
||||
('description', StringProperty()),
|
||||
('identity_class', StringProperty(required=True)),
|
||||
('sectors', ListProperty(StringProperty)),
|
||||
('contact_information', StringProperty()),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
|
||||
class Indicator(STIXDomainObject):
|
||||
|
||||
_type = 'indicator'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('name', StringProperty()),
|
||||
('description', StringProperty()),
|
||||
('pattern', PatternProperty(required=True)),
|
||||
('valid_from', TimestampProperty(default=lambda: NOW)),
|
||||
('valid_until', TimestampProperty()),
|
||||
('kill_chain_phases', ListProperty(KillChainPhase)),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty, required=True)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
|
||||
class IntrusionSet(STIXDomainObject):
|
||||
|
||||
_type = 'intrusion-set'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('name', StringProperty(required=True)),
|
||||
('description', StringProperty()),
|
||||
('aliases', ListProperty(StringProperty)),
|
||||
('first_seen', TimestampProperty()),
|
||||
('last_seen ', TimestampProperty()),
|
||||
('goals', ListProperty(StringProperty)),
|
||||
('resource_level', StringProperty()),
|
||||
('primary_motivation', StringProperty()),
|
||||
('secondary_motivations', ListProperty(StringProperty)),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
|
||||
class Malware(STIXDomainObject):
|
||||
|
||||
_type = 'malware'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('name', StringProperty(required=True)),
|
||||
('description', StringProperty()),
|
||||
('kill_chain_phases', ListProperty(KillChainPhase)),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty, required=True)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
|
||||
class ObservedData(STIXDomainObject):
|
||||
|
||||
_type = 'observed-data'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('first_observed', TimestampProperty(required=True)),
|
||||
('last_observed', TimestampProperty(required=True)),
|
||||
('number_observed', IntegerProperty(required=True)),
|
||||
('objects', ObservableProperty(required=True)),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
|
||||
class Report(STIXDomainObject):
|
||||
|
||||
_type = 'report'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('name', StringProperty(required=True)),
|
||||
('description', StringProperty()),
|
||||
('published', TimestampProperty(required=True)),
|
||||
('object_refs', ListProperty(ReferenceProperty, required=True)),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty, required=True)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
|
||||
class ThreatActor(STIXDomainObject):
|
||||
|
||||
_type = 'threat-actor'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('name', StringProperty(required=True)),
|
||||
('description', StringProperty()),
|
||||
('aliases', ListProperty(StringProperty)),
|
||||
('roles', ListProperty(StringProperty)),
|
||||
('goals', ListProperty(StringProperty)),
|
||||
('sophistication', StringProperty()),
|
||||
('resource_level', StringProperty()),
|
||||
('primary_motivation', StringProperty()),
|
||||
('secondary_motivations', ListProperty(StringProperty)),
|
||||
('personal_motivations', ListProperty(StringProperty)),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty, required=True)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
|
||||
class Tool(STIXDomainObject):
|
||||
|
||||
_type = 'tool'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('name', StringProperty(required=True)),
|
||||
('description', StringProperty()),
|
||||
('kill_chain_phases', ListProperty(KillChainPhase)),
|
||||
('tool_version', StringProperty()),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty, required=True)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
|
||||
class Vulnerability(STIXDomainObject):
|
||||
|
||||
_type = 'vulnerability'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('name', StringProperty(required=True)),
|
||||
('description', StringProperty()),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
|
||||
def CustomObject(type='x-custom-type', properties=None):
|
||||
"""Custom STIX Object type decorator.
|
||||
|
||||
Example:
|
||||
>>> @CustomObject('x-type-name', [
|
||||
... ('property1', StringProperty(required=True)),
|
||||
... ('property2', IntegerProperty()),
|
||||
... ])
|
||||
... class MyNewObjectType():
|
||||
... pass
|
||||
|
||||
Supply an ``__init__()`` function to add any special validations to the custom
|
||||
type. Don't call ``super().__init__()`` though - doing so will cause an error.
|
||||
|
||||
Example:
|
||||
>>> @CustomObject('x-type-name', [
|
||||
... ('property1', StringProperty(required=True)),
|
||||
... ('property2', IntegerProperty()),
|
||||
... ])
|
||||
... class MyNewObjectType():
|
||||
... def __init__(self, property2=None, **kwargs):
|
||||
... if property2 and property2 < 10:
|
||||
... raise ValueError("'property2' is too small.")
|
||||
"""
|
||||
|
||||
def custom_builder(cls):
|
||||
|
||||
class _Custom(cls, STIXDomainObject):
|
||||
_type = type
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
])
|
||||
|
||||
if not properties or not isinstance(properties, list):
|
||||
raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]")
|
||||
|
||||
_properties.update([x for x in properties if not x[0].startswith("x_")])
|
||||
|
||||
# This is to follow the general properties structure.
|
||||
_properties.update([
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
# Put all custom properties at the bottom, sorted alphabetically.
|
||||
_properties.update(sorted([x for x in properties if x[0].startswith("x_")], key=lambda x: x[0]))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
_STIXBase.__init__(self, **kwargs)
|
||||
try:
|
||||
cls.__init__(self, **kwargs)
|
||||
except (AttributeError, TypeError) as e:
|
||||
# Don't accidentally catch errors raised in a custom __init__()
|
||||
if ("has no attribute '__init__'" in str(e) or
|
||||
str(e) == "object.__init__() takes no parameters"):
|
||||
return
|
||||
raise e
|
||||
|
||||
stix2._register_type(_Custom, version="2.0")
|
||||
return _Custom
|
||||
|
||||
return custom_builder
|
|
@ -0,0 +1,82 @@
|
|||
"""STIX 2.0 Relationship Objects."""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from ..base import _STIXBase
|
||||
from ..markings import _MarkingsMixin
|
||||
from ..properties import (BooleanProperty, IDProperty, IntegerProperty,
|
||||
ListProperty, ReferenceProperty, StringProperty,
|
||||
TimestampProperty, TypeProperty)
|
||||
from ..utils import NOW
|
||||
from .common import ExternalReference, GranularMarking
|
||||
|
||||
|
||||
class STIXRelationshipObject(_STIXBase, _MarkingsMixin):
|
||||
pass
|
||||
|
||||
|
||||
class Relationship(STIXRelationshipObject):
|
||||
|
||||
_type = 'relationship'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('relationship_type', StringProperty(required=True)),
|
||||
('description', StringProperty()),
|
||||
('source_ref', ReferenceProperty(required=True)),
|
||||
('target_ref', ReferenceProperty(required=True)),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
# Explicitly define the first three kwargs to make readable Relationship declarations.
|
||||
def __init__(self, source_ref=None, relationship_type=None,
|
||||
target_ref=None, **kwargs):
|
||||
# Allow (source_ref, relationship_type, target_ref) as positional args.
|
||||
if source_ref and not kwargs.get('source_ref'):
|
||||
kwargs['source_ref'] = source_ref
|
||||
if relationship_type and not kwargs.get('relationship_type'):
|
||||
kwargs['relationship_type'] = relationship_type
|
||||
if target_ref and not kwargs.get('target_ref'):
|
||||
kwargs['target_ref'] = target_ref
|
||||
|
||||
super(Relationship, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class Sighting(STIXRelationshipObject):
|
||||
_type = 'sighting'
|
||||
_properties = OrderedDict()
|
||||
_properties.update([
|
||||
('type', TypeProperty(_type)),
|
||||
('id', IDProperty(_type)),
|
||||
('created_by_ref', ReferenceProperty(type="identity")),
|
||||
('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
|
||||
('first_seen', TimestampProperty()),
|
||||
('last_seen', TimestampProperty()),
|
||||
('count', IntegerProperty()),
|
||||
('sighting_of_ref', ReferenceProperty(required=True)),
|
||||
('observed_data_refs', ListProperty(ReferenceProperty(type="observed-data"))),
|
||||
('where_sighted_refs', ListProperty(ReferenceProperty(type="identity"))),
|
||||
('summary', BooleanProperty()),
|
||||
('revoked', BooleanProperty()),
|
||||
('labels', ListProperty(StringProperty)),
|
||||
('external_references', ListProperty(ExternalReference)),
|
||||
('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))),
|
||||
('granular_markings', ListProperty(GranularMarking)),
|
||||
])
|
||||
|
||||
# Explicitly define the first kwargs to make readable Sighting declarations.
|
||||
def __init__(self, sighting_of_ref=None, **kwargs):
|
||||
# Allow sighting_of_ref as a positional arg.
|
||||
if sighting_of_ref and not kwargs.get('sighting_of_ref'):
|
||||
kwargs['sighting_of_ref'] = sighting_of_ref
|
||||
|
||||
super(Sighting, self).__init__(**kwargs)
|
|
@ -9,10 +9,10 @@ from .common import (TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, CustomMarking,
|
|||
from .observables import (URL, AlternateDataStream, ArchiveExt, Artifact,
|
||||
AutonomousSystem, CustomExtension, CustomObservable,
|
||||
Directory, DomainName, EmailAddress, EmailMessage,
|
||||
EmailMIMEComponent, File, HTTPRequestExt, ICMPExt,
|
||||
IPv4Address, IPv6Address, MACAddress, Mutex,
|
||||
NetworkTraffic, NTFSExt, PDFExt, Process,
|
||||
RasterImageExt, SocketExt, Software, TCPExt,
|
||||
EmailMIMEComponent, ExtensionsProperty, File,
|
||||
HTTPRequestExt, ICMPExt, IPv4Address, IPv6Address,
|
||||
MACAddress, Mutex, NetworkTraffic, NTFSExt, PDFExt,
|
||||
Process, RasterImageExt, SocketExt, Software, TCPExt,
|
||||
UNIXAccountExt, UserAccount, WindowsPEBinaryExt,
|
||||
WindowsPEOptionalHeaderType, WindowsPESection,
|
||||
WindowsProcessExt, WindowsRegistryKey,
|
||||
|
|
Loading…
Reference in New Issue