Merge pull request #93 from emmanvg/issue-91

Update stix2 Package Structure
stix2.0
Greg Back 2017-11-01 20:09:15 +00:00 committed by GitHub
commit 37e9049536
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 421 additions and 100 deletions

3
MANIFEST.in Normal file
View File

@ -0,0 +1,3 @@
include LICENSE
include CHANGELOG
recursive-exclude stix2\test *

View File

@ -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
----------

237
docs/guide/ts_support.ipynb Normal file
View File

@ -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
}

View File

@ -45,7 +45,7 @@ setup(
'Programming Language :: Python :: 3.6',
],
keywords="stix stix2 json cti cyber threat intelligence",
packages=find_packages(),
packages=find_packages(exclude=['*.test']),
install_requires=[
'python-dateutil',
'pytz',

View File

@ -19,27 +19,10 @@
# flake8: noqa
from . import exceptions
from .common import (TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, CustomMarking,
ExternalReference, GranularMarking, KillChainPhase,
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,
@ -58,9 +41,6 @@ from .patterns import (AndBooleanExpression, AndObservationExpression,
ReferenceObjectPathComponent, RepeatQualifier,
StartStopQualifier, StringConstant, TimestampConstant,
WithinQualifier)
from .sdo import (AttackPattern, Campaign, CourseOfAction, CustomObject,
Identity, Indicator, IntrusionSet, Malware, ObservedData,
Report, ThreatActor, Tool, Vulnerability)
from .sources import CompositeDataSource
from .sources.filesystem import (FileSystemSink, FileSystemSource,
FileSystemStore)
@ -68,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 .v20 import * # This import will always be the latest STIX 2.X version
from .version import __version__
_collect_stix2_obj_maps()
DEFAULT_VERSION = "2.0" # Default version will always be the latest STIX 2.X version

View File

@ -1,16 +1,15 @@
"""STIX 2.0 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, Malware, ObservedData, Report,
STIXDomainObject, ThreatActor, Tool, Vulnerability)
from .sro import Relationship, Sighting, STIXRelationshipObject
from .utils import get_dict
from .utils import get_class_hierarchy_names, get_dict
class STIXObjectProperty(Property):
@ -22,8 +21,8 @@ class STIXObjectProperty(Property):
def clean(self, value):
# Any STIX Object (SDO, SRO, or Marking Definition) can be added to
# a bundle with no further checks.
if isinstance(value, (STIXDomainObject, STIXRelationshipObject,
MarkingDefinition)):
if any(x in ('STIXDomainObject', 'STIXRelationshipObject', 'MarkingDefinition')
for x in get_class_hierarchy_names(value)):
return value
try:
dictified = get_dict(value)
@ -67,37 +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,
'malware': Malware,
'marking-definition': MarkingDefinition,
'observed-data': ObservedData,
'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:
@ -110,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

View File

@ -8,13 +8,10 @@ TODO:
import json
import os
from stix2.common import MarkingDefinition
from stix2.core import Bundle, parse
from stix2.sdo import STIXDomainObject
from stix2.sources import DataSink, DataSource, DataStore
from stix2.sources.filters import Filter, apply_common_filters
from stix2.sro import STIXRelationshipObject
from stix2.utils import deduplicate
from stix2.utils import deduplicate, get_class_hierarchy_names
class FileSystemStore(DataStore):
@ -78,7 +75,7 @@ class FileSystemSink(DataSink):
with open(path, "w") as f:
f.write(str(stix_obj))
def add(self, stix_data=None, allow_custom=False):
def add(self, stix_data=None, allow_custom=False, version=None):
"""Add STIX objects to file directory.
Args:
@ -87,6 +84,8 @@ class FileSystemSink(DataSink):
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
@ -94,12 +93,13 @@ class FileSystemSink(DataSink):
the Bundle contained, but not the Bundle itself.
"""
if isinstance(stix_data, (STIXDomainObject, STIXRelationshipObject, MarkingDefinition)):
if any(x in ('STIXDomainObject', 'STIXRelationshipObject', 'MarkingDefinition')
for x in get_class_hierarchy_names(stix_data)):
# adding python STIX object
self._check_path_and_write(stix_data)
elif isinstance(stix_data, (str, dict)):
stix_data = parse(stix_data, allow_custom)
stix_data = parse(stix_data, allow_custom, version)
if stix_data["type"] == "bundle":
# extract STIX objects
for stix_obj in stix_data.get("objects", []):
@ -146,15 +146,17 @@ class FileSystemSource(DataSource):
def stix_dir(self):
return self._stix_dir
def get(self, stix_id, _composite_filters=None, allow_custom=False):
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.
@ -164,7 +166,8 @@ class FileSystemSource(DataSource):
"""
query = [Filter("id", "=", stix_id)]
all_data = self.query(query=query, _composite_filters=_composite_filters, allow_custom=allow_custom)
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]
@ -173,7 +176,7 @@ class FileSystemSource(DataSource):
return stix_obj
def all_versions(self, stix_id, _composite_filters=None, allow_custom=False):
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
@ -181,10 +184,12 @@ class FileSystemSource(DataSource):
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.
@ -192,9 +197,10 @@ class FileSystemSource(DataSource):
a python STIX objects and then returned
"""
return [self.get(stix_id=stix_id, _composite_filters=_composite_filters, allow_custom=allow_custom)]
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):
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
@ -203,10 +209,12 @@ class FileSystemSource(DataSource):
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
@ -297,7 +305,7 @@ class FileSystemSource(DataSource):
all_data = deduplicate(all_data)
# parse python STIX objects from the STIX object dicts
stix_objs = [parse(stix_obj_dict, allow_custom) 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

View File

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

View File

@ -484,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()

View File

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

View File

@ -247,3 +247,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

43
stix2/v20/__init__.py Normal file
View File

@ -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,
}

View File

@ -2,12 +2,12 @@
from collections import OrderedDict
from .base import _STIXBase
from .markings import _MarkingsMixin
from .properties import (HashesProperty, IDProperty, ListProperty, Property,
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
from ..utils import NOW, get_dict
class ExternalReference(_STIXBase):

View File

@ -7,15 +7,15 @@ Observable and do not have a ``_type`` attribute.
from collections import OrderedDict
from .base import _Extension, _Observable, _STIXBase
from .exceptions import (AtLeastOnePropertyError, DependentPropertiesError,
from ..base import _Extension, _Observable, _STIXBase
from ..exceptions import (AtLeastOnePropertyError, DependentPropertiesError,
ParseError)
from .properties import (BinaryProperty, BooleanProperty, DictionaryProperty,
from ..properties import (BinaryProperty, BooleanProperty, DictionaryProperty,
EmbeddedObjectProperty, EnumProperty, FloatProperty,
HashesProperty, HexProperty, IntegerProperty,
ListProperty, ObjectReferenceProperty, Property,
StringProperty, TimestampProperty, TypeProperty)
from .utils import get_dict
from ..utils import get_dict
class ObservableProperty(Property):

View File

@ -4,14 +4,14 @@ from collections import OrderedDict
import stix2
from .base import _STIXBase
from .common import ExternalReference, GranularMarking, KillChainPhase
from .markings import _MarkingsMixin
from .observables import ObservableProperty
from .properties import (BooleanProperty, IDProperty, IntegerProperty,
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 ..utils import NOW
from .common import ExternalReference, GranularMarking, KillChainPhase
from .observables import ObservableProperty
class STIXDomainObject(_STIXBase, _MarkingsMixin):
@ -358,7 +358,7 @@ def CustomObject(type='x-custom-type', properties=None):
return
raise e
stix2._register_type(_Custom)
stix2._register_type(_Custom, version="2.0")
return _Custom
return custom_builder

View File

@ -2,13 +2,13 @@
from collections import OrderedDict
from .base import _STIXBase
from .common import ExternalReference, GranularMarking
from .markings import _MarkingsMixin
from .properties import (BooleanProperty, IDProperty, IntegerProperty,
from ..base import _STIXBase
from ..markings import _MarkingsMixin
from ..properties import (BooleanProperty, IDProperty, IntegerProperty,
ListProperty, ReferenceProperty, StringProperty,
TimestampProperty, TypeProperty)
from .utils import NOW
from ..utils import NOW
from .common import ExternalReference, GranularMarking
class STIXRelationshipObject(_STIXBase, _MarkingsMixin):