2017-01-18 00:52:03 +01:00
|
|
|
import collections
|
2017-01-19 01:57:26 +01:00
|
|
|
import datetime
|
2017-01-17 22:58:19 +01:00
|
|
|
import json
|
2017-01-17 21:37:47 +01:00
|
|
|
import uuid
|
|
|
|
|
2017-01-17 22:58:19 +01:00
|
|
|
import pytz
|
|
|
|
|
2017-02-01 21:51:59 +01:00
|
|
|
DEFAULT_ERROR = "{type} must have {field}='{expected}'."
|
2017-02-01 21:35:41 +01:00
|
|
|
COMMON_PROPERTIES = {
|
|
|
|
'type': {
|
|
|
|
'default': (lambda x: x._type),
|
2017-02-01 21:51:59 +01:00
|
|
|
'validate': (lambda x, val: val == x._type)
|
2017-02-01 21:35:41 +01:00
|
|
|
},
|
|
|
|
'id': {},
|
|
|
|
'created': {},
|
|
|
|
'modified': {},
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-01-17 22:58:19 +01:00
|
|
|
def format_datetime(dt):
|
2017-01-18 01:53:27 +01:00
|
|
|
# TODO: how to handle naive datetime
|
|
|
|
|
2017-01-17 22:58:19 +01:00
|
|
|
# 1. Convert to UTC
|
|
|
|
# 2. Format in isoformat
|
|
|
|
# 3. Strip off "+00:00"
|
|
|
|
# 4. Add "Z"
|
2017-01-18 01:53:27 +01:00
|
|
|
|
|
|
|
# TODO: how to handle timestamps with subsecond 0's
|
2017-01-17 22:58:19 +01:00
|
|
|
return dt.astimezone(pytz.utc).isoformat()[:-6] + "Z"
|
|
|
|
|
|
|
|
|
2017-02-01 23:04:20 +01:00
|
|
|
class STIXJSONEncoder(json.JSONEncoder):
|
|
|
|
|
|
|
|
def default(self, obj):
|
|
|
|
if isinstance(obj, (datetime.date, datetime.datetime)):
|
|
|
|
return format_datetime(obj)
|
|
|
|
elif isinstance(obj, _STIXBase):
|
|
|
|
return dict(obj)
|
|
|
|
else:
|
|
|
|
return super(STIXJSONEncoder, self).default(obj)
|
|
|
|
|
|
|
|
|
2017-01-18 01:53:27 +01:00
|
|
|
class _STIXBase(collections.Mapping):
|
|
|
|
"""Base class for STIX object types"""
|
2017-01-17 22:58:19 +01:00
|
|
|
|
2017-02-01 20:44:57 +01:00
|
|
|
@classmethod
|
|
|
|
def _make_id(cls):
|
|
|
|
return cls._type + "--" + str(uuid.uuid4())
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _check_kwargs(cls, **kwargs):
|
|
|
|
class_name = cls.__name__
|
2017-02-01 20:27:24 +01:00
|
|
|
|
2017-02-01 21:35:41 +01:00
|
|
|
for prop_name, prop_metadata in cls._properties.items():
|
2017-02-01 21:51:59 +01:00
|
|
|
if prop_name not in kwargs:
|
|
|
|
if prop_metadata.get('default'):
|
|
|
|
kwargs[prop_name] = prop_metadata['default'](cls)
|
|
|
|
elif prop_metadata.get('fixed'):
|
|
|
|
kwargs[prop_name] = prop_metadata['fixed']
|
2017-02-01 21:35:41 +01:00
|
|
|
|
|
|
|
if prop_metadata.get('validate'):
|
|
|
|
if not prop_metadata['validate'](cls, kwargs[prop_name]):
|
2017-02-01 21:51:59 +01:00
|
|
|
msg = prop_metadata.get('error_msg', DEFAULT_ERROR).format(
|
2017-02-01 21:35:41 +01:00
|
|
|
type=class_name,
|
|
|
|
field=prop_name,
|
2017-02-01 21:51:59 +01:00
|
|
|
expected=prop_metadata['default'](cls),
|
|
|
|
)
|
|
|
|
raise ValueError(msg)
|
|
|
|
elif prop_metadata.get('fixed'):
|
|
|
|
if kwargs[prop_name] != prop_metadata['fixed']:
|
|
|
|
msg = prop_metadata.get('error_msg', DEFAULT_ERROR).format(
|
|
|
|
type=class_name,
|
|
|
|
field=prop_name,
|
|
|
|
expected=prop_metadata['fixed']
|
2017-02-01 21:35:41 +01:00
|
|
|
)
|
|
|
|
raise ValueError(msg)
|
2017-02-01 20:27:24 +01:00
|
|
|
|
2017-02-01 20:44:57 +01:00
|
|
|
id_prefix = cls._type + "--"
|
|
|
|
if not kwargs.get('id'):
|
|
|
|
kwargs['id'] = cls._make_id()
|
|
|
|
if not kwargs['id'].startswith(id_prefix):
|
|
|
|
msg = "{0} id values must begin with '{1}'."
|
|
|
|
raise ValueError(msg.format(class_name, id_prefix))
|
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
return kwargs
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
2017-02-01 21:51:59 +01:00
|
|
|
# TODO: move all of this back into init, once we check the right things
|
|
|
|
# in the right order, or move after the unexpected check.
|
|
|
|
kwargs = self._check_kwargs(**kwargs)
|
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
# Detect any keyword arguments not allowed for a specific type
|
|
|
|
extra_kwargs = list(set(kwargs) - set(self.__class__._properties))
|
|
|
|
if extra_kwargs:
|
|
|
|
raise TypeError("unexpected keyword arguments: " + str(extra_kwargs))
|
|
|
|
|
|
|
|
self._inner = kwargs
|
|
|
|
|
2017-01-18 00:03:56 +01:00
|
|
|
def __getitem__(self, key):
|
2017-01-18 00:52:03 +01:00
|
|
|
return self._inner[key]
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
return iter(self._inner)
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
return len(self._inner)
|
|
|
|
|
|
|
|
# Handle attribute access just like key access
|
|
|
|
def __getattr__(self, name):
|
|
|
|
return self.get(name)
|
|
|
|
|
|
|
|
def __setattr__(self, name, value):
|
|
|
|
if name != '_inner':
|
|
|
|
raise ValueError("Cannot modify properties after creation.")
|
2017-01-18 01:53:27 +01:00
|
|
|
super(_STIXBase, self).__setattr__(name, value)
|
|
|
|
|
2017-01-18 01:58:17 +01:00
|
|
|
def __str__(self):
|
|
|
|
# TODO: put keys in specific order. Probably need custom JSON encoder.
|
2017-02-01 23:04:20 +01:00
|
|
|
return json.dumps(self, indent=4, sort_keys=True, cls=STIXJSONEncoder,
|
2017-01-18 01:58:17 +01:00
|
|
|
separators=(",", ": ")) # Don't include spaces after commas.
|
|
|
|
|
2017-01-18 01:53:27 +01:00
|
|
|
|
2017-01-19 01:58:25 +01:00
|
|
|
class Bundle(_STIXBase):
|
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
_type = 'bundle'
|
2017-02-01 21:35:41 +01:00
|
|
|
_properties = {
|
|
|
|
'type': {
|
|
|
|
'default': (lambda x: x._type),
|
2017-02-01 21:51:59 +01:00
|
|
|
'validate': (lambda x, val: val == x._type)
|
2017-02-01 21:35:41 +01:00
|
|
|
},
|
|
|
|
'id': {},
|
2017-02-01 21:51:59 +01:00
|
|
|
'spec_version': {
|
|
|
|
'fixed': "2.0",
|
|
|
|
},
|
2017-02-01 21:35:41 +01:00
|
|
|
'objects': {},
|
|
|
|
}
|
2017-01-19 01:58:25 +01:00
|
|
|
|
2017-02-01 21:57:07 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
# Add any positional arguments to the 'objects' kwarg.
|
|
|
|
if args:
|
|
|
|
kwargs['objects'] = kwargs.get('objects', []) + list(args)
|
2017-01-19 01:58:25 +01:00
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
super(Bundle, self).__init__(**kwargs)
|
2017-01-19 01:58:25 +01:00
|
|
|
|
|
|
|
|
2017-01-18 01:53:27 +01:00
|
|
|
class Indicator(_STIXBase):
|
2017-01-18 00:03:56 +01:00
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
_type = 'indicator'
|
2017-02-01 21:35:41 +01:00
|
|
|
_properties = COMMON_PROPERTIES.copy()
|
|
|
|
_properties.update({
|
|
|
|
'labels': {},
|
|
|
|
'pattern': {},
|
|
|
|
'valid_from': {},
|
|
|
|
})
|
2017-01-18 02:25:40 +01:00
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
2017-01-18 00:52:03 +01:00
|
|
|
# TODO:
|
|
|
|
# - created_by_ref
|
|
|
|
# - revoked
|
|
|
|
# - external_references
|
|
|
|
# - object_marking_refs
|
|
|
|
# - granular_markings
|
|
|
|
|
|
|
|
# - name
|
|
|
|
# - description
|
|
|
|
# - valid_until
|
|
|
|
# - kill_chain_phases
|
|
|
|
|
2017-01-18 02:25:40 +01:00
|
|
|
# TODO: do we care about the performance penalty of creating this
|
|
|
|
# if we won't need it?
|
2017-01-19 01:57:26 +01:00
|
|
|
now = datetime.datetime.now(tz=pytz.UTC)
|
2017-01-17 22:58:19 +01:00
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
# TODO: remove once we check all the fields in the right order
|
|
|
|
kwargs = self._check_kwargs(**kwargs)
|
2017-01-17 23:46:00 +01:00
|
|
|
|
2017-01-18 02:25:40 +01:00
|
|
|
if not kwargs.get('labels'):
|
2017-01-17 23:52:56 +01:00
|
|
|
raise ValueError("Missing required field for Indicator: 'labels'.")
|
|
|
|
|
2017-01-18 02:25:40 +01:00
|
|
|
if not kwargs.get('pattern'):
|
2017-01-17 23:52:56 +01:00
|
|
|
raise ValueError("Missing required field for Indicator: 'pattern'.")
|
2017-01-18 00:52:03 +01:00
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
kwargs.update({
|
2017-01-18 02:25:40 +01:00
|
|
|
'created': kwargs.get('created', now),
|
|
|
|
'modified': kwargs.get('modified', now),
|
|
|
|
'valid_from': kwargs.get('valid_from', now),
|
2017-02-01 20:27:24 +01:00
|
|
|
})
|
|
|
|
super(Indicator, self).__init__(**kwargs)
|
2017-01-17 21:37:47 +01:00
|
|
|
|
2017-01-18 01:53:27 +01:00
|
|
|
|
|
|
|
class Malware(_STIXBase):
|
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
_type = 'malware'
|
2017-02-01 21:35:41 +01:00
|
|
|
_properties = COMMON_PROPERTIES.copy()
|
|
|
|
_properties.update({
|
|
|
|
'labels': {},
|
|
|
|
'name': {},
|
|
|
|
})
|
2017-01-18 02:25:40 +01:00
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
2017-01-18 01:53:27 +01:00
|
|
|
# TODO:
|
|
|
|
# - created_by_ref
|
|
|
|
# - revoked
|
|
|
|
# - external_references
|
|
|
|
# - object_marking_refs
|
|
|
|
# - granular_markings
|
|
|
|
|
|
|
|
# - description
|
|
|
|
# - kill_chain_phases
|
|
|
|
|
2017-01-18 02:25:40 +01:00
|
|
|
# TODO: do we care about the performance penalty of creating this
|
|
|
|
# if we won't need it?
|
2017-01-19 01:57:26 +01:00
|
|
|
now = datetime.datetime.now(tz=pytz.UTC)
|
2017-01-18 01:53:27 +01:00
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
# TODO: remove once we check all the fields in the right order
|
|
|
|
kwargs = self._check_kwargs(**kwargs)
|
|
|
|
|
2017-01-18 02:25:40 +01:00
|
|
|
if not kwargs.get('labels'):
|
2017-01-18 01:53:27 +01:00
|
|
|
raise ValueError("Missing required field for Malware: 'labels'.")
|
|
|
|
|
2017-01-18 02:25:40 +01:00
|
|
|
if not kwargs.get('name'):
|
2017-01-18 01:53:27 +01:00
|
|
|
raise ValueError("Missing required field for Malware: 'name'.")
|
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
kwargs.update({
|
2017-01-18 02:25:40 +01:00
|
|
|
'created': kwargs.get('created', now),
|
|
|
|
'modified': kwargs.get('modified', now),
|
2017-02-01 20:27:24 +01:00
|
|
|
})
|
|
|
|
super(Malware, self).__init__(**kwargs)
|
2017-01-18 01:53:27 +01:00
|
|
|
|
2017-01-18 19:59:28 +01:00
|
|
|
|
|
|
|
class Relationship(_STIXBase):
|
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
_type = 'relationship'
|
2017-02-01 21:35:41 +01:00
|
|
|
_properties = COMMON_PROPERTIES.copy()
|
|
|
|
_properties.update({
|
|
|
|
'relationship_type': {},
|
|
|
|
'source_ref': {},
|
|
|
|
'target_ref': {},
|
|
|
|
})
|
2017-01-18 19:59:28 +01:00
|
|
|
|
2017-01-19 01:10:18 +01:00
|
|
|
# Explicitly define the first three kwargs to make readable Relationship declarations.
|
2017-01-19 00:14:56 +01:00
|
|
|
def __init__(self, source_ref=None, relationship_type=None, target_ref=None,
|
|
|
|
**kwargs):
|
2017-01-18 19:59:28 +01:00
|
|
|
# TODO:
|
|
|
|
# - created_by_ref
|
|
|
|
# - revoked
|
|
|
|
# - external_references
|
|
|
|
# - object_marking_refs
|
|
|
|
# - granular_markings
|
|
|
|
|
|
|
|
# - description
|
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
# TODO: remove once we check all the fields in the right order
|
|
|
|
kwargs = self._check_kwargs(**kwargs)
|
|
|
|
|
2017-01-19 00:14:56 +01:00
|
|
|
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
|
|
|
|
|
2017-01-18 19:59:28 +01:00
|
|
|
# TODO: do we care about the performance penalty of creating this
|
|
|
|
# if we won't need it?
|
2017-01-19 01:57:26 +01:00
|
|
|
now = datetime.datetime.now(tz=pytz.UTC)
|
2017-01-18 19:59:28 +01:00
|
|
|
|
|
|
|
if not kwargs.get('id'):
|
|
|
|
kwargs['id'] = 'relationship--' + str(uuid.uuid4())
|
|
|
|
if not kwargs['id'].startswith('relationship--'):
|
|
|
|
raise ValueError("Relationship id values must begin with 'relationship--'.")
|
|
|
|
|
|
|
|
if not kwargs.get('relationship_type'):
|
|
|
|
raise ValueError("Missing required field for Relationship: 'relationship_type'.")
|
|
|
|
|
|
|
|
if not kwargs.get('source_ref'):
|
|
|
|
raise ValueError("Missing required field for Relationship: 'source_ref'.")
|
2017-01-19 00:14:22 +01:00
|
|
|
elif isinstance(kwargs['source_ref'], _STIXBase):
|
|
|
|
kwargs['source_ref'] = kwargs['source_ref'].id
|
2017-01-18 19:59:28 +01:00
|
|
|
|
|
|
|
if not kwargs.get('target_ref'):
|
|
|
|
raise ValueError("Missing required field for Relationship: 'target_ref'.")
|
2017-01-19 00:14:22 +01:00
|
|
|
elif isinstance(kwargs['target_ref'], _STIXBase):
|
|
|
|
kwargs['target_ref'] = kwargs['target_ref'].id
|
2017-01-18 19:59:28 +01:00
|
|
|
|
2017-02-01 20:27:24 +01:00
|
|
|
kwargs.update({
|
2017-01-18 19:59:28 +01:00
|
|
|
'created': kwargs.get('created', now),
|
|
|
|
'modified': kwargs.get('modified', now),
|
|
|
|
'relationship_type': kwargs['relationship_type'],
|
2017-02-01 20:44:57 +01:00
|
|
|
'target_ref': kwargs['target_ref'],
|
2017-02-01 20:27:24 +01:00
|
|
|
})
|
2017-01-18 19:59:28 +01:00
|
|
|
|
2017-02-01 23:04:20 +01:00
|
|
|
super(Relationship, self).__init__(**kwargs)
|