300 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			300 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
"""
 | 
						|
Python STIX 2.0 Memory Source/Sink
 | 
						|
 | 
						|
TODO:
 | 
						|
    Use deduplicate() calls only when memory corpus is dirty (been added to)
 | 
						|
    can save a lot of time for successive queries
 | 
						|
 | 
						|
Note:
 | 
						|
    Not worrying about STIX versioning. The in memory STIX data at anytime
 | 
						|
    will only hold one version of a STIX object. As such, when save() is called,
 | 
						|
    the single versions of all the STIX objects are what is written to file.
 | 
						|
 | 
						|
"""
 | 
						|
 | 
						|
import collections
 | 
						|
import io
 | 
						|
import json
 | 
						|
import os
 | 
						|
 | 
						|
from stix2 import Bundle, v20
 | 
						|
from stix2.core import parse
 | 
						|
from stix2.datastore import DataSink, DataSource, DataStoreMixin
 | 
						|
from stix2.datastore.filters import Filter, FilterSet, apply_common_filters
 | 
						|
 | 
						|
 | 
						|
def _add(store, stix_data=None):
 | 
						|
    """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
 | 
						|
 | 
						|
    """
 | 
						|
    if isinstance(stix_data, collections.Mapping):
 | 
						|
        if stix_data['type'] == 'bundle':
 | 
						|
            # adding a json bundle - so just grab STIX objects
 | 
						|
            for stix_obj in stix_data.get('objects', []):
 | 
						|
                _add(store, stix_obj)
 | 
						|
        else:
 | 
						|
            # adding a json STIX object
 | 
						|
            store._data[stix_data['id']] = 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)
 | 
						|
 | 
						|
    else:
 | 
						|
        raise TypeError("stix_data expected to be a python-stix2 object (or list of), JSON formatted STIX (or list of),"
 | 
						|
                        " or a JSON formatted STIX bundle. stix_data was of type: " + str(type(stix_data)))
 | 
						|
 | 
						|
 | 
						|
class MemoryStore(DataStoreMixin):
 | 
						|
    """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
 | 
						|
    be data concurrency issues. As well, just as easy to create new MemoryStore.
 | 
						|
 | 
						|
    Args:
 | 
						|
        stix_data (list OR dict OR STIX object): STIX content to be added
 | 
						|
        allow_custom (bool): whether to allow custom STIX content.
 | 
						|
            Only applied when export/input functions called, i.e.
 | 
						|
            load_from_file() and save_to_file(). Defaults to True.
 | 
						|
 | 
						|
    Attributes:
 | 
						|
        _data (dict): the in-memory dict that holds STIX objects
 | 
						|
        source (MemorySource): MemorySource
 | 
						|
        sink (MemorySink): MemorySink
 | 
						|
 | 
						|
    """
 | 
						|
    def __init__(self, stix_data=None, allow_custom=True):
 | 
						|
        self._data = {}
 | 
						|
 | 
						|
        if stix_data:
 | 
						|
            _add(self, stix_data)
 | 
						|
 | 
						|
        super(MemoryStore, self).__init__(
 | 
						|
            source=MemorySource(stix_data=self._data, allow_custom=allow_custom, _store=True),
 | 
						|
            sink=MemorySink(stix_data=self._data, allow_custom=allow_custom, _store=True),
 | 
						|
        )
 | 
						|
 | 
						|
    def save_to_file(self, *args, **kwargs):
 | 
						|
        """Write SITX objects from in-memory dictionary to JSON file, as a STIX
 | 
						|
        Bundle. If a directory is given, the Bundle 'id' will be used as
 | 
						|
        filename. Otherwise, the provided value will be used.
 | 
						|
 | 
						|
        Args:
 | 
						|
            path (str): file path to write STIX data to.
 | 
						|
            encoding (str): The file encoding. Default utf-8.
 | 
						|
 | 
						|
        """
 | 
						|
        return self.sink.save_to_file(*args, **kwargs)
 | 
						|
 | 
						|
    def load_from_file(self, *args, **kwargs):
 | 
						|
        """Load STIX data from JSON file.
 | 
						|
 | 
						|
        File format is expected to be a single JSON STIX object or JSON STIX
 | 
						|
        bundle.
 | 
						|
 | 
						|
        Args:
 | 
						|
            path (str): file path to load STIX data from
 | 
						|
 | 
						|
        """
 | 
						|
        return self.source.load_from_file(*args, **kwargs)
 | 
						|
 | 
						|
 | 
						|
class MemorySink(DataSink):
 | 
						|
    """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.
 | 
						|
 | 
						|
    Args:
 | 
						|
        stix_data (dict OR list): valid STIX 2.0 content in
 | 
						|
            bundle or a list.
 | 
						|
        _store (bool): whether the MemorySink is a part of a MemoryStore,
 | 
						|
            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
 | 
						|
            when exporting STIX content to file.
 | 
						|
            Default: True.
 | 
						|
 | 
						|
    Attributes:
 | 
						|
        _data (dict): the in-memory dict that holds STIX objects.
 | 
						|
            If part of a MemoryStore, the dict is shared with a MemorySource
 | 
						|
 | 
						|
    """
 | 
						|
    def __init__(self, stix_data=None, allow_custom=True, _store=False):
 | 
						|
        super(MemorySink, self).__init__()
 | 
						|
        self._data = {}
 | 
						|
        self.allow_custom = allow_custom
 | 
						|
 | 
						|
        if _store:
 | 
						|
            self._data = stix_data
 | 
						|
        elif stix_data:
 | 
						|
            _add(self, stix_data)
 | 
						|
 | 
						|
    def add(self, stix_data):
 | 
						|
        _add(self, stix_data)
 | 
						|
    add.__doc__ = _add.__doc__
 | 
						|
 | 
						|
    def save_to_file(self, path, encoding='utf-8'):
 | 
						|
        path = os.path.abspath(path)
 | 
						|
        all_objs = list(self._data.values())
 | 
						|
 | 
						|
        if any('spec_version' in x for x in all_objs):
 | 
						|
            bundle = Bundle(all_objs, allow_custom=self.allow_custom)
 | 
						|
        else:
 | 
						|
            bundle = v20.Bundle(all_objs, allow_custom=self.allow_custom)
 | 
						|
 | 
						|
        if not os.path.exists(os.path.dirname(path)):
 | 
						|
            os.makedirs(os.path.dirname(path))
 | 
						|
 | 
						|
        # if the user only provided a directory, use the bundle id for filename
 | 
						|
        if os.path.isdir(path):
 | 
						|
            path = os.path.join(path, bundle['id'] + '.json')
 | 
						|
 | 
						|
        with io.open(path, 'w', encoding=encoding) as f:
 | 
						|
            bundle = bundle.serialize(pretty=True, encoding=encoding, ensure_ascii=False)
 | 
						|
            f.write(bundle)
 | 
						|
    save_to_file.__doc__ = MemoryStore.save_to_file.__doc__
 | 
						|
 | 
						|
 | 
						|
class MemorySource(DataSource):
 | 
						|
    """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.
 | 
						|
 | 
						|
    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 MemoryStore,
 | 
						|
            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
 | 
						|
            when importing STIX content from file.
 | 
						|
            Default: True.
 | 
						|
 | 
						|
    Attributes:
 | 
						|
        _data (dict): the in-memory dict that holds STIX objects.
 | 
						|
            If part of a MemoryStore, the dict is shared with a MemorySink
 | 
						|
 | 
						|
    """
 | 
						|
    def __init__(self, stix_data=None, allow_custom=True, _store=False):
 | 
						|
        super(MemorySource, self).__init__()
 | 
						|
        self._data = {}
 | 
						|
        self.allow_custom = allow_custom
 | 
						|
 | 
						|
        if _store:
 | 
						|
            self._data = stix_data
 | 
						|
        elif stix_data:
 | 
						|
            _add(self, stix_data)
 | 
						|
 | 
						|
    def get(self, stix_id, _composite_filters=None):
 | 
						|
        """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 (FilterSet): collection of filters passed from the parent
 | 
						|
                CompositeDataSource, not user supplied
 | 
						|
 | 
						|
        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:
 | 
						|
                stix_obj = self._data[stix_id]
 | 
						|
            except KeyError:
 | 
						|
                stix_obj = None
 | 
						|
            return stix_obj
 | 
						|
 | 
						|
        # 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)
 | 
						|
 | 
						|
        if all_data:
 | 
						|
            # reduce to most recent version
 | 
						|
            stix_obj = sorted(all_data, key=lambda k: k['modified'])[0]
 | 
						|
 | 
						|
            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.
 | 
						|
 | 
						|
        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 (FilterSet): collection of filters passed from
 | 
						|
                the parent CompositeDataSource, not user supplied
 | 
						|
 | 
						|
        Returns:
 | 
						|
            (list): list of STIX objects 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
 | 
						|
 | 
						|
        """
 | 
						|
 | 
						|
        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.
 | 
						|
 | 
						|
        A "complete query" includes the filters from the query, the 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 (FilterSet): collection of filters passed from
 | 
						|
                the CompositeDataSource, not user supplied
 | 
						|
 | 
						|
        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.
 | 
						|
 | 
						|
        """
 | 
						|
        query = FilterSet(query)
 | 
						|
 | 
						|
        # combine all query filters
 | 
						|
        if self.filters:
 | 
						|
            query.add(self.filters)
 | 
						|
        if _composite_filters:
 | 
						|
            query.add(_composite_filters)
 | 
						|
 | 
						|
        # Apply STIX common property filters.
 | 
						|
        all_data = list(apply_common_filters(self._data.values(), query))
 | 
						|
 | 
						|
        return all_data
 | 
						|
 | 
						|
    def load_from_file(self, file_path):
 | 
						|
        stix_data = json.load(open(os.path.abspath(file_path), 'r'))
 | 
						|
 | 
						|
        if stix_data['type'] == 'bundle':
 | 
						|
            for stix_obj in stix_data['objects']:
 | 
						|
                _add(self, stix_data=parse(stix_obj, allow_custom=self.allow_custom))
 | 
						|
        else:
 | 
						|
            _add(self, stix_data=parse(stix_data, allow_custom=self.allow_custom))
 | 
						|
    load_from_file.__doc__ = MemoryStore.load_from_file.__doc__
 |