Repository: oasis-open/cti-taxii-server

Issue:
Updates to DirectoryBackend
This backend could be improved because there are some inconsistencies in its design:
- Some resources are defined in the config document (which drastically add up as you add more api roots, collections, server discovery, etc)
- Although the objects are "cached" many other resources are not like the status and the aforementioned resources. The cache mechanism itself could be improved
- The directory structure could be improved as handling them via bundles is impractical

https://github.com/oasis-open/cti-taxii-server/blob/b9441e14b73eefbd5c3b85c6de7a1c4e4db81a8a/medallion/backends/taxii/directory_backend.py#L1-L342

For example:
``` sh
<root-path>/
     discovery.json
     <api_root1>/
          <api_root1>.json
          status/
               <status_id>.json
          <collection_id>/
               <collection_id>.json
               objects/
                   <type>/
                        <uuid>/
                             <modified | created | request_time>.json
               manifest/
                       <type>/
                            <type>--<uuid>.json

```

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index db18c32..a49706b 100644
--- a/README.rst
+++ b/README.rst
@@ -96,29 +96,26 @@ To use the Memory backend plugin, include the following in the <config-file>:
         }
     }
 
-To use the Directory backend plugin, include the following in the <config-file>:
+To use the directory features, include the following in the <config-file>:
 
 .. code:: json
 
     {
         "backend": {
-            "module": "medallion.backends.taxii.directory_backend",
-            "module_class": "DirectoryBackend",
-            "path": "<path to directory>"
+            "module": "medallion.backends.taxii.memory_backend",
+            "module_class": "MemoryBackend",
+            "path": "<path to directory>",
+            "load_from_path": true
         }
     }
 
-The directory backend config also contains information for certain requests to the TAXII 2.0 server.
 A complete config can be seen in this `example <https:/example_configs/directory_backend_config_auth_from_file.json>`_
 
-The directory backend uses the path pointed to by the path config as its root. Each directory within becomes a TAXII 2.0
+The backend uses the path pointed to by the path config as its root. Each directory within becomes a TAXII 2.0
 api root. STIX 2.0 bundles as JSON files can be placed within the root, and the contents of each file will be aggregated
 into a single collection.
 
-The directory backend caches the contents of the files in memory and is best suited for frequent reads and
-infrequent writes.
-
-To use the Mongo DB backend plugin, include the following in the <config-file>:
+To use the MongoDB backend plugin, include the following in the <config-file>:
 
 .. code:: json
 
diff --git a/example_configs/directory_backend_config_auth_from_file.json b/example_configs/directory_backend_config_auth_from_file.json
index c44d7a7..23772f9 100644
--- a/example_configs/directory_backend_config_auth_from_file.json
+++ b/example_configs/directory_backend_config_auth_from_file.json
@@ -1,32 +1,9 @@
 {
   "backend": {
-    "module": "medallion.backends.taxii.directory_backend",
-    "module_class": "DirectoryBackend",
+    "module": "medallion.backends.taxii.memory_backend",
+    "module_class": "MemoryBackend",
     "path": "./example_configs/directory/",
-    "discovery": {
-      "title": "Some TAXII Server",
-      "description": "This TAXII Server contains a listing of...",
-      "contact": "string containing contact information",
-      "host": "http://localhost:5000/"
-    },
-    "api-root": {
-      "title": "",
-      "description": "",
-      "versions": [
-        "taxii-2.0"
-      ],
-      "max-content-length": 9765625
-    },
-    "collection": {
-      "id": "",
-      "title": "",
-      "description": "",
-      "can_read": true,
-      "can_write": true,
-      "media_types": [
-        "application/vnd.oasis.stix+json; version=2.0"
-      ]
-    }
+    "load_from_path": true
   },
   "taxii": {
     "max_page_size": 100
diff --git a/example_configs/directory_backend_config_auth_from_mongodb.json b/example_configs/directory_backend_config_auth_from_mongodb.json
index b6417ef..8bec624 100644
--- a/example_configs/directory_backend_config_auth_from_mongodb.json
+++ b/example_configs/directory_backend_config_auth_from_mongodb.json
@@ -1,32 +1,9 @@
 {
   "backend": {
-    "module": "medallion.backends.taxii.directory_backend",
-    "module_class": "DirectoryBackend",
+    "module": "medallion.backends.taxii.memory_backend",
+    "module_class": "MemoryBackend",
     "path": "./example_configs/directory/",
-    "discovery": {
-      "title": "Some TAXII Server",
-      "description": "This TAXII Server contains a listing of...",
-      "contact": "string containing contact information",
-      "host": "http://localhost:5000/"
-    },
-    "api-root": {
-      "title": "",
-      "description": "",
-      "versions": [
-        "taxii-2.0"
-      ],
-      "max-content-length": 9765625
-    },
-    "collection": {
-      "id": "",
-      "title": "",
-      "description": "",
-      "can_read": true,
-      "can_write": false,
-      "media_types": [
-        "application/vnd.oasis.stix+json; version=2.0"
-      ]
-    }
+    "load_from_path": true
   },
   "taxii": {
     "max_page_size": 100
diff --git a/medallion/backends/taxii/directory_backend.py b/medallion/backends/taxii/directory_backend.py
deleted file mode 100644
index f6c6146..0000000
--- a/medallion/backends/taxii/directory_backend.py
+++ /dev/null
@@ -1,342 +0,0 @@
-import datetime
-import json
-import os
-import uuid
-
-from medallion.backends.taxii.base import Backend
-from medallion.exceptions import ProcessingError
-from medallion.filters.basic_filter import BasicFilter
-from medallion.utils.common import create_bundle, generate_status
-
-
-class DirectoryBackend(Backend):
-    # access control is handled at the views level
-
-    def __init__(self, path=None, **kwargs):
-        self.path = path
-        self.discovery_config = self.init_discovery_config(kwargs.get('discovery', None))
-        self.api_root_config = self.init_api_root_config(kwargs.get('api-root', None))
-        self.collection_config = self.init_collection_config(kwargs.get('collection', None))
-        self.cache = {}
-        self.statuses = []
-
-    # noinspection PyMethodMayBeStatic
-    def init_discovery_config(self, discovery_config):
-        if not self.path:
-            raise ProcessingError('path was not specified in the config file', 400)
-
-        if not os.path.isdir(self.path):
-            raise ProcessingError("directory '{}' was not found".format(self.path), 500)
-
-        return discovery_config
-
-    def update_discovery_config(self):
-        dc = self.discovery_config
-        collection_dirs = sorted([f for f in os.listdir(self.path) if os.path.isdir(os.path.join(self.path, f))])
-
-        if not collection_dirs:
-            raise ProcessingError('at least one api-root directory is required', 500)
-
-        updated_roots = ['{}{}/'.format(dc['host'], f) for f in collection_dirs]
-
-        self.discovery_config['default'] = updated_roots[0]
-        self.discovery_config['api_roots'] = updated_roots
-
-    # noinspection PyMethodMayBeStatic
-    def init_api_root_config(self, api_root_config):
-        if api_root_config:
-            return api_root_config
-        else:
-            raise ProcessingError('api-root was not specified in the config file', 400)
-
-    # noinspection PyMethodMayBeStatic
-    def init_collection_config(self, collection_config):
-        if collection_config:
-            return collection_config
-        else:
-            raise ProcessingError('collection was not specified in the config file', 400)
-
-    def validate_requested_api_root(self, requested_api_root):
-        api_roots = self.discovery_config['api_roots']
-
-        host_port = self.discovery_config['default'].rsplit('/', 2)[0]
-
-        full_api_root = '{}/{}/'.format(host_port, requested_api_root)
-
-        return full_api_root in api_roots
-
-    def server_discovery(self):
-        self.update_discovery_config()
-        return self.discovery_config
-
-    def get_api_root_information(self, api_root):
-        self.update_discovery_config()
-
-        api_roots = self.discovery_config['api_roots']
-
-        for r in api_roots:
-            c_dir = r.rsplit('/', 2)[1]
-
-            if api_root == c_dir:
-                i_title = "Indicators from directory '{}'".format(c_dir)
-
-                i = {
-                    "title": i_title,
-                    "description": "",
-                    "versions": self.api_root_config['versions'],
-                    "max-content-length": self.api_root_config['max-content-length']
-                }
-
-                return i
-
-    def get_collections(self, api_root, start_index, end_index):
-        self.update_discovery_config()
-
-        api_roots = self.discovery_config['api_roots']
-
-        collections = []
-
-        # Generate a collection object for each api_root
-        for r in api_roots:
-            c_dir = r.rsplit('/', 2)[1]
-
-            if api_root == c_dir:
-                c_id = uuid.uuid3(uuid.NAMESPACE_URL, r)
-                c_title = "Indicators from directory '{}'".format(c_dir)
-
-                c = {
-                    "id": str(c_id),
-                    "title": c_title,
-                    "description": self.collection_config['description'],
-                    "can_read": self.collection_config['can_read'],
-                    "can_write": self.collection_config['can_write'],
-                    "media_types": self.collection_config['media_types']
-                }
-
-                collections.append(c)
-
-        count = len(collections)
-
-        collections = collections if end_index == -1 else collections[start_index:end_index]
-
-        return count, collections
-
-    def get_collection(self, api_root, collection_id):
-        count, collections = self.get_collections(api_root, 0, -1)
-
-        for c in collections:
-            if 'id' in c and collection_id == c['id']:
-                return c
-
-    def set_modified_time_stamp(self, objects, modified):
-        for o in objects:
-            o['modified'] = modified
-
-        return objects
-
-    def get_modified_time_stamp(self, fp):
-        fp_modified = os.path.getmtime(fp)
-        dt = datetime.datetime.utcfromtimestamp(fp_modified)
-        modified = '{:%Y-%m-%dT%H:%M:%S.%fZ}'.format(dt)
-
-        return modified
-
-    def delete_from_cache(self, api_root):
-        p = os.path.join(self.path, api_root)
-        files = [f for f in os.listdir(p) if os.path.isfile(os.path.join(p, f)) and f.endswith('.json')]
-
-        for f in self.cache[api_root]['files'].keys():
-            if f not in files:
-                del self.cache[api_root]['files'][f]
-
-    def add_to_cache(self, api_root, api_root_modified, file_name, file_modified):
-        fp = os.path.join(self.path, api_root, file_name)
-
-        u_objects = []
-
-        with open(fp, 'r') as raw_json:
-            try:
-                stix2 = json.load(raw_json)
-
-                if stix2.get('type', '') == 'bundle' and stix2.get('spec_version', '') == '2.0':
-                    objects = stix2.get('objects', [])
-                    u_objects = self.set_modified_time_stamp(objects, file_modified)
-
-                    if api_root not in self.cache:
-                        self.cache[api_root] = {'modified': '', 'files': {}}
-
-                    self.cache[api_root]['modified'] = api_root_modified
-                    self.cache[api_root]['files'][file_name] = {'modified': file_modified, 'objects': u_objects}
-            except Exception as e:
-                raise ProcessingError('error adding objects to cache', 500, e)
-            finally:
-                return u_objects
-
-    def with_cache(self, api_root):
-        api_root_path = os.path.join(self.path, api_root)
-        api_root_modified = self.get_modified_time_stamp(api_root_path)
-
-        if api_root in self.cache:
-            if self.cache[api_root]['modified'] == api_root_modified:
-                # Return objects from cache
-                objects = []
-                for k, v in self.cache[api_root]['files'].items():
-                    objects.extend(v['objects'])
-                return objects
-            else:
-                # Cleanup the cache
-                self.delete_from_cache(api_root)
-
-                # Add to the cache and return objects for collection
-                dir_list = os.listdir(api_root_path)
-                files = [f for f in dir_list if os.path.isfile(os.path.join(api_root_path, f)) and f.endswith('.json')]
-
-                objects = []
-                for f in files:
-                    fp = os.path.join(api_root_path, f)
-                    file_modified = self.get_modified_time_stamp(fp)
-
-                    cached_files = self.cache[api_root]['files']
-                    if f in cached_files and cached_files[f]['modified'] == file_modified:
-                        objects.extend(cached_files[f]['objects'])
-                    else:
-                        u_objects = self.add_to_cache(api_root, api_root_modified, f, file_modified)
-                        objects.extend(u_objects)
-                return objects
-        else:
-            # Update the cache and return the objects for the collection
-            dir_list = os.listdir(api_root_path)
-            files = [f for f in dir_list if os.path.isfile(os.path.join(api_root_path, f)) and f.endswith('.json')]
-
-            objects = []
-            for f in files:
-                fp = os.path.join(api_root_path, f)
-                file_modified = self.get_modified_time_stamp(fp)
-
-                u_objects = self.add_to_cache(api_root, api_root_modified, f, file_modified)
-                objects.extend(u_objects)
-            return objects
-
-    def get_objects_without_bundle(self, api_root, collection_id, filter_args, allowed_filters):
-        self.update_discovery_config()
-
-        if self.validate_requested_api_root(api_root):
-            # Get the collection
-            collection = None
-            num_collections, collections = self.get_collections(api_root, 0, -1)
-
-            for c in collections:
-                if 'id' in c and collection_id == c['id']:
-                    collection = c
-                    break
-
-            if not collection:
-                raise ProcessingError("collection for api-root '{}' was not found".format(api_root), 500)
-
-            # Add the objects to the collection
-            collection['objects'] = self.with_cache(api_root)
-
-            # Filter the collection
-            filtered_objects = []
-
-            if filter_args:
-                full_filter = BasicFilter(filter_args)
-                filtered_objects.extend(
-                    full_filter.process_filter(
-                        collection.get('objects', []),
-                        allowed_filters,
-                        collection.get('manifest', [])
-                    )
-                )
-            else:
-                filtered_objects.extend(collection.get('objects', []))
-
-            return filtered_objects
-
-    def get_objects(self, api_root, collection_id, filter_args, allowed_filters, start_index, end_index):
-        # print('start_index: {}, end_index: {}'.format(start_index, end_index))
-
-        objects = self.get_objects_without_bundle(api_root, collection_id, filter_args, allowed_filters)
-
-        objects.sort(key=lambda x: datetime.datetime.strptime(x['modified'], '%Y-%m-%dT%H:%M:%S.%fZ'))
-
-        count = len(objects)
-
-        objects = objects if end_index == -1 else objects[start_index:end_index]
-
-        return count, create_bundle(objects)
-
-    def get_object(self, api_root, collection_id, object_id, filter_args, allowed_filters):
-        objects = self.get_objects_without_bundle(api_root, collection_id, filter_args, allowed_filters)
-
-        req_object = [i for i in objects if i['id'] == object_id]
-
-        if len(req_object) == 1:
-            return create_bundle(req_object)
-
-    def get_object_manifest(self, api_root, collection_id, filter_args, allowed_filters, start_index, end_index):
-        self.update_discovery_config()
-
-        if self.validate_requested_api_root(api_root):
-            count, collections = self.get_collections(api_root, 0, -1)
-
-            for collection in collections:
-                if 'id' in collection and collection_id == collection['id']:
-                    manifest = collection.get('manifest', [])
-                    if filter_args:
-                        full_filter = BasicFilter(filter_args)
-                        manifest = full_filter.process_filter(
-                            manifest,
-                            allowed_filters,
-                            None
-                        )
-
-                    count = len(manifest)
-
-                    manifest = manifest if end_index == -1 else manifest[start_index:end_index]
-
-                    return count, manifest
-
-    def add_objects(self, api_root, collection_id, objs, request_time):
-        failed = 0
-        succeeded = 0
-        pending = 0
-        successes = []
-        failures = []
-
-        file_name = '{}--{}.{}'.format(request_time, objs['id'], 'json')
-        p = os.path.join(self.path, api_root)
-        fp = os.path.join(p, file_name)
-
-        try:
-            add_objs = objs['objects']
-            num_objs = len(add_objs)
-
-            try:
-                # Each add_object request writes the provided bundle to a new file
-                with open(fp, 'w') as out_file:
-                    out_file.write(json.dumps(objs, indent=4, sort_keys=True))
-
-                    succeeded += num_objs
-                    successes = list(map(lambda x: x['id'], add_objs))
-
-                # Update the cache after the file is written
-                self.with_cache(api_root)
-            except IOError:
-                failed += num_objs
-                failures = list(map(lambda x: x['id'], add_objs))
-
-        except Exception as e:
-            raise ProcessingError('error adding objects', 500, e)
-
-        status = generate_status(request_time, 'complete', succeeded, failed,
-                                 pending, successes_ids=successes, failures=failures)
-
-        self.statuses.append(status)
-
-        return status
-
-    def get_status(self, api_root, status_id):
-        for s in self.statuses:
-            if status_id == s['id']:
-                return s
diff --git a/medallion/backends/taxii/memory_backend.py b/medallion/backends/taxii/memory_backend.py
index 198ecda..15d0ab9 100644
--- a/medallion/backends/taxii/memory_backend.py
+++ b/medallion/backends/taxii/memory_backend.py
@@ -1,10 +1,19 @@
+import codecs
 import copy
 import json
+import logging
+import os
+import re
 
 from medallion.backends.taxii.base import Backend
 from medallion.exceptions import ProcessingError
 from medallion.filters.basic_filter import BasicFilter
-from medallion.utils.common import create_bundle, generate_status, iterpath
+from medallion.utils.common import (create_bundle, determine_spec_version,
+                                    determine_version, format_datetime,
+                                    generate_status, iterpath)
+
+# Module-level logger
+log = logging.getLogger(__name__)
 
 
 class MemoryBackend(Backend):
@@ -13,10 +22,95 @@ class MemoryBackend(Backend):
 
     def __init__(self, **kwargs):
         filename = kwargs.get("filename")
+        self.data = {}
+        self.save_status = kwargs.get("save_status", False)
+        self.save_resources = kwargs.get("save_resources", False)
+        self.path = kwargs.get("path")
+        self.load_from_path = kwargs.get("load_from_path", False)
+        if filename and self.load_from_path:
+            raise RuntimeError("Currently is not possible to load from various sources simultaneously.")
         if filename:
             self.load_data_from_file(filename)
-        else:
-            self.data = {}
+        if self.path and self.load_from_path:
+            self._init_discovery()
+
+    def _init_discovery(self):
+        if not self.path:
+            raise ProcessingError('path was not specified in the config file', 400)
+
+        if os.path.isdir(self.path) is False:
+            raise ProcessingError("directory '{}' was not found".format(self.path), 500)
+
+        discovery_path = os.path.join(self.path, "discovery.json")
+        self.data["/discovery"] = self._load_file(discovery_path)
+
+        for dirpath, dirnames, filenames in os.walk(self.path):
+            for dirname in dirnames:
+                filename = dirname + ".json"
+                file_path = os.path.join(dirpath, dirname, filename)
+                if os.path.isfile(file_path):
+                    self.data[dirname] = {}
+                    self.data[dirname]["information"] = self._load_file(file_path)
+                    self.data[dirname]["collections"] = []
+                    self.data[dirname]["status"] = []
+                    self._init_collections(dirname, os.path.join(dirpath, dirname))
+            break
+
+    def _init_collections(self, api_root, apiroot_path):
+        for dirpath, dirnames, filenames in os.walk(apiroot_path):
+            for dirname in dirnames:
+                filename = dirname + ".json"
+                file_path = os.path.join(dirpath, dirname, filename)
+
+                if os.path.isfile(file_path):
+                    collection = self._load_file(file_path)
+                    collection["manifest"] = []
+                    collection["objects"] = []
+
+                    self.data[api_root]["collections"].append(collection)
+                    self._init_resource(collection, os.path.join(dirpath, dirname), "objects")
+                    self._init_resource(collection, os.path.join(dirpath, dirname), "manifest")
+
+                if dirname == "status":
+                    self._init_status(api_root, os.path.join(dirpath, "status"))
+            break
+
+    def _init_resource(self, collection, collection_path, resource):
+        collection_path = os.path.join(collection_path, resource)
+
+        for dirpath, dirnames, filenames in os.walk(collection_path):
+            for filename in filenames:
+                file_path = os.path.join(dirpath, filename)
+
+                if os.path.isfile(file_path):
+                    loaded_obj = self._load_file(file_path)
+                    collection[resource].append(loaded_obj)
+
+    def _init_status(self, api_root, status_path):
+        for dirpath, dirnames, filenames in os.walk(status_path):
+            for filename in filenames:
+                file_path = os.path.join(dirpath, filename)
+                self.data[api_root]["status"].append(self._load_file(file_path))
+
+    @staticmethod
+    def _timestamp2filename(timestamp):
+        """Takes timestamp string and removes some characters for normalized name"""
+        ts = re.sub(r"[-T:\.Z ]", "", timestamp)
+        return ts
+
+    @staticmethod
+    def _save_file(obj, filename, *paths):
+        final_path = os.path.join(*paths)
+        if os.path.isdir(final_path) is False:
+            os.makedirs(final_path)
+        with codecs.open(os.path.join(final_path, filename), mode="w", encoding="utf8") as infile:
+            json.dump(obj, infile, ensure_ascii=False, separators=(",", ":"))
+
+    @staticmethod
+    def _load_file(*paths):
+        final_path = os.path.join(*paths)
+        with codecs.open(final_path, mode="r", encoding="utf8") as infile:
+            return json.load(infile)
 
     def load_data_from_file(self, filename):
         with open(filename, "r") as infile:
@@ -38,9 +132,17 @@ class MemoryBackend(Backend):
     def _update_manifest(self, new_obj, api_root, collection_id, request_time):
         api_info = self._get(api_root)
         collections = api_info.get("collections", [])
+        media_type_fmt = "application/vnd.oasis.stix+json; version={}"
+        media_type = media_type_fmt.format(determine_spec_version(new_obj))
+        version = determine_version(new_obj, request_time)
+
+        obj_type, obj_id = new_obj["id"].split("--")
+        json_name = self._timestamp2filename(version) + ".json"
 
         for collection in collections:
-            if "id" in collection and collection_id == collection["id"]:
+            if collection_id == collection["id"]:
+                if "manifest" not in collection:
+                    collection["manifest"] = []
                 for entry in collection["manifest"]:
                     if new_obj["id"] == entry["id"]:
                         if "modified" in new_obj:
@@ -51,15 +153,30 @@ class MemoryBackend(Backend):
                         # to do.
                         break
                 else:
-                    version = new_obj.get("modified", new_obj["created"])
-                    collection["manifest"].append(
-                        {
-                            "id": new_obj["id"],
-                            "date_added": request_time,
-                            "versions": [version],
-                            "media_types": ["application/vnd.oasis.stix+json; version=2.0"],
-                        },
-                    )  # media_types hardcoded for now...
+                    entry = {
+                        "id": new_obj["id"],
+                        "date_added": format_datetime(request_time),
+                        "versions": [version],
+                        "media_types": [media_type],
+                    }
+                    collection["manifest"].append(entry)
+                    if self.save_resources:
+                        self._save_file(entry, json_name, self.path, api_root, collection_id, "manifest", obj_type, obj_id)
+                if self.save_resources:
+                    self._save_file(new_obj, json_name, self.path, api_root, collection_id, "objects", obj_type, obj_id)
+
+                # if the media type is new, attach it to the collection
+                if media_type not in collection["media_types"]:
+                    collection["media_types"].append(media_type)
+                    collection_cp = copy.deepcopy(collection)
+                    collection_cp.pop("manifest", None)
+                    collection_cp.pop("modified", None)
+                    collection_cp.pop("objects", None)
+
+                    json_name = collection["id"] + ".json"
+                    if self.save_resources:
+                        self._save_file(collection_cp, json_name, self.path, api_root, collection_id)
+
                 # quit once you have found the collection that needed updating
                 break
 
@@ -172,8 +289,6 @@ class MemoryBackend(Backend):
                         for new_obj in objs["objects"]:
                             id_and_version_already_present = False
                             for obj in collection["objects"]:
-                                id_and_version_already_present = False
-
                                 if new_obj["id"] == obj["id"]:
                                     if "modified" in new_obj:
                                         if new_obj["modified"] == obj["modified"]:
@@ -181,23 +296,30 @@ class MemoryBackend(Backend):
                                     else:
                                         # There is no modified field, so this object is immutable
                                         id_and_version_already_present = True
-                            if not id_and_version_already_present:
+                                    break
+                            if id_and_version_already_present is False:
                                 collection["objects"].append(new_obj)
                                 self._update_manifest(new_obj, api_root, collection["id"], request_time)
                                 successes.append(new_obj["id"])
                                 succeeded += 1
                             else:
-                                failures.append({"id": new_obj["id"], "message": "Unable to process object"})
+                                failures.append({
+                                    "id": new_obj["id"],
+                                    "message": "Unable to process object because identical version exist."
+                                })
                                 failed += 1
                     except Exception as e:
+                        log.exception(e)
                         raise ProcessingError("While processing supplied content, an error occurred", 422, e)
 
             status = generate_status(
-                request_time, "complete", succeeded,
+                format_datetime(request_time), "complete", succeeded,
                 failed, pending, successes_ids=successes,
                 failures=failures,
             )
             api_info["status"].append(status)
+            if self.save_status:
+                self._save_file(status, status["id"] + ".json", self.path, api_root, "status")
             return status
 
     def get_object(self, api_root, id_, object_id, filter_args, allowed_filters):
diff --git a/medallion/utils/common.py b/medallion/utils/common.py
index 16fb458..8df6852 100644
--- a/medallion/utils/common.py
+++ b/medallion/utils/common.py
@@ -21,6 +21,23 @@ def create_bundle(o):
     }
 
 
+def determine_version(new_obj, request_time):
+    """Grab the modified time if present, if not grab created time,
+    if not grab request time provided by server."""
+    return new_obj.get("modified", new_obj.get("created", format_datetime(request_time)))
+
+
+def determine_spec_version(obj):
+    """Given a STIX 2.x object, determine its spec version."""
+    missing = ("created", "modified")
+    if all(x not in obj for x in missing):
+        # Special case: only SCOs are 2.1 objects and they don't have a spec_version
+        # For now the only way to identify one is checking the created and modified
+        # are both missing.
+        return "2.1"
+    return obj.get("spec_version", "2.0")
+
+
 def get(data, key):
     for ancestors, item in iterpath(data):
         if key in ancestors:
@@ -81,22 +98,19 @@ def get_timestamp():
 
 
 def format_datetime(dttm):
+    """Given a datetime instance, produce the string representation
+    with microsecond precision"""
     # 1. Convert to timezone-aware
     # 2. Convert to UTC
-    # 3. Format in ISO format
-    # 4. Add subsecond value if non-zero
-    # 5. Add "Z"
+    # 3. Format in ISO format with microsecond precision
 
     if dttm.tzinfo is None or dttm.tzinfo.utcoffset(dttm) is None:
         # dttm is timezone-naive; assume UTC
         zoned = pytz.UTC.localize(dttm)
     else:
         zoned = dttm.astimezone(pytz.UTC)
-    ts = zoned.strftime("%Y-%m-%dT%H:%M:%S")
-    if zoned.microsecond > 0:
-        ms = zoned.strftime("%f")
-        ts = ts + "." + ms.rstrip("0")
-    return ts + "Z"
+    ts = zoned.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+    return ts
 
 
 def convert_to_stix_datetime(timestamp_string):
diff --git a/medallion/views/objects.py b/medallion/views/objects.py
index 7e4e260..433169d 100644
--- a/medallion/views/objects.py
+++ b/medallion/views/objects.py
@@ -5,7 +5,7 @@ from flask import Blueprint, Response, current_app, json, request
 from . import MEDIA_TYPE_STIX_V20, MEDIA_TYPE_TAXII_V20
 from .. import auth
 from ..exceptions import ProcessingError
-from ..utils.common import format_datetime, get_timestamp
+from ..utils.common import get_timestamp
 
 mod = Blueprint("objects", __name__)
 
@@ -96,7 +96,7 @@ def get_response_status_and_headers(start_index, total_count, objects):
 @auth.login_required
 def get_or_add_objects(api_root, id_):
     # TODO: Check if user has access to read or write objects in collection - right now just check for permissions on the collection.
-    request_time = format_datetime(get_timestamp())  # Can't I get this from the request itself?
+    request_time = get_timestamp()  # Can't I get this from the request itself?
     if collection_exists(api_root, id_):
         if request.method == "GET" and permission_to_read(api_root, id_):
             start_index, end_index = get_range_request_from_headers()


Test patch:
diff --git a/medallion/test/__init__.py b/medallion/test/__init__.py
index 615879f..4929af5 100644
--- a/medallion/test/__init__.py
+++ b/medallion/test/__init__.py
@@ -13,7 +13,7 @@ MANIFESTS_EP = GET_ADD_COLLECTION_EP + "manifest/"
 OBJECTS_EP = GET_ADD_COLLECTION_EP + "objects/"
 GET_OBJECT_EP = GET_OBJECTS_EP = ADD_OBJECTS_EP = OBJECTS_EP
 
-GET_COLLECTION_EP_FOR_DIRECTORY_BACKEND = COLLECTIONS_EP + "46bb17fa-0af3-3446-a570-b55cdfdc7881/"
+GET_COLLECTION_EP_FOR_DIRECTORY_BACKEND = COLLECTIONS_EP + "91a7b528-80eb-42ed-a74d-c6fbd5a26116/"
 GET_OBJECTS_FROM_DIRECTORY_BACKEND_EP_NOT_EXISTANT = COLLECTIONS_EP + "indicator--d772d620-9720-4457-897d-7030d1a972fd/"
 GET_OBJECTS_FROM_DIRECTORY_BACKEND_EP = GET_COLLECTION_EP_FOR_DIRECTORY_BACKEND + "objects/"
 GET_OBJECTS_FROM_DIRECTORY_BACKEND_EP_NOT_EXISTANT_OBJECTS = \
diff --git a/medallion/test/config.py b/medallion/test/config.py
index 2d0c784..111bc29 100644
--- a/medallion/test/config.py
+++ b/medallion/test/config.py
@@ -36,33 +36,11 @@ def memory_config(data_file):
 
 def directory_config():
     return _base_config({
-        "module": "medallion.backends.taxii.directory_backend",
-        "module_class": "DirectoryBackend",
+        "module": "medallion.backends.taxii.memory_backend",
+        "module_class": "MemoryBackend",
         "path": "./medallion/test/directory/",
-        "discovery": {
-            "title": "Indicators from the directory backend",
-            "description": "Indicators from the directory backend",
-            "contact": "",
-            "host": "http://localhost:5000/"
-        },
-        "api-root": {
-            "title": "",
-            "description": "",
-            "versions": [
-                "taxii-2.0"
-            ],
-            "max-content-length": 9765625
-        },
-        "collection": {
-            "id": "",
-            "title": "",
-            "description": "",
-            "can_read": True,
-            "can_write": True,
-            "media_types": [
-                "application/vnd.oasis.stix+json; version=2.0"
-            ]
-        }
+        "load_from_path": True,
+        "save_resources": True
     }, {
         "module": "medallion.backends.auth.memory_auth",
         "module_class": "AuthMemoryBackend",
diff --git a/medallion/test/directory/discovery.json b/medallion/test/directory/discovery.json
new file mode 100644
index 0000000..366a302
--- /dev/null
+++ b/medallion/test/directory/discovery.json
@@ -0,0 +1,11 @@
+{
+    "title": "Some TAXII Server",
+    "description": "This TAXII Server contains a listing of...",
+    "contact": "string containing contact information ®",
+    "default": "http://localhost:5000/trustgroup1/",
+    "api_roots": [
+        "http://localhost:5000/api1/",
+        "http://localhost:5000/api2/",
+        "http://localhost:5000/trustgroup1/"
+    ]
+}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/91a7b528-80eb-42ed-a74d-c6fbd5a26116.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/91a7b528-80eb-42ed-a74d-c6fbd5a26116.json
new file mode 100644
index 0000000..e3e85e8
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/91a7b528-80eb-42ed-a74d-c6fbd5a26116.json
@@ -0,0 +1,1 @@
+{"id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", "title": "High Value Indicator Collection", "description": "This data collection is for collecting high value IOCs", "can_read": true, "can_write": true, "media_types": ["application/vnd.oasis.stix+json; version=2.0", "application/vnd.oasis.stix+json; version=2.1"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/attack-pattern/20138b9d-1aac-4a26-8654-a36b6bbf2bba/20190116173959787.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/attack-pattern/20138b9d-1aac-4a26-8654-a36b6bbf2bba/20190116173959787.json
new file mode 100644
index 0000000..fa56b79
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/attack-pattern/20138b9d-1aac-4a26-8654-a36b6bbf2bba/20190116173959787.json
@@ -0,0 +1,1 @@
+{"id":"attack-pattern--20138b9d-1aac-4a26-8654-a36b6bbf2bba","date_added":"2019-10-29T19:12:15.038371Z","versions":["2019-01-16T17:39:59.787Z"],"media_types":["application/vnd.oasis.stix+json; version=2.0"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/attack-pattern/389735f1-f21c-4208-b8f0-f8031e7169b8/20190116173939364.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/attack-pattern/389735f1-f21c-4208-b8f0-f8031e7169b8/20190116173939364.json
new file mode 100644
index 0000000..2f7e4b2
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/attack-pattern/389735f1-f21c-4208-b8f0-f8031e7169b8/20190116173939364.json
@@ -0,0 +1,1 @@
+{"id":"attack-pattern--389735f1-f21c-4208-b8f0-f8031e7169b8","date_added":"2019-10-29T19:12:15.038371Z","versions":["2019-01-16T17:39:39.364Z"],"media_types":["application/vnd.oasis.stix+json; version=2.0"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/campaign/b3e138d1-6ef7-4bdc-924a-285f588330d7/20190626134606887.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/campaign/b3e138d1-6ef7-4bdc-924a-285f588330d7/20190626134606887.json
new file mode 100644
index 0000000..6077ad2
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/campaign/b3e138d1-6ef7-4bdc-924a-285f588330d7/20190626134606887.json
@@ -0,0 +1,1 @@
+{"id":"campaign--b3e138d1-6ef7-4bdc-924a-285f588330d7","date_added":"2019-10-29T19:12:15.038371Z","versions":["2019-06-26T13:46:06.887Z"],"media_types":["application/vnd.oasis.stix+json; version=2.0"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/indicator/7d72d620-9720-4457-897d-7030d1a972df/20190625182415157.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/indicator/7d72d620-9720-4457-897d-7030d1a972df/20190625182415157.json
new file mode 100644
index 0000000..423428c
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/indicator/7d72d620-9720-4457-897d-7030d1a972df/20190625182415157.json
@@ -0,0 +1,1 @@
+{"id":"indicator--7d72d620-9720-4457-897d-7030d1a972df","date_added":"2019-10-29T19:12:15.038371Z","versions":["2019-06-25T18:24:15.157Z"],"media_types":["application/vnd.oasis.stix+json; version=2.0"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/intrusion-set/f7fa709b-a740-4bdc-b48c-3ea69ed39cdf/20190626134606887.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/intrusion-set/f7fa709b-a740-4bdc-b48c-3ea69ed39cdf/20190626134606887.json
new file mode 100644
index 0000000..0c85066
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/intrusion-set/f7fa709b-a740-4bdc-b48c-3ea69ed39cdf/20190626134606887.json
@@ -0,0 +1,1 @@
+{"id":"intrusion-set--f7fa709b-a740-4bdc-b48c-3ea69ed39cdf","date_added":"2019-10-29T19:12:15.038371Z","versions":["2019-06-26T13:46:06.887Z"],"media_types":["application/vnd.oasis.stix+json; version=2.0"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/20138b9d-1aac-4a26-8654-a36b6bbf2bba/20190626134606887.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/20138b9d-1aac-4a26-8654-a36b6bbf2bba/20190626134606887.json
new file mode 100644
index 0000000..f30c073
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/20138b9d-1aac-4a26-8654-a36b6bbf2bba/20190626134606887.json
@@ -0,0 +1,1 @@
+{"id":"relationship--20138b9d-1aac-4a26-8654-a36b6bbf2bba","date_added":"2019-10-29T19:12:15.038371Z","versions":["2019-06-26T13:46:06.887Z"],"media_types":["application/vnd.oasis.stix+json; version=2.0"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/29e639c6-4bcf-49c7-b637-3bce69b5e1f6/20190626134606887.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/29e639c6-4bcf-49c7-b637-3bce69b5e1f6/20190626134606887.json
new file mode 100644
index 0000000..682d490
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/29e639c6-4bcf-49c7-b637-3bce69b5e1f6/20190626134606887.json
@@ -0,0 +1,1 @@
+{"id":"relationship--29e639c6-4bcf-49c7-b637-3bce69b5e1f6","date_added":"2019-10-29T19:12:15.038371Z","versions":["2019-06-26T13:46:06.887Z"],"media_types":["application/vnd.oasis.stix+json; version=2.0"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/5d399937-d736-413f-a6ec-f77cdf824032/20190626134448412.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/5d399937-d736-413f-a6ec-f77cdf824032/20190626134448412.json
new file mode 100644
index 0000000..a552ce1
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/5d399937-d736-413f-a6ec-f77cdf824032/20190626134448412.json
@@ -0,0 +1,1 @@
+{"id":"relationship--5d399937-d736-413f-a6ec-f77cdf824032","date_added":"2019-10-29T19:12:15.038371Z","versions":["2019-06-26T13:44:48.412Z"],"media_types":["application/vnd.oasis.stix+json; version=2.0"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/a5fa69e1-bcb4-43a6-b7d6-eda102017f77/20190626134606886.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/a5fa69e1-bcb4-43a6-b7d6-eda102017f77/20190626134606886.json
new file mode 100644
index 0000000..a891f29
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/a5fa69e1-bcb4-43a6-b7d6-eda102017f77/20190626134606886.json
@@ -0,0 +1,1 @@
+{"id":"relationship--a5fa69e1-bcb4-43a6-b7d6-eda102017f77","date_added":"2019-10-29T19:12:15.038371Z","versions":["2019-06-26T13:46:06.886Z"],"media_types":["application/vnd.oasis.stix+json; version=2.0"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/c141c773-338c-4575-a26a-c0dc3f156ad3/20190626134606887.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/c141c773-338c-4575-a26a-c0dc3f156ad3/20190626134606887.json
new file mode 100644
index 0000000..899ffa1
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/relationship/c141c773-338c-4575-a26a-c0dc3f156ad3/20190626134606887.json
@@ -0,0 +1,1 @@
+{"id":"relationship--c141c773-338c-4575-a26a-c0dc3f156ad3","date_added":"2019-10-29T19:12:15.038371Z","versions":["2019-06-26T13:46:06.887Z"],"media_types":["application/vnd.oasis.stix+json; version=2.0"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/report/a5fa69e1-bcb4-43a6-b7d6-eda102017f77/20190626134606886.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/report/a5fa69e1-bcb4-43a6-b7d6-eda102017f77/20190626134606886.json
new file mode 100644
index 0000000..af2522e
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/report/a5fa69e1-bcb4-43a6-b7d6-eda102017f77/20190626134606886.json
@@ -0,0 +1,1 @@
+{"id":"report--a5fa69e1-bcb4-43a6-b7d6-eda102017f77","date_added":"2019-10-29T19:12:15.038371Z","versions":["2019-06-26T13:46:06.886Z"],"media_types":["application/vnd.oasis.stix+json; version=2.0"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/report/b3e138d1-6ef7-4bdc-924a-285f588330d7/20190626134606887.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/report/b3e138d1-6ef7-4bdc-924a-285f588330d7/20190626134606887.json
new file mode 100644
index 0000000..3b7efd4
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/manifest/report/b3e138d1-6ef7-4bdc-924a-285f588330d7/20190626134606887.json
@@ -0,0 +1,1 @@
+{"id":"report--b3e138d1-6ef7-4bdc-924a-285f588330d7","date_added":"2019-10-29T19:12:15.038371Z","versions":["2019-06-26T13:46:06.887Z"],"media_types":["application/vnd.oasis.stix+json; version=2.0"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/attack-pattern/20138b9d-1aac-4a26-8654-a36b6bbf2bba/20190116173959787.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/attack-pattern/20138b9d-1aac-4a26-8654-a36b6bbf2bba/20190116173959787.json
new file mode 100644
index 0000000..94488a7
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/attack-pattern/20138b9d-1aac-4a26-8654-a36b6bbf2bba/20190116173959787.json
@@ -0,0 +1,1 @@
+{"type":"attack-pattern","id":"attack-pattern--20138b9d-1aac-4a26-8654-a36b6bbf2bba","created_by_ref":"identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5","created":"2019-01-16T17:39:59.787Z","modified":"2019-01-16T17:39:59.787Z","name":"T1192: Spearphishing Link","description":"Spearphishing with a link is a specific variant of spearphishing. It is different from other forms of spearphishing in that it employs the use of links to download malware contained in email, instead of attaching malicious files to the email itself, to avoid defenses that may inspect email attachments. \n\nAll forms of spearphishing are electronically delivered social engineering targeted at a specific individual, company, or industry. In this case, the malicious emails contain links. Generally, the links will be accompanied by social engineering text and require the user to actively click or copy and paste a URL into a browser, leveraging [User Execution](https://attack.mitre.org/techniques/T1204). The visited website may compromise the web browser using an exploit, or the user will be prompted to download applications, documents, zip files, or even executables depending on the pretext for the email in the first place. Adversaries may also include links that are intended to interact directly with an email reader, including embedded images intended to exploit the end system directly or verify the receipt of an email (i.e. web bugs/web beacons).","kill_chain_phases":[{"kill_chain_name":"mitre-attack","phase_name":"initial-access"},{"kill_chain_name":"lockheed","phase_name":"delivery"}],"external_references":[{"source_name":"mitre-attack","url":"https://attack.mitre.org/techniques/T1192","external_id":"T1192"},{"source_name":"capec","url":"https://capec.mitre.org/data/definitions/163.html","external_id":"CAPEC-163"}],"object_marking_refs":["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/attack-pattern/389735f1-f21c-4208-b8f0-f8031e7169b8/20190116173939364.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/attack-pattern/389735f1-f21c-4208-b8f0-f8031e7169b8/20190116173939364.json
new file mode 100644
index 0000000..95dc73f
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/attack-pattern/389735f1-f21c-4208-b8f0-f8031e7169b8/20190116173939364.json
@@ -0,0 +1,1 @@
+{"type":"attack-pattern","id":"attack-pattern--389735f1-f21c-4208-b8f0-f8031e7169b8","created_by_ref":"identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5","created":"2019-01-16T17:39:39.364Z","modified":"2019-01-16T17:39:39.364Z","name":"T1176: Browser Extensions","description":"Browser extensions or plugins are small programs that can add functionality and customize aspects of internet browsers. They can be installed directly or through a browser's app store. Extensions generally have access and permissions to everything that the browser can access. (Citation: Wikipedia Browser Extension) (Citation: Chrome Extensions Definition)\n\nMalicious extensions can be installed into a browser through malicious app store downloads masquerading as legitimate extensions, through social engineering, or by an adversary that has already compromised a system. Security can be limited on browser app stores so may not be difficult for malicious extensions to defeat automated scanners and be uploaded. (Citation: Malicious Chrome Extension Numbers) Once the extension is installed, it can browse to websites in the background, (Citation: Chrome Extension Crypto Miner) (Citation: ICEBRG Chrome Extensions) steal all information that a user enters into a browser, to include credentials, (Citation: Banker Google Chrome Extension Steals Creds) (Citation: Catch All Chrome Extension) and be used as an installer for a RAT for persistence. There have been instances of botnets using a persistent backdoor through malicious Chrome extensions. (Citation: Stantinko Botnet) There have also been similar examples of extensions being used for command & control  (Citation: Chrome Extension C2 Malware).","kill_chain_phases":[{"kill_chain_name":"mitre-attack","phase_name":"persistence"},{"kill_chain_name":"lockheed","phase_name":"installation"}],"external_references":[{"source_name":"mitre-attack","url":"https://attack.mitre.org/techniques/T1176","external_id":"T1176"},{"source_name":"Chrome Extensions Definition","url":"https://developer.chrome.com/extensions","description":"Chrome. (n.d.). What are Extensions?. Retrieved November 16, 2017."},{"source_name":"Malicious Chrome Extension Numbers","url":"https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43824.pdf","description":"Jagpal, N., et al. (2015, August). Trends and Lessons from Three Years Fighting Malicious Extensions. Retrieved November 17, 2017."},{"source_name":"Chrome Extension Crypto Miner","url":"https://www.ghacks.net/2017/09/19/first-chrome-extension-with-javascript-crypto-miner-detected/","description":"Brinkmann, M. (2017, September 19). First Chrome extension with JavaScript Crypto Miner detected. Retrieved November 16, 2017."},{"source_name":"Banker Google Chrome Extension Steals Creds","url":"https://isc.sans.edu/forums/diary/BankerGoogleChromeExtensiontargetingBrazil/22722/","description":"Marinho, R. (n.d.). (Banker(GoogleChromeExtension)).targeting. Retrieved November 18, 2017."},{"source_name":"Catch All Chrome Extension","url":"https://isc.sans.edu/forums/diary/CatchAll+Google+Chrome+Malicious+Extension+Steals+All+Posted+Data/22976/https:/threatpost.com/malicious-chrome-extension-steals-data-posted-to-any-website/128680/)","description":"Marinho, R. (n.d.). \"Catch-All\" Google Chrome Malicious Extension Steals All Posted Data. Retrieved November 16, 2017."},{"source_name":"Chrome Extension C2 Malware","url":"https://kjaer.io/extension-malware/","description":"Kjaer, M. (2016, July 18). Malware in the browser: how you might get hacked by a Chrome extension. Retrieved November 22, 2017."},{"source_name":"Stantinko Botnet","url":"https://www.welivesecurity.com/2017/07/20/stantinko-massive-adware-campaign-operating-covertly-since-2012/","description":"Vachon, F., Faou, M. (2017, July 20). Stantinko: A massive adware campaign operating covertly since 2012. Retrieved November 16, 2017."},{"source_name":"Wikipedia Browser Extension","url":"https://en.wikipedia.org/wiki/Browser_extension","description":"Wikipedia. (2017, October 8). Browser Extension. Retrieved January 11, 2018."},{"source_name":"ICEBRG Chrome Extensions","url":"https://www.icebrg.io/blog/malicious-chrome-extensions-enable-criminals-to-impact-over-half-a-million-users-and-global-businesses","description":"De Tore, M., Warner, J. (2018, January 15). MALICIOUS CHROME EXTENSIONS ENABLE CRIMINALS TO IMPACT OVER HALF A MILLION USERS AND GLOBAL BUSINESSES. Retrieved January 17, 2018."}],"object_marking_refs":["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/campaign/b3e138d1-6ef7-4bdc-924a-285f588330d7/20190626134606887.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/campaign/b3e138d1-6ef7-4bdc-924a-285f588330d7/20190626134606887.json
new file mode 100644
index 0000000..aeea176
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/campaign/b3e138d1-6ef7-4bdc-924a-285f588330d7/20190626134606887.json
@@ -0,0 +1,1 @@
+{"type":"campaign","id":"campaign--b3e138d1-6ef7-4bdc-924a-285f588330d7","created":"2019-06-26T13:42:34.372Z","modified":"2019-06-26T13:46:06.887Z","name":"Campaign 1 - Very Simple Playbook","description":"","first_seen":"1970-01-01T00:00:00.000Z","last_seen":"1970-01-01T00:00:00.000Z"}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/indicator/7d72d620-9720-4457-897d-7030d1a972df/20190625182415157.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/indicator/7d72d620-9720-4457-897d-7030d1a972df/20190625182415157.json
new file mode 100644
index 0000000..68c2f1c
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/indicator/7d72d620-9720-4457-897d-7030d1a972df/20190625182415157.json
@@ -0,0 +1,1 @@
+{"type":"indicator","id":"indicator--7d72d620-9720-4457-897d-7030d1a972df","created":"2019-06-25T18:15:42.452Z","modified":"2019-06-25T18:24:15.157Z","name":"https://dailymemes.net","pattern":"[url:value = 'https://dailymemes.net']","valid_from":"2019-06-25T18:15:42.452Z","labels":["malicious-activity"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/intrusion-set/f7fa709b-a740-4bdc-b48c-3ea69ed39cdf/20190626134606887.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/intrusion-set/f7fa709b-a740-4bdc-b48c-3ea69ed39cdf/20190626134606887.json
new file mode 100644
index 0000000..1552f66
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/intrusion-set/f7fa709b-a740-4bdc-b48c-3ea69ed39cdf/20190626134606887.json
@@ -0,0 +1,1 @@
+{"type":"intrusion-set","id":"intrusion-set--f7fa709b-a740-4bdc-b48c-3ea69ed39cdf","created":"2019-06-26T13:42:34.372Z","modified":"2019-06-26T13:46:06.887Z","name":"Very Simple Playbook"}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/20138b9d-1aac-4a26-8654-a36b6bbf2bba/20190626134606887.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/20138b9d-1aac-4a26-8654-a36b6bbf2bba/20190626134606887.json
new file mode 100644
index 0000000..f1620e6
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/20138b9d-1aac-4a26-8654-a36b6bbf2bba/20190626134606887.json
@@ -0,0 +1,1 @@
+{"type":"relationship","id":"relationship--20138b9d-1aac-4a26-8654-a36b6bbf2bba","created":"2019-06-26T13:42:34.372Z","modified":"2019-06-26T13:46:06.887Z","relationship_type":"uses","source_ref":"campaign--b3e138d1-6ef7-4bdc-924a-285f588330d7","target_ref":"attack-pattern--20138b9d-1aac-4a26-8654-a36b6bbf2bba"}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/29e639c6-4bcf-49c7-b637-3bce69b5e1f6/20190626134606887.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/29e639c6-4bcf-49c7-b637-3bce69b5e1f6/20190626134606887.json
new file mode 100644
index 0000000..4cb9db8
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/29e639c6-4bcf-49c7-b637-3bce69b5e1f6/20190626134606887.json
@@ -0,0 +1,1 @@
+{"type":"relationship","id":"relationship--29e639c6-4bcf-49c7-b637-3bce69b5e1f6","created":"2019-06-26T13:45:54.754Z","modified":"2019-06-26T13:46:06.887Z","relationship_type":"uses","source_ref":"campaign--b3e138d1-6ef7-4bdc-924a-285f588330d7","target_ref":"attack-pattern--389735f1-f21c-4208-b8f0-f8031e7169b8"}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/5d399937-d736-413f-a6ec-f77cdf824032/20190626134448412.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/5d399937-d736-413f-a6ec-f77cdf824032/20190626134448412.json
new file mode 100644
index 0000000..ac2963e
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/5d399937-d736-413f-a6ec-f77cdf824032/20190626134448412.json
@@ -0,0 +1,1 @@
+{"type":"relationship","id":"relationship--5d399937-d736-413f-a6ec-f77cdf824032","created":"2019-06-25T18:15:42.578Z","modified":"2019-06-26T13:44:48.412Z","relationship_type":"uses","description":"Cat pictures with bonus malware","source_ref":"indicator--7d72d620-9720-4457-897d-7030d1a972df","target_ref":"attack-pattern--20138b9d-1aac-4a26-8654-a36b6bbf2bba"}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/a5fa69e1-bcb4-43a6-b7d6-eda102017f77/20190626134606886.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/a5fa69e1-bcb4-43a6-b7d6-eda102017f77/20190626134606886.json
new file mode 100644
index 0000000..026ff03
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/a5fa69e1-bcb4-43a6-b7d6-eda102017f77/20190626134606886.json
@@ -0,0 +1,1 @@
+{"type":"relationship","id":"relationship--a5fa69e1-bcb4-43a6-b7d6-eda102017f77","created":"2019-06-26T13:42:34.268Z","modified":"2019-06-26T13:46:06.886Z","relationship_type":"attributed-to","source_ref":"report--a5fa69e1-bcb4-43a6-b7d6-eda102017f77","target_ref":"intrusion-set--f7fa709b-a740-4bdc-b48c-3ea69ed39cdf"}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/c141c773-338c-4575-a26a-c0dc3f156ad3/20190626134606887.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/c141c773-338c-4575-a26a-c0dc3f156ad3/20190626134606887.json
new file mode 100644
index 0000000..fe03d2a
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/relationship/c141c773-338c-4575-a26a-c0dc3f156ad3/20190626134606887.json
@@ -0,0 +1,1 @@
+{"type":"relationship","id":"relationship--c141c773-338c-4575-a26a-c0dc3f156ad3","created":"2019-06-26T13:44:14.536Z","modified":"2019-06-26T13:46:06.887Z","relationship_type":"indicates","source_ref":"indicator--7d72d620-9720-4457-897d-7030d1a972df","target_ref":"campaign--b3e138d1-6ef7-4bdc-924a-285f588330d7"}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/report/a5fa69e1-bcb4-43a6-b7d6-eda102017f77/20190626134606886.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/report/a5fa69e1-bcb4-43a6-b7d6-eda102017f77/20190626134606886.json
new file mode 100644
index 0000000..2f29f3d
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/report/a5fa69e1-bcb4-43a6-b7d6-eda102017f77/20190626134606886.json
@@ -0,0 +1,1 @@
+{"type":"report","id":"report--a5fa69e1-bcb4-43a6-b7d6-eda102017f77","created":"2019-06-26T13:42:34.268Z","modified":"2019-06-26T13:46:06.886Z","name":"Very Simple Playbook","description":"A very simple Playbook with one Campaign, two Attack Patterns, and one Indicator.","published":"2019-06-26T13:46:06.886Z","object_refs":["intrusion-set--f7fa709b-a740-4bdc-b48c-3ea69ed39cdf","report--b3e138d1-6ef7-4bdc-924a-285f588330d7"],"labels":["intrusion-set"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/report/b3e138d1-6ef7-4bdc-924a-285f588330d7/20190626134606887.json b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/report/b3e138d1-6ef7-4bdc-924a-285f588330d7/20190626134606887.json
new file mode 100644
index 0000000..d16e23c
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/report/b3e138d1-6ef7-4bdc-924a-285f588330d7/20190626134606887.json
@@ -0,0 +1,1 @@
+{"type":"report","id":"report--b3e138d1-6ef7-4bdc-924a-285f588330d7","created":"2019-06-26T13:42:34.372Z","modified":"2019-06-26T13:46:06.887Z","name":"Campaign 1 - Very Simple Playbook","published":"2019-06-26T13:46:06.887Z","object_refs":["campaign--b3e138d1-6ef7-4bdc-924a-285f588330d7","intrusion-set--f7fa709b-a740-4bdc-b48c-3ea69ed39cdf","indicator--7d72d620-9720-4457-897d-7030d1a972df","attack-pattern--20138b9d-1aac-4a26-8654-a36b6bbf2bba","attack-pattern--389735f1-f21c-4208-b8f0-f8031e7169b8","relationship--c141c773-338c-4575-a26a-c0dc3f156ad3","relationship--5d399937-d736-413f-a6ec-f77cdf824032","relationship--29e639c6-4bcf-49c7-b637-3bce69b5e1f6","relationship--a5fa69e1-bcb4-43a6-b7d6-eda102017f77","relationship--20138b9d-1aac-4a26-8654-a36b6bbf2bba"],"labels":["campaign"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/status/a949bd0e-60c9-4616-a59d-c89d6b7b06e5.json b/medallion/test/directory/trustgroup1/status/a949bd0e-60c9-4616-a59d-c89d6b7b06e5.json
new file mode 100644
index 0000000..9a951f3
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/status/a949bd0e-60c9-4616-a59d-c89d6b7b06e5.json
@@ -0,0 +1,1 @@
+{"id":"a949bd0e-60c9-4616-a59d-c89d6b7b06e5","status":"complete","request_timestamp":"2019-10-29T19:12:15.038371Z","total_count":12,"success_count":12,"failure_count":0,"pending_count":0,"successes":["report--a5fa69e1-bcb4-43a6-b7d6-eda102017f77","intrusion-set--f7fa709b-a740-4bdc-b48c-3ea69ed39cdf","attack-pattern--20138b9d-1aac-4a26-8654-a36b6bbf2bba","attack-pattern--389735f1-f21c-4208-b8f0-f8031e7169b8","indicator--7d72d620-9720-4457-897d-7030d1a972df","relationship--c141c773-338c-4575-a26a-c0dc3f156ad3","relationship--5d399937-d736-413f-a6ec-f77cdf824032","relationship--29e639c6-4bcf-49c7-b637-3bce69b5e1f6","relationship--a5fa69e1-bcb4-43a6-b7d6-eda102017f77","relationship--20138b9d-1aac-4a26-8654-a36b6bbf2bba","report--b3e138d1-6ef7-4bdc-924a-285f588330d7","campaign--b3e138d1-6ef7-4bdc-924a-285f588330d7"]}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/trustgroup1.json b/medallion/test/directory/trustgroup1/trustgroup1.json
new file mode 100644
index 0000000..61493a1
--- /dev/null
+++ b/medallion/test/directory/trustgroup1/trustgroup1.json
@@ -0,0 +1,8 @@
+{
+    "title": "Malware Research Group",
+    "description": "A trust group setup for malware researchers",
+    "versions": [
+    	"taxii-2.0"
+    ],
+    "max_content_length": 9765625
+}
\ No newline at end of file
diff --git a/medallion/test/directory/trustgroup1/very-simple-playbook.json b/medallion/test/directory/trustgroup1/very-simple-playbook.json
deleted file mode 100644
index f160a81..0000000
--- a/medallion/test/directory/trustgroup1/very-simple-playbook.json
+++ /dev/null
@@ -1,229 +0,0 @@
-{
-  "type": "bundle",
-  "id": "bundle--a5fa69e1-bcb4-43a6-b7d6-eda102017f77",
-  "spec_version": "2.0",
-  "objects": [
-    {
-      "type": "report",
-      "id": "report--a5fa69e1-bcb4-43a6-b7d6-eda102017f77",
-      "created": "2019-06-26T13:42:34.268Z",
-      "modified": "2019-06-26T13:46:06.886Z",
-      "name": "Very Simple Playbook",
-      "description": "A very simple Playbook with one Campaign, two Attack Patterns, and one Indicator.",
-      "published": "2019-06-26T13:46:06.886Z",
-      "object_refs": [
-        "intrusion-set--f7fa709b-a740-4bdc-b48c-3ea69ed39cdf",
-        "report--b3e138d1-6ef7-4bdc-924a-285f588330d7"
-      ],
-      "labels": [
-        "intrusion-set"
-      ]
-    },
-    {
-      "type": "intrusion-set",
-      "id": "intrusion-set--f7fa709b-a740-4bdc-b48c-3ea69ed39cdf",
-      "created": "2019-06-26T13:42:34.372Z",
-      "modified": "2019-06-26T13:46:06.887Z",
-      "name": "Very Simple Playbook"
-    },
-    {
-      "type": "attack-pattern",
-      "id": "attack-pattern--20138b9d-1aac-4a26-8654-a36b6bbf2bba",
-      "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
-      "created": "2019-01-16T17:39:59.787Z",
-      "modified": "2019-01-16T17:39:59.787Z",
-      "name": "T1192: Spearphishing Link",
-      "description": "Spearphishing with a link is a specific variant of spearphishing. It is different from other forms of spearphishing in that it employs the use of links to download malware contained in email, instead of attaching malicious files to the email itself, to avoid defenses that may inspect email attachments. \n\nAll forms of spearphishing are electronically delivered social engineering targeted at a specific individual, company, or industry. In this case, the malicious emails contain links. Generally, the links will be accompanied by social engineering text and require the user to actively click or copy and paste a URL into a browser, leveraging [User Execution](https://attack.mitre.org/techniques/T1204). The visited website may compromise the web browser using an exploit, or the user will be prompted to download applications, documents, zip files, or even executables depending on the pretext for the email in the first place. Adversaries may also include links that are intended to interact directly with an email reader, including embedded images intended to exploit the end system directly or verify the receipt of an email (i.e. web bugs/web beacons).",
-      "kill_chain_phases": [
-        {
-          "kill_chain_name": "mitre-attack",
-          "phase_name": "initial-access"
-        },
-        {
-          "kill_chain_name": "lockheed",
-          "phase_name": "delivery"
-        }
-      ],
-      "external_references": [
-        {
-          "source_name": "mitre-attack",
-          "url": "https://attack.mitre.org/techniques/T1192",
-          "external_id": "T1192"
-        },
-        {
-          "source_name": "capec",
-          "url": "https://capec.mitre.org/data/definitions/163.html",
-          "external_id": "CAPEC-163"
-        }
-      ],
-      "object_marking_refs": [
-        "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
-      ]
-    },
-    {
-      "type": "attack-pattern",
-      "id": "attack-pattern--389735f1-f21c-4208-b8f0-f8031e7169b8",
-      "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
-      "created": "2019-01-16T17:39:39.364Z",
-      "modified": "2019-01-16T17:39:39.364Z",
-      "name": "T1176: Browser Extensions",
-      "description": "Browser extensions or plugins are small programs that can add functionality and customize aspects of internet browsers. They can be installed directly or through a browser's app store. Extensions generally have access and permissions to everything that the browser can access. (Citation: Wikipedia Browser Extension) (Citation: Chrome Extensions Definition)\n\nMalicious extensions can be installed into a browser through malicious app store downloads masquerading as legitimate extensions, through social engineering, or by an adversary that has already compromised a system. Security can be limited on browser app stores so may not be difficult for malicious extensions to defeat automated scanners and be uploaded. (Citation: Malicious Chrome Extension Numbers) Once the extension is installed, it can browse to websites in the background, (Citation: Chrome Extension Crypto Miner) (Citation: ICEBRG Chrome Extensions) steal all information that a user enters into a browser, to include credentials, (Citation: Banker Google Chrome Extension Steals Creds) (Citation: Catch All Chrome Extension) and be used as an installer for a RAT for persistence. There have been instances of botnets using a persistent backdoor through malicious Chrome extensions. (Citation: Stantinko Botnet) There have also been similar examples of extensions being used for command & control  (Citation: Chrome Extension C2 Malware).",
-      "kill_chain_phases": [
-        {
-          "kill_chain_name": "mitre-attack",
-          "phase_name": "persistence"
-        },
-        {
-          "kill_chain_name": "lockheed",
-          "phase_name": "installation"
-        }
-      ],
-      "external_references": [
-        {
-          "source_name": "mitre-attack",
-          "url": "https://attack.mitre.org/techniques/T1176",
-          "external_id": "T1176"
-        },
-        {
-          "source_name": "Chrome Extensions Definition",
-          "url": "https://developer.chrome.com/extensions",
-          "description": "Chrome. (n.d.). What are Extensions?. Retrieved November 16, 2017."
-        },
-        {
-          "source_name": "Malicious Chrome Extension Numbers",
-          "url": "https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/43824.pdf",
-          "description": "Jagpal, N., et al. (2015, August). Trends and Lessons from Three Years Fighting Malicious Extensions. Retrieved November 17, 2017."
-        },
-        {
-          "source_name": "Chrome Extension Crypto Miner",
-          "url": "https://www.ghacks.net/2017/09/19/first-chrome-extension-with-javascript-crypto-miner-detected/",
-          "description": "Brinkmann, M. (2017, September 19). First Chrome extension with JavaScript Crypto Miner detected. Retrieved November 16, 2017."
-        },
-        {
-          "source_name": "Banker Google Chrome Extension Steals Creds",
-          "url": "https://isc.sans.edu/forums/diary/BankerGoogleChromeExtensiontargetingBrazil/22722/",
-          "description": "Marinho, R. (n.d.). (Banker(GoogleChromeExtension)).targeting. Retrieved November 18, 2017."
-        },
-        {
-          "source_name": "Catch All Chrome Extension",
-          "url": "https://isc.sans.edu/forums/diary/CatchAll+Google+Chrome+Malicious+Extension+Steals+All+Posted+Data/22976/https:/threatpost.com/malicious-chrome-extension-steals-data-posted-to-any-website/128680/)",
-          "description": "Marinho, R. (n.d.). \"Catch-All\" Google Chrome Malicious Extension Steals All Posted Data. Retrieved November 16, 2017."
-        },
-        {
-          "source_name": "Chrome Extension C2 Malware",
-          "url": "https://kjaer.io/extension-malware/",
-          "description": "Kjaer, M. (2016, July 18). Malware in the browser: how you might get hacked by a Chrome extension. Retrieved November 22, 2017."
-        },
-        {
-          "source_name": "Stantinko Botnet",
-          "url": "https://www.welivesecurity.com/2017/07/20/stantinko-massive-adware-campaign-operating-covertly-since-2012/",
-          "description": "Vachon, F., Faou, M. (2017, July 20). Stantinko: A massive adware campaign operating covertly since 2012. Retrieved November 16, 2017."
-        },
-        {
-          "source_name": "Wikipedia Browser Extension",
-          "url": "https://en.wikipedia.org/wiki/Browser_extension",
-          "description": "Wikipedia. (2017, October 8). Browser Extension. Retrieved January 11, 2018."
-        },
-        {
-          "source_name": "ICEBRG Chrome Extensions",
-          "url": "https://www.icebrg.io/blog/malicious-chrome-extensions-enable-criminals-to-impact-over-half-a-million-users-and-global-businesses",
-          "description": "De Tore, M., Warner, J. (2018, January 15). MALICIOUS CHROME EXTENSIONS ENABLE CRIMINALS TO IMPACT OVER HALF A MILLION USERS AND GLOBAL BUSINESSES. Retrieved January 17, 2018."
-        }
-      ],
-      "object_marking_refs": [
-        "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
-      ]
-    },
-    {
-      "type": "indicator",
-      "id": "indicator--7d72d620-9720-4457-897d-7030d1a972df",
-      "created": "2019-06-25T18:15:42.452Z",
-      "modified": "2019-06-25T18:24:15.157Z",
-      "name": "https://dailymemes.net",
-      "pattern": "[url:value = 'https://dailymemes.net']",
-      "valid_from": "2019-06-25T18:15:42.452Z",
-      "labels": [
-        "malicious-activity"
-      ]
-    },
-    {
-      "type": "relationship",
-      "id": "relationship--c141c773-338c-4575-a26a-c0dc3f156ad3",
-      "created": "2019-06-26T13:44:14.536Z",
-      "modified": "2019-06-26T13:46:06.887Z",
-      "relationship_type": "indicates",
-      "source_ref": "indicator--7d72d620-9720-4457-897d-7030d1a972df",
-      "target_ref": "campaign--b3e138d1-6ef7-4bdc-924a-285f588330d7"
-    },
-    {
-      "type": "relationship",
-      "id": "relationship--5d399937-d736-413f-a6ec-f77cdf824032",
-      "created": "2019-06-25T18:15:42.578Z",
-      "modified": "2019-06-26T13:44:48.412Z",
-      "relationship_type": "uses",
-      "description": "Cat pictures with bonus malware",
-      "source_ref": "indicator--7d72d620-9720-4457-897d-7030d1a972df",
-      "target_ref": "attack-pattern--20138b9d-1aac-4a26-8654-a36b6bbf2bba"
-    },
-    {
-      "type": "relationship",
-      "id": "relationship--29e639c6-4bcf-49c7-b637-3bce69b5e1f6",
-      "created": "2019-06-26T13:45:54.754Z",
-      "modified": "2019-06-26T13:46:06.887Z",
-      "relationship_type": "uses",
-      "source_ref": "campaign--b3e138d1-6ef7-4bdc-924a-285f588330d7",
-      "target_ref": "attack-pattern--389735f1-f21c-4208-b8f0-f8031e7169b8"
-    },
-    {
-      "type": "relationship",
-      "id": "relationship--a5fa69e1-bcb4-43a6-b7d6-eda102017f77",
-      "created": "2019-06-26T13:42:34.268Z",
-      "modified": "2019-06-26T13:46:06.886Z",
-      "relationship_type": "attributed-to",
-      "source_ref": "report--a5fa69e1-bcb4-43a6-b7d6-eda102017f77",
-      "target_ref": "intrusion-set--f7fa709b-a740-4bdc-b48c-3ea69ed39cdf"
-    },
-    {
-      "type": "relationship",
-      "id": "relationship--20138b9d-1aac-4a26-8654-a36b6bbf2bba",
-      "created": "2019-06-26T13:42:34.372Z",
-      "modified": "2019-06-26T13:46:06.887Z",
-      "relationship_type": "uses",
-      "source_ref": "campaign--b3e138d1-6ef7-4bdc-924a-285f588330d7",
-      "target_ref": "attack-pattern--20138b9d-1aac-4a26-8654-a36b6bbf2bba"
-    },
-    {
-      "type": "report",
-      "id": "report--b3e138d1-6ef7-4bdc-924a-285f588330d7",
-      "created": "2019-06-26T13:42:34.372Z",
-      "modified": "2019-06-26T13:46:06.887Z",
-      "name": "Campaign 1 - Very Simple Playbook",
-      "published": "2019-06-26T13:46:06.887Z",
-      "object_refs": [
-        "campaign--b3e138d1-6ef7-4bdc-924a-285f588330d7",
-        "intrusion-set--f7fa709b-a740-4bdc-b48c-3ea69ed39cdf",
-        "indicator--7d72d620-9720-4457-897d-7030d1a972df",
-        "attack-pattern--20138b9d-1aac-4a26-8654-a36b6bbf2bba",
-        "attack-pattern--389735f1-f21c-4208-b8f0-f8031e7169b8",
-        "relationship--c141c773-338c-4575-a26a-c0dc3f156ad3",
-        "relationship--5d399937-d736-413f-a6ec-f77cdf824032",
-        "relationship--29e639c6-4bcf-49c7-b637-3bce69b5e1f6",
-        "relationship--a5fa69e1-bcb4-43a6-b7d6-eda102017f77",
-        "relationship--20138b9d-1aac-4a26-8654-a36b6bbf2bba"
-      ],
-      "labels": [
-        "campaign"
-      ]
-    },
-    {
-      "type": "campaign",
-      "id": "campaign--b3e138d1-6ef7-4bdc-924a-285f588330d7",
-      "created": "2019-06-26T13:42:34.372Z",
-      "modified": "2019-06-26T13:46:06.887Z",
-      "name": "Campaign 1 - Very Simple Playbook",
-      "description": "",
-      "first_seen": "1970-01-01T00:00:00.000Z",
-      "last_seen": "1970-01-01T00:00:00.000Z"
-    }
-  ]
-}
\ No newline at end of file
diff --git a/medallion/test/test_directory_backend.py b/medallion/test/test_directory_backend.py
index 13b059a..22c4369 100644
--- a/medallion/test/test_directory_backend.py
+++ b/medallion/test/test_directory_backend.py
@@ -1,7 +1,6 @@
 import copy
 import json
 import os
-import uuid
 
 import six
 
@@ -24,16 +23,17 @@ class TestTAXIIServerWithDirectoryBackend(TaxiiTest):
         io = six.StringIO(response)
         return json.load(io)
 
-    def cleanup(self):
-        p = os.path.join(self.configuration['backend']['path'], "trustgroup1")
+    def cleanup(self, api_root, collection_id, resource, obj_id):
+        obj_type, obj_id = obj_id.split("--")
+        p = os.path.join(self.configuration['backend']['path'], api_root, collection_id, resource, obj_type, obj_id)
 
         rm_files = [f for f in os.listdir(p)
-                    if os.path.isfile(os.path.join(p, f)) and f.endswith(".json") and f != "very-simple-playbook.json"]
+                    if os.path.isfile(os.path.join(p, f)) and f.endswith(".json")]
 
         for f in rm_files:
             fp = os.path.join(p, f)
-
             os.remove(fp)
+        os.removedirs(p)
 
     def test_server_discovery(self):
         r = self.client.get(test.DISCOVERY_EP, headers=self.auth)
@@ -41,9 +41,9 @@ class TestTAXIIServerWithDirectoryBackend(TaxiiTest):
         self.assertEqual(r.status_code, 200)
         self.assertEqual(r.content_type, MEDIA_TYPE_TAXII_V20)
         server_info = self.load_json_response(r.data)
-        assert server_info["title"] == "Indicators from the directory backend"
-        assert len(server_info["api_roots"]) == 1
-        assert server_info["api_roots"][0] == "http://localhost:5000/trustgroup1/"
+        assert server_info["title"] == "Some TAXII Server"
+        assert len(server_info["api_roots"]) == 3
+        assert server_info["api_roots"][-1] == "http://localhost:5000/trustgroup1/"
 
     def test_get_api_root_information(self):
         r = self.client.get(test.API_ROOT_EP, headers=self.auth)
@@ -51,7 +51,7 @@ class TestTAXIIServerWithDirectoryBackend(TaxiiTest):
         self.assertEqual(r.status_code, 200)
         self.assertEqual(r.content_type, MEDIA_TYPE_TAXII_V20)
         api_root_metadata = self.load_json_response(r.data)
-        assert api_root_metadata["title"] == "Indicators from directory \'trustgroup1\'"
+        assert api_root_metadata["title"] == "Malware Research Group"
 
     def test_get_api_root_information_not_existent(self):
         r = self.client.get("/trustgroup2/", headers=self.auth)
@@ -68,7 +68,7 @@ class TestTAXIIServerWithDirectoryBackend(TaxiiTest):
         collection_ids = [cm["id"] for cm in collections_metadata]
 
         assert len(collection_ids) == 1
-        assert "46bb17fa-0af3-3446-a570-b55cdfdc7881" in collection_ids
+        assert "91a7b528-80eb-42ed-a74d-c6fbd5a26116" in collection_ids
 
     def test_get_collection(self):
         r = self.client.get(test.GET_COLLECTION_EP_FOR_DIRECTORY_BACKEND, headers=self.auth)
@@ -119,12 +119,8 @@ class TestTAXIIServerWithDirectoryBackend(TaxiiTest):
         self.assertEqual(r.status_code, 404)
 
     def test_add_objects(self):
-        # ------------- BEGIN: cleanup section ------------- #
-        self.cleanup()
-        # ------------- END: cleanup section ------------- #
-
         new_bundle = copy.deepcopy(self.API_OBJECTS_2)
-        new_id = "indicator--%s" % uuid.uuid4()
+        new_id = "indicator--b351b42e-6d40-416c-9f7b-9f6344326d63"
         new_bundle["objects"][0]["id"] = new_id
 
         # ------------- BEGIN: add object section ------------- #
@@ -179,5 +175,6 @@ class TestTAXIIServerWithDirectoryBackend(TaxiiTest):
 
         # ------------- END: get objects section ------------- #
         # ------------- BEGIN: cleanup section ------------- #
-        self.cleanup()
+        self.cleanup("trustgroup1", "91a7b528-80eb-42ed-a74d-c6fbd5a26116", "objects", new_id)
+        self.cleanup("trustgroup1", "91a7b528-80eb-42ed-a74d-c6fbd5a26116", "manifest", new_id)
         # ------------- END: cleanup section ------------- #
diff --git a/medallion/test/test_memory_backend.py b/medallion/test/test_memory_backend.py
index 5a00cfe..3dd8ef6 100644
--- a/medallion/test/test_memory_backend.py
+++ b/medallion/test/test_memory_backend.py
@@ -227,7 +227,7 @@ class TestTAXIIServerWithMemoryBackend(TaxiiTest):
         self.assertEqual(status_response2["success_count"], 0)
         self.assertEqual(
             status_response2["failures"][0]["message"],
-            "Unable to process object",
+            "Unable to process object because identical version exist.",
         )
 
         # ------------- END: add object again section ------------- #