cti-python-stix2/stix2/__init__.py

261 lines
7.7 KiB
Python
Raw Normal View History

import collections
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
# Sentinel value for fields that should be set to the current time.
# We can't use the standard 'default' approach, since if there are multiple
# timestamps in a single object, the timestamps will vary by a few microseconds.
NOW = object()
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
},
2017-02-02 15:33:36 +01:00
'id': {
'default': (lambda x: x._make_id()),
'validate': (lambda x, val: val.startswith(x._type + "--")),
'expected': (lambda x: x._type + "--"),
'error_msg': "{type} {field} values must begin with '{expected}'."
},
'created': {
'default': NOW,
},
'modified': {
'default': NOW,
},
2017-02-01 21:35:41 +01:00
}
2017-01-17 22:58:19 +01:00
def format_datetime(dt):
# 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"
# 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)
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())
def __init__(self, **kwargs):
cls = self.__class__
2017-02-01 20:44:57 +01:00
class_name = cls.__name__
2017-02-01 20:27:24 +01:00
# Use the same timestamp for any auto-generated datetimes
now = datetime.datetime.now(tz=pytz.UTC)
# Detect any keyword arguments not allowed for a specific type
extra_kwargs = list(set(kwargs) - set(cls._properties))
if extra_kwargs:
raise TypeError("unexpected keyword arguments: " + str(extra_kwargs))
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('required'):
msg = "Missing required field for {type}: '{field}'."
raise ValueError(msg.format(type=class_name,
field=prop_name))
2017-02-01 21:51:59 +01:00
if prop_metadata.get('default'):
default = prop_metadata['default']
if default == NOW:
kwargs[prop_name] = now
else:
kwargs[prop_name] = default(cls)
2017-02-01 21:51:59 +01:00
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-02 15:33:36 +01:00
expected=prop_metadata.get('expected',
prop_metadata['default'])(cls),
2017-02-01 21:51:59 +01:00
)
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
self._inner = kwargs
def __getitem__(self, key):
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.")
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-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 = {
2017-02-02 15:33:36 +01:00
# Borrow the 'type' and 'id' definitions
'type': COMMON_PROPERTIES['type'],
'id': COMMON_PROPERTIES['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
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
class Indicator(_STIXBase):
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': {
'required': True,
},
'pattern': {
'required': True,
},
'valid_from': {
'default': NOW,
},
2017-02-01 21:35:41 +01:00
})
2017-01-18 02:25:40 +01:00
def __init__(self, **kwargs):
# TODO:
# - created_by_ref
# - revoked
# - external_references
# - object_marking_refs
# - granular_markings
# - name
# - description
# - valid_until
# - kill_chain_phases
2017-02-01 20:27:24 +01:00
super(Indicator, self).__init__(**kwargs)
2017-01-17 21:37:47 +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': {
'required': True,
},
'name': {
'required': True,
},
2017-02-01 21:35:41 +01:00
})
2017-01-18 02:25:40 +01:00
def __init__(self, **kwargs):
# TODO:
# - created_by_ref
# - revoked
# - external_references
# - object_marking_refs
# - granular_markings
# - description
# - kill_chain_phases
2017-02-01 20:27:24 +01:00
super(Malware, self).__init__(**kwargs)
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': {
'required': True,
},
'source_ref': {
'required': True,
},
'target_ref': {
'required': True,
},
2017-02-01 21:35:41 +01:00
})
2017-01-19 01:10:18 +01:00
# Explicitly define the first three kwargs to make readable Relationship declarations.
def __init__(self, source_ref=None, relationship_type=None, target_ref=None,
**kwargs):
# TODO:
# - created_by_ref
# - revoked
# - external_references
# - object_marking_refs
# - granular_markings
# - description
# 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
# If actual STIX objects (vs. just the IDs) are passed in, extract the
# ID values to use in the Relationship object.
if kwargs.get('source_ref') and isinstance(kwargs['source_ref'], _STIXBase):
kwargs['source_ref'] = kwargs['source_ref'].id
if kwargs.get('target_ref') and isinstance(kwargs['target_ref'], _STIXBase):
kwargs['target_ref'] = kwargs['target_ref'].id
2017-02-01 23:04:20 +01:00
super(Relationship, self).__init__(**kwargs)