Repository: google/turbinia
Pull request #450 (2019-10-10 15:36:54)

Added function for email notifcations 
Issue #48 

aarontp: Thanks!  I'll try to review this in the next day or two.
CiaranFahy: Yeh I plan on hooking this into task_manager in a later pull request, because I still need to figure out how.
aarontp: Sounds good.  I see you made some updates since my initial review, let me know if/when the PR is ready for re-review.

BTW, The call to send the notification can probably be done somewhere around here:  https://github.com/google/turbinia/blob/7d185da0af88460497eda43aa8f9e2c23ae097d3/turbinia/task_manager.py#L456

 I think you can get the requester from the Task that is passed in to that method.   Ideally it would send the 'status' report in the email, but you might need to plumb in the client similar to here:  https://github.com/google/turbinia/blob/7d185da0af88460497eda43aa8f9e2c23ae097d3/turbinia/workers/finalize_request.py#L43

Or maybe read directly from Datastore.  Let me know if you want me to dig into that more.
CiaranFahy: @aarontp Thank you, It's ready for another review.

Patch:
diff --git a/README.md b/README.md
index 58178ed..f755bb6 100644
--- a/README.md
+++ b/README.md
@@ -1,35 +1,65 @@
 # Turbinia
 
 ## Summary
-Turbinia is an open-source framework for deploying, managing, and running distributed forensic workloads.  It is intended to automate running of common forensic processing tools (i.e. Plaso, TSK, strings, etc) to help with processing evidence in the Cloud, scaling the processing of large amounts of evidence, and decreasing response time by parallelizing processing where possible.
+
+Turbinia is an open-source framework for deploying, managing, and running
+distributed forensic workloads. It is intended to automate running of common
+forensic processing tools (i.e. Plaso, TSK, strings, etc) to help with
+processing evidence in the Cloud, scaling the processing of large amounts of
+evidence, and decreasing response time by parallelizing processing where
+possible.
 
 <img src="docs/images/turbinia-logo.jpg?raw=true" width=240>
 
 ## How it works
-Turbinia is composed of different components for the client, server and the workers.  These components can be run in the Cloud, on local machines, or as a hybrid of both.  The Turbinia client makes requests to process evidence to the Turbinia server.  The Turbinia server creates logical jobs from these incoming user requests, which creates and schedules forensic processing tasks to be run by the workers.  The evidence to be processed will be split up by the jobs when possible, and many tasks can be created in order to process the evidence in parallel.  One or more workers run continuously to process tasks from the server.  Any new evidence created or discovered by the tasks will be fed back into Turbinia for further processing.
-
-Communication from the client to the server is currently done with either Google Cloud PubSub or [Kombu](https://github.com/celery/kombu) messaging.  The worker implementation can use either [PSQ](https://github.com/GoogleCloudPlatform/psq) (a Google Cloud PubSub Task Queue) or [Celery](http://www.celeryproject.org/) for task scheduling.
 
-More information on Turbinia and how it works can be [found here](docs/how-it-works.md).
+Turbinia is composed of different components for the client, server and the
+workers. These components can be run in the Cloud, on local machines, or as a
+hybrid of both. The Turbinia client makes requests to process evidence to the
+Turbinia server. The Turbinia server creates logical jobs from these incoming
+user requests, which creates and schedules forensic processing tasks to be run
+by the workers. The evidence to be processed will be split up by the jobs when
+possible, and many tasks can be created in order to process the evidence in
+parallel. One or more workers run continuously to process tasks from the server.
+Any new evidence created or discovered by the tasks will be fed back into
+Turbinia for further processing.
+
+Communication from the client to the server is currently done with either Google
+Cloud PubSub or [Kombu](https://github.com/celery/kombu) messaging. The worker
+implementation can use either [PSQ](https://github.com/GoogleCloudPlatform/psq)
+(a Google Cloud PubSub Task Queue) or [Celery](http://www.celeryproject.org/)
+for task scheduling.
+
+More information on Turbinia and how it works can be
+[found here](docs/how-it-works.md).
 
 ## Status
+
 Turbinia is currently in Alpha release.
 
 ## Installation
+
 There is an [rough installation guide here](docs/install.md).
 
 ## Usage
-The basic steps to get things running after the initial installation and configuration are:
-* Start Turbinia server component with ```turbiniactl server``` command
-* Start one or more Turbinia workers with ```turbiniactl psqworker```
-* Send evidence to be processed from the turbinia client with ```turbiniactl ${evidencetype}```
-* Check status of running tasks with ```turbiniactl status```
-
-turbiniactl can be used to start the different components, and here is the basic usage:
-``` 
+
+The basic steps to get things running after the initial installation and
+configuration are:
+
+*   Start Turbinia server component with `turbiniactl server` command
+*   Start one or more Turbinia workers with `turbiniactl psqworker`
+*   Send evidence to be processed from the turbinia client with `turbiniactl
+    ${evidencetype}`
+*   Check status of running tasks with `turbiniactl status`
+
+turbiniactl can be used to start the different components, and here is the basic
+usage:
+
+```
 $ turbiniactl --help
-usage: turbiniactl [-h] [-q] [-v] [-d] [-a] [-f] [-o OUTPUT_DIR] [-L LOG_FILE]
-                   [-r REQUEST_ID] [-R] [-S] [-C] [-V] [-D]
+usage: turbiniactl [-h] [-q] [-v] [-d] [-a] [-c CONFIG_FILE]
+                   [-C RECIPE_CONFIG] [-f] [-o OUTPUT_DIR] [-L LOG_FILE]
+                   [-r REQUEST_ID] [-R] [-S] [-V] [-D]
                    [-F FILTER_PATTERNS_FILE] [-j JOBS_WHITELIST]
                    [-J JOBS_BLACKLIST] [-p POLL_INTERVAL] [-t TASK] [-w]
                    <command> ...
@@ -40,6 +70,19 @@ optional arguments:
   -v, --verbose         Show verbose output
   -d, --debug           Show debug output
   -a, --all_fields      Show all task status fields in output
+  -c CONFIG_FILE, --config_file CONFIG_FILE
+                        Load explicit config file. If specified it will ignore
+                        config files in other default locations
+                        (/etc/turbinia.conf, ~/.turbiniarc, or in paths
+                        referenced in environment variable
+                        TURBINIA_CONFIG_PATH)
+  -C RECIPE_CONFIG, --recipe_config RECIPE_CONFIG
+                        Recipe configuration data passed in as comma separated
+                        key=value pairs (e.g. "-C
+                        key=value,otherkey=othervalue"). These will get passed
+                        to tasks as evidence config, and will also be written
+                        to the metadata.json file for Evidence types that
+                        write it
   -f, --force_evidence  Force evidence processing request in potentially
                         unsafe conditions
   -o OUTPUT_DIR, --output_dir OUTPUT_DIR
@@ -52,8 +95,6 @@ optional arguments:
                         infrastructure. This can be used to run one-off Tasks
                         to process data locally.
   -S, --server          Run Turbinia Server indefinitely
-  -C, --use_celery      Pass this flag when using Celery/Kombu for task
-                        queuing and messaging (instead of Google PSQ/pubsub)
   -V, --version         Show the version
   -D, --dump_json       Dump JSON output of Turbinia Request instead of
                         sending it
@@ -63,10 +104,18 @@ optional arguments:
                         grep regex format). This filtered output will be in
                         addition to the complete output
   -j JOBS_WHITELIST, --jobs_whitelist JOBS_WHITELIST
-                        A whitelist for Jobs that we will allow to run (note
-                        that it will not force them to run).
+                        A whitelist for Jobs that will be allowed to run (in
+                        CSV format, no spaces). This will not force them to
+                        run if they are not configured to. This is applied
+                        both at server start time and when the client makes a
+                        processing request. When applied at server start time
+                        the change is persistent while the server is running.
+                        When applied by the client, it will only affect that
+                        processing request.
   -J JOBS_BLACKLIST, --jobs_blacklist JOBS_BLACKLIST
-                        A blacklist for Jobs we will not allow to run
+                        A blacklist for Jobs we will not allow to run. See
+                        --jobs_whitelist help for details on format and when
+                        it is applied.
   -p POLL_INTERVAL, --poll_interval POLL_INTERVAL
                         Number of seconds to wait between polling for task
                         state info
@@ -88,9 +137,17 @@ Commands:
     celeryworker        Run Celery worker
     status              Get Turbinia Task status
     server              Run Turbinia Server
+    config              Prints out config file
+    testnotify          Sends test notification
 ```
 
-The commands for processing the evidence types of rawdisk and directory specify information about evidence that Turbinia should process. By default, when adding new evidence to be processed, turbiniactl will act as a client and send a request to the configured Turbinia server, otherwise if ```--server``` is specified, it will start up its own Turbinia server process.  Here's the turbiniactl usage for adding a raw disk type of evidence to be processed by Turbinia:
+The commands for processing evidence specify the metadata about that evidence
+for Turbinia to process. By default, when adding new evidence to be processed,
+turbiniactl will act as a client and send a request to the configured Turbinia
+server, otherwise if `server` is specified, it will start up its own Turbinia
+server process. Here's the turbiniactl usage for adding a raw disk type of
+evidence to be processed by Turbinia:
+
 ```
 $ ./turbiniactl rawdisk -h
 usage: turbiniactl rawdisk [-h] -l LOCAL_PATH [-s SOURCE] [-n NAME]
@@ -104,19 +161,55 @@ optional arguments:
   -n NAME, --name NAME  Descriptive name of the evidence
 ```
 
-## Other documentation
-* [Installation](docs/install.md)
-* [How it works](docs/how-it-works.md)
-* [Contributing to Turbinia](docs/contributing.md)
-* [Developing new Tasks](docs/developing-new-tasks.md)
-* [FAQ](docs/faq.md)
-* [Debugging and Common Errors](docs/debugging.md)
+Status information about the requests that are being or have been processed can
+be viewed with the `turbiniactl status` command.  You can specify the request ID
+that was generated, or other filters like the username of the requester, or how
+many days of processing history you want to view.  You can also generate
+statistics and reports (in markdown format) with other flags.
+
+```
+$ turbiniactl status -h
+usage: turbiniactl status [-h] [-c] [-C] [-d DAYS_HISTORY] [-f]
+                          [-r REQUEST_ID] [-p PRIORITY_FILTER] [-R] [-s]
+                          [-t TASK_ID] [-u USER]
+
+optional arguments:
+  -h, --help            show this help message and exit
+  -c, --close_tasks     Close tasks based on Request ID or Task ID
+  -C, --csv             When used with --statistics, the output will be in CSV
+                        format
+  -d DAYS_HISTORY, --days_history DAYS_HISTORY
+                        Number of days of history to show
+  -f, --force           Gatekeeper for --close_tasks
+  -r REQUEST_ID, --request_id REQUEST_ID
+                        Show tasks with this Request ID
+  -p PRIORITY_FILTER, --priority_filter PRIORITY_FILTER
+                        This sets what report sections are shown in full
+                        detail in report output. Any tasks that have set a
+                        report_priority value equal to or lower than this
+                        setting will be shown in full detail, and tasks with a
+                        higher value will only have a summary shown. To see
+                        all tasks report output in full detail, set
+                        --priority_filter=100
+  -R, --full_report     Generate full markdown report instead of just a
+                        summary
+  -s, --statistics      Generate statistics only
+  -t TASK_ID, --task_id TASK_ID
+                        Show task for given Task ID
+  -u USER, --user USER  Show task for given user
+```
 
+## Other documentation
 
-## Notes
-* Turbinia currently assumes that Evidence is equally available to all worker nodes (e.g. through locally mapped storage, or through attachable persistent Google Cloud Disks, etc).
-* Not all evidence types are supported yet
-* Still only a small number of processing job types supported, but more are being developed.
+*   [Installation](docs/install.md)
+*   [How it works](docs/how-it-works.md)
+*   [Operational Details](docs/operational-details.md)
+*   [Contributing to Turbinia](docs/contributing.md)
+*   [Developing new Tasks](docs/developing-new-tasks.md)
+*   [FAQ](docs/faq.md)
+*   [Debugging and Common Errors](docs/debugging.md)
 
 ##### Obligatory Fine Print
-This is not an official Google product (experimental or otherwise), it is just code that happens to be owned by Google.
+
+This is not an official Google product (experimental or otherwise), it is just
+code that happens to be owned by Google.
diff --git a/docs/how-it-works.md b/docs/how-it-works.md
index 790c111..f43c356 100644
--- a/docs/how-it-works.md
+++ b/docs/how-it-works.md
@@ -87,12 +87,14 @@ that is available to all Workers.
 
 ## Task Manager
 
-The Task Manager Acts as a broker between the clients and workers and handles
-management of Evidence, Jobs, Tasks and Workers.
+The Task Manager runs in the server and acts as a broker between the clients and
+workers and handles management of Evidence, Jobs, Tasks and Workers.  It will keep
+state on these objects while the processing request that generated them is still
+ongoing.
 
 ## Task Manager Flow
 
-Jobs are configured to run for specific Evidence types, and if the Task
+Jobs are configured to process specific Evidence types, and if the Task
 Manager sees a new piece of Evidence (either from a new Turbinia request, or
 because another Task generated a new piece of Evidence), and there is a Job
 that is configured to run for that type of Evidence, then the Task Mananger will
diff --git a/docs/operational-details.md b/docs/operational-details.md
new file mode 100644
index 0000000..0a5a1ff
--- /dev/null
+++ b/docs/operational-details.md
@@ -0,0 +1,48 @@
+# Operational Details
+
+## Google Cloud Platform (GCP) Processing Details
+
+Turbinia can read Evidence from either cloud Persistent Disks, or from other
+Evidence types saved as GCS objects. Turbinia can also write output to GCS. Note
+that you can operate multiple Turbinia instances within the same GCP Project as
+long as you're careful to make sure your config options (Pub/Sub
+topics/subscriptions, output paths, instance name, etc) don't overlap.
+
+### Persistent Disks
+
+Persistent disks are the default when processing disks that come from the Cloud.
+The account you run Turbinia as must have access to the persistent disks that
+you want to process, and those disks must also be in the same zone as the
+Turbinia workers. If you process GoogleCloudDisk Evidence with Turbinia, the
+worker node will attach the disk automatically before it runs its tasks. If you
+already have access to a persistent disk in a separate project, Turbinia can
+copy this into the project where Turbinia is being run.
+
+### Processing non-cloud disks in GCP
+
+If you have raw disk images from physical machines or elsewhere that you want to
+process in the cloud, the best option is to create a new Cloud Persistent Disk,
+and then copy the image into the filesystem of the new disk. Then you can use
+the GoogleCloudDiskRawEmbedded Evidence type.
+
+Another option is to
+[convert the raw image to a cloud image](https://cloud.google.com/compute/docs/images/import-existing-image),
+and then create a Persistent Disk from that and process it as the
+GoogleCloudDisk Evidence type, but this is not generally recommended as it
+requires zero-padding the disk to a GB boundary which can change the hash of the
+disk, and isn't considered forensically sound.
+
+One last option is to copy the image into GCS and process them directly from
+there, but the GoogleCloudDiskRawEmbeded option is generally recommended because
+this method requires setting up
+[GCS FUSE](https://cloud.google.com/storage/docs/gcs-fuse), and this is less
+stable than using Persistent Disks. If you do choose this option you will need
+to configure all of your worker nodes to mounting your GCS bucket at a common
+path. Once your GCS bucket is mounted, you can process these images as the
+'rawdisk' Evidence type.
+
+## General Notes
+
+*   Turbinia currently assumes that Evidence is equally available to all worker
+    nodes (e.g. through locally mapped storage, or through attachable persistent
+    Google Cloud Disks, etc).
diff --git a/history.txt b/history.txt
new file mode 100644
index 0000000..e69de29
diff --git a/setup.py b/setup.py
index eb0181e..c13a91e 100644
--- a/setup.py
+++ b/setup.py
@@ -16,6 +16,8 @@
 # limitations under the License.
 """This is the setup file for the project."""
 
+# yapf: disable
+
 from __future__ import unicode_literals
 
 import sys
diff --git a/tools/turbinia_job_graph.py b/tools/turbinia_job_graph.py
index ab0c2e2..72a404c 100755
--- a/tools/turbinia_job_graph.py
+++ b/tools/turbinia_job_graph.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python -v
 # -*- coding: utf-8 -*-
 # Copyright 2018 Google Inc.
 #
@@ -14,10 +15,12 @@
 # limitations under the License.
 """Graph to visualise job/evidence relationships."""
 
+from __future__ import print_function
 from __future__ import unicode_literals
 
 import argparse
 import graphviz
+import sys
 
 from turbinia.jobs import manager as jobs_manager
 
@@ -49,14 +52,23 @@ def create_graph():
 if __name__ == '__main__':
   parser = argparse.ArgumentParser(
       description='Create Turbinia evidence graph.')
+  parser.add_argument(
+      '-f', '--format', default='png',
+      help='The format of the output file you wish to generate.  Supported '
+      'types are here: http://www.graphviz.org/doc/info/output.html')
   parser.add_argument('filename', type=unicode, help='where to save the file')
   args = parser.parse_args()
 
+  if args.format not in graphviz.FORMATS:
+    print('Format type {0:s} is not supported'.format(args.format))
+    sys.exit(1)
+
   graph = create_graph()
   output_file = args.filename.replace('.png', '')
 
   try:
-    rendered_graph = graph.render(filename=output_file, cleanup=True)
+    rendered_graph = graph.render(
+        filename=output_file, format=args.format, cleanup=True)
     print('Graph generated and saved to: {0}'.format(rendered_graph))
   except graphviz.ExecutableNotFound:
     print('Graphviz is not installed - Run: apt-get install graphviz')
diff --git a/turbinia/client.py b/turbinia/client.py
index 024d25c..40d5975 100644
--- a/turbinia/client.py
+++ b/turbinia/client.py
@@ -49,6 +49,7 @@ from turbinia.workers.strings import StringsUnicodeTask
 from turbinia.workers.tomcat import TomcatAnalysisTask
 from turbinia.workers.volatility import VolatilityTask
 from turbinia.workers.worker_stat import StatTask
+from turbinia.workers.binary_extractor import BinaryExtractorTask
 from turbinia.workers.bulk_extractor import BulkExtractorTask
 
 # TODO(aarontp): Remove this map after
@@ -69,6 +70,7 @@ TASK_MAP = {
     'tomcatanalysistask': TomcatAnalysisTask,
     'volatilitytask': VolatilityTask,
     'stattask': StatTask,
+    'binaryextractor': BinaryExtractorTask,
     'bulkextractortask': BulkExtractorTask
 }
 
@@ -282,7 +284,7 @@ class TurbiniaClient(object):
     log.info('All {0:d} Tasks completed'.format(len(task_results)))
 
   def get_task_data(
-      self, instance, project, region, days=0, task_id=None, request_id=None,
+      self, instance, project, region, days=1000, task_id=None, request_id=None,
       user=None, function_name='gettasks'):
     """Gets task data from Google Cloud Functions.
 
@@ -567,7 +569,7 @@ class TurbiniaClient(object):
     return '\n'.join(report)
 
   def format_task_status(
-      self, instance, project, region, days=0, task_id=None, request_id=None,
+      self, instance, project, region, days=1000, task_id=None, request_id=None,
       user=None, all_fields=False, full_report=False,
       priority_filter=Priority.HIGH):
     """Formats the recent history for Turbinia Tasks.
diff --git a/turbinia/config/__init__.py b/turbinia/config/__init__.py
index 3f0457e..93f9792 100644
--- a/turbinia/config/__init__.py
+++ b/turbinia/config/__init__.py
@@ -79,6 +79,12 @@ OPTIONAL_VARS = [
     'KOMBU_BROKER',
     'KOMBU_CHANNEL',
     'KOMBU_DURABLE',
+    # Email config
+    'EMAIL_NOTIFICATIONS',
+    'EMAIL_HOST_ADDRESS',
+    'EMAIL_PORT',
+    'EMAIL_ADDRESS',
+    'EMAIL_PASSWORD',
 ]
 
 # Environment variable to look for path data in
diff --git a/turbinia/config/turbinia_config_tmpl.py b/turbinia/config/turbinia_config_tmpl.py
index 2ea8426..4bb004e 100644
--- a/turbinia/config/turbinia_config_tmpl.py
+++ b/turbinia/config/turbinia_config_tmpl.py
@@ -131,3 +131,20 @@ KOMBU_DURABLE = True
 REDIS_HOST = 'localhost'
 REDIS_PORT = '6379'
 REDIS_DB = '0'
+
+################################################################################
+#                           Email Config
+#
+# These options are required if you wish to use email notifications
+################################################################################
+
+# Will emails notifcations be enabled
+EMAIL_NOTIFICATIONS = False
+
+# Host Address and port number(TLS)
+EMAIL_HOST_ADDRESS = 'example.address.com'
+EMAIL_PORT = 587
+
+# Email address and password
+EMAIL_ADDRESS = 'example@address.com'
+EMAIL_PASSWORD = 'Hunter2'
diff --git a/turbinia/evidence.py b/turbinia/evidence.py
index e814ae2..977d85b 100644
--- a/turbinia/evidence.py
+++ b/turbinia/evidence.py
@@ -16,7 +16,6 @@
 
 from __future__ import unicode_literals
 
-import copy
 import json
 import os
 import sys
@@ -52,19 +51,19 @@ def evidence_decode(evidence_dict):
         'Evidence_dict is not a dictionary, type is {0:s}'.format(
             str(type(evidence_dict))))
 
-  type_ = evidence_dict.get('type', None)
+  type_ = evidence_dict.pop('type', None)
   if not type_:
     raise TurbiniaException(
         'No Type attribute for evidence object [{0:s}]'.format(
             str(evidence_dict)))
 
   try:
-    evidence = getattr(sys.modules[__name__], type_)()
+    evidence_class = getattr(sys.modules[__name__], type_)
+    evidence = evidence_class.from_dict(evidence_dict)
   except AttributeError:
     raise TurbiniaException(
         'No Evidence object of type {0:s} in evidence module'.format(type_))
 
-  evidence.__dict__ = evidence_dict
   if evidence_dict.get('parent_evidence'):
     evidence.parent_evidence = evidence_decode(evidence_dict['parent_evidence'])
   if evidence_dict.get('collection'):
@@ -98,7 +97,10 @@ class Evidence(object):
         to the saved_path location.
     source (str): String indicating where evidence came from (including tool
         version that created it, if appropriate).
-    local_path (str): A string of the local_path to the evidence.
+    local_path (str): Path to the processed data (can be a blockdevice or a
+        directory).
+    source_path (str): Path to the un-processed source data for the Evidence.
+    mount_path (str): Path to a mounted file system (if relevant).
     tags (dict): Extra tags associated with this evidence.
     request_id (str): The id of the request this evidence came from, if any.
     parent_evidence (Evidence): The Evidence object that was used to generate
@@ -114,21 +116,24 @@ class Evidence(object):
   REQUIRED_ATTRIBUTES = []
 
   def __init__(
-      self, name=None, description=None, source=None, local_path=None,
-      tags=None, request_id=None):
+      self, name=None, description=None, source=None, source_path=None,
+      tags=None, request_id=None, copyable=False):
     """Initialization for Evidence."""
-    self.copyable = False
+    self.copyable = copyable
     self.config = {}
     self.context_dependent = False
     self.cloud_only = False
     self.description = description
+    self.mount_path = None
     self.source = source
-    self.local_path = local_path
+    self.source_path = source_path
     self.tags = tags if tags else {}
     self.request_id = request_id
     self.parent_evidence = None
     self.save_metadata = False
 
+    self.local_path = source_path
+
     # List of jobs that have processed this evidence
     self.processed_by = []
     self.type = self.__class__.__name__
@@ -136,15 +141,41 @@ class Evidence(object):
     self.saved_path = None
     self.saved_path_type = None
 
+    if self.copyable and not self.local_path:
+      raise TurbiniaException(
+          '{0:s} is a copyable evidence and needs a source_path'.format(
+              self.type))
+
   def __str__(self):
-    return '{0:s}:{1:s}:{2!s}'.format(self.type, self.name, self.local_path)
+    return '{0:s}:{1:s}:{2!s}'.format(self.type, self.name, self.source_path)
 
   def __repr__(self):
     return self.__str__()
 
+  @classmethod
+  def from_dict(cls, dictionary):
+    """Instanciate an Evidence object from a dictionary of attributes.
+
+    Args:
+      dictionary(dict): the attributes to set for this object.
+    Returns:
+      Evidence: the instantiated evidence.
+    """
+    name = dictionary.pop('name', None)
+    description = dictionary.pop('description', None)
+    source = dictionary.pop('source', None)
+    source_path = dictionary.pop('source_path', None)
+    tags = dictionary.pop('tags', None)
+    request_id = dictionary.pop('request_id', None)
+    new_object = cls(
+        name=name, description=description, source=source,
+        source_path=source_path, tags=tags, request_id=request_id)
+    new_object.__dict__.update(dictionary)
+    return new_object
+
   def serialize(self):
     """Return JSON serializable object."""
-    serialized_evidence = copy.deepcopy(self.__dict__)
+    serialized_evidence = self.__dict__.copy()
     if self.parent_evidence:
       serialized_evidence['parent_evidence'] = self.parent_evidence.serialize()
     return serialized_evidence
@@ -197,7 +228,11 @@ class Evidence(object):
                        Task will write to.
 
     """
-    if self.parent_evidence:
+    if self.context_dependent:
+      if not self.parent_evidence:
+        raise TurbiniaException(
+            'Evidence of type {0:s} needs parent_evidence to be set'.format(
+                self.type))
       self.parent_evidence.preprocess()
     self._preprocess(tmp_dir)
 
@@ -313,37 +348,44 @@ class ChromiumProfile(Evidence):
 
   def __init__(self, browser_type=None, output_format=None, *args, **kwargs):
     """Initialization for chromium profile evidence object."""
-    super(ChromiumProfile, self).__init__(*args, **kwargs)
+    super(ChromiumProfile, self).__init__(copyable=True, *args, **kwargs)
     self.browser_type = browser_type
     self.output_format = output_format
-    self.copyable = True
 
 
 class RawDisk(Evidence):
   """Evidence object for Disk based evidence.
 
   Attributes:
-    loopdevice_path: Path to the losetup device for this disk.
-    mount_path: The mount path for this disk (if any).
+    device_path (str): Path to a relevant 'raw' data source (ie: a block
+        device or a raw disk image).
     mount_partition: The mount partition for this disk (if any).
-    size:  The size of the disk in bytes.
+    size: The size of the disk in bytes.
   """
 
-  def __init__(
-      self, mount_path=None, mount_partition=None, size=None, *args, **kwargs):
+  def __init__(self, mount_partition=1, size=None, *args, **kwargs):
     """Initialization for raw disk evidence object."""
-    self.loopdevice_path = None
-    self.mount_path = mount_path
+
+    if mount_partition < 1:
+      raise TurbiniaException(
+          'Partition numbers start at 1, but was given {0:d}'.format(
+              mount_partition))
+
+    self.device_path = None
     self.mount_partition = mount_partition
     self.size = size
     super(RawDisk, self).__init__(*args, **kwargs)
 
   def _preprocess(self, _):
-    self.loopdevice_path = mount_local.PreprocessLosetup(self.local_path)
+    self.device_path, partition_paths = mount_local.PreprocessLosetup(
+        self.source_path)
+    self.mount_path = mount_local.PreprocessMountDisk(
+        partition_paths, self.mount_partition)
+    self.local_path = self.device_path
 
   def _postprocess(self):
-    mount_local.PostprocessDeleteLosetup(self.loopdevice_path)
-    self.loopdevice_path = None
+    mount_local.PostprocessUnmountPath(self.mount_path)
+    mount_local.PostprocessDeleteLosetup(self.device_path)
 
 
 class EncryptedDisk(RawDisk):
@@ -408,7 +450,7 @@ class APFSEncryptedDisk(EncryptedDisk):
 
 
 class GoogleCloudDisk(RawDisk):
-  """Evidence object for Google Cloud Disks.
+  """Evidence object for a Google Cloud Disk.
 
   Attributes:
     project: The cloud project name this disk is associated with.
@@ -427,11 +469,16 @@ class GoogleCloudDisk(RawDisk):
     self.cloud_only = True
 
   def _preprocess(self, _):
-    self.local_path = google_cloud.PreprocessAttachDisk(self.disk_name)
+    self.device_path, partition_paths = google_cloud.PreprocessAttachDisk(
+        self.disk_name)
+
+    self.mount_path = mount_local.PreprocessMountDisk(
+        partition_paths, self.mount_partition)
+    self.local_path = self.device_path
 
   def _postprocess(self):
-    google_cloud.PostprocessDetachDisk(self.disk_name, self.local_path)
-    self.local_path = None
+    mount_local.PostprocessUnmountPath(self.mount_path)
+    google_cloud.PostprocessDetachDisk(self.disk_name, self.device_path)
 
 
 class GoogleCloudDiskRawEmbedded(GoogleCloudDisk):
@@ -446,24 +493,37 @@ class GoogleCloudDiskRawEmbedded(GoogleCloudDisk):
     embedded_path: The path of the raw disk image inside the Persistent Disk
   """
 
-  REQUIRED_ATTRIBUTES = ['disk_name', 'project', 'zone', 'embedded_path']
+  REQUIRED_ATTRIBUTES = [
+      'disk_name', 'project', 'zone', 'embedded_partition', 'embedded_path'
+  ]
 
-  def __init__(self, embedded_path=None, *args, **kwargs):
-    """Initialization for Google Cloud Disk."""
+  def __init__(
+      self, embedded_path=None, embedded_partition=None, *args, **kwargs):
+    """Initialization for Google Cloud Disk containing a raw disk image."""
     self.embedded_path = embedded_path
+    self.embedded_partition = embedded_partition
     super(GoogleCloudDiskRawEmbedded, self).__init__(*args, **kwargs)
 
+    # This Evidence needs to have a GoogleCloudDisk as a parent
+    self.context_dependent = True
+
   def _preprocess(self, _):
-    self.local_path = google_cloud.PreprocessAttachDisk(self.disk_name)
-    self.loopdevice_path = mount_local.PreprocessLosetup(self.local_path)
+    rawdisk_path = os.path.join(
+        self.parent_evidence.mount_path, self.embedded_path)
+    if not os.path.exists(rawdisk_path):
+      raise TurbiniaException(
+          'Unable to find raw disk image {0:s} in GoogleCloudDisk'.format(
+              rawdisk_path))
+    self.device_path, partition_paths = mount_local.PreprocessLosetup(
+        rawdisk_path)
+
     self.mount_path = mount_local.PreprocessMountDisk(
-        self.loopdevice_path, self.mount_partition)
-    self.local_path = os.path.join(self.mount_path, self.embedded_path)
+        partition_paths, self.mount_partition)
+    self.local_path = self.device_path
 
   def _postprocess(self):
-    google_cloud.PostprocessDetachDisk(self.disk_name, self.local_path)
     mount_local.PostprocessUnmountPath(self.mount_path)
-    mount_local.PostprocessDeleteLosetup(self.loopdevice_path)
+    mount_local.PostprocessDeleteLosetup(self.device_path)
 
 
 class PlasoFile(Evidence):
@@ -476,18 +536,17 @@ class PlasoFile(Evidence):
   def __init__(self, plaso_version=None, *args, **kwargs):
     """Initialization for Plaso File evidence."""
     self.plaso_version = plaso_version
-    super(PlasoFile, self).__init__(*args, **kwargs)
-    self.copyable = True
+    super(PlasoFile, self).__init__(copyable=True, *args, **kwargs)
     self.save_metadata = True
 
 
-class PlasoCsvFile(PlasoFile):
+class PlasoCsvFile(Evidence):
   """Psort output file evidence.  """
 
   def __init__(self, plaso_version=None, *args, **kwargs):
     """Initialization for Plaso File evidence."""
     self.plaso_version = plaso_version
-    super(PlasoCsvFile, self).__init__(*args, **kwargs)
+    super(PlasoCsvFile, self).__init__(copyable=True, *args, **kwargs)
     self.save_metadata = False
 
 
@@ -497,8 +556,7 @@ class ReportText(Evidence):
 
   def __init__(self, text_data=None, *args, **kwargs):
     self.text_data = text_data
-    super(ReportText, self).__init__(*args, **kwargs)
-    self.copyable = True
+    super(ReportText, self).__init__(copyable=True, *args, **kwargs)
 
 
 class FinalReport(ReportText):
@@ -513,8 +571,7 @@ class TextFile(Evidence):
   """Text data."""
 
   def __init__(self, *args, **kwargs):
-    super(TextFile, self).__init__(*args, **kwargs)
-    self.copyable = True
+    super(TextFile, self).__init__(copyable=True, *args, **kwargs)
 
 
 class FilteredTextFile(TextFile):
@@ -529,9 +586,8 @@ class ExportedFileArtifact(Evidence):
 
   def __init__(self, artifact_name=None, *args, **kwargs):
     """Initializes an exported file artifact."""
-    super(ExportedFileArtifact, self).__init__(*args, **kwargs)
+    super(ExportedFileArtifact, self).__init__(copyable=True, *args, **kwargs)
     self.artifact_name = artifact_name
-    self.copyable = True
 
 
 class VolatilityReport(TextFile):
@@ -554,3 +610,8 @@ class RawMemory(Evidence):
     super(RawMemory, self).__init__(*args, **kwargs)
     self.profile = profile
     self.module_list = module_list
+
+
+class BinaryExtraction(CompressedDirectory):
+  """Binaries extracted from evidence."""
+  pass
diff --git a/turbinia/jobs/__init__.py b/turbinia/jobs/__init__.py
index 8e51312..b60b1f2 100644
--- a/turbinia/jobs/__init__.py
+++ b/turbinia/jobs/__init__.py
@@ -26,4 +26,5 @@ from turbinia.jobs import strings
 from turbinia.jobs import tomcat
 from turbinia.jobs import volatility
 from turbinia.jobs import worker_stat
+from turbinia.jobs import binary_extractor
 from turbinia.jobs import bulk_extractor
diff --git a/turbinia/jobs/binary_extractor.py b/turbinia/jobs/binary_extractor.py
new file mode 100644
index 0000000..3fb4346
--- /dev/null
+++ b/turbinia/jobs/binary_extractor.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Job to execute binary_extractor task."""
+
+from __future__ import unicode_literals
+
+from turbinia.evidence import Directory
+from turbinia.evidence import GoogleCloudDisk
+from turbinia.evidence import GoogleCloudDiskRawEmbedded
+from turbinia.evidence import RawDisk
+from turbinia.evidence import BinaryExtraction
+from turbinia.jobs import interface
+from turbinia.jobs import manager
+from turbinia.workers.binary_extractor import BinaryExtractorTask
+
+
+class BinaryExtractorJob(interface.TurbiniaJob):
+  """Run image_export.py on evidence to extract binaries."""
+
+  # The types of evidence that this Job will process.
+  evidence_input = [
+      Directory, RawDisk, GoogleCloudDisk, GoogleCloudDiskRawEmbedded
+  ]
+  evidence_output = [BinaryExtraction]
+
+  NAME = 'BinaryExtractorJob'
+
+  def create_tasks(self, evidence):
+    """Create task.
+
+    Args:
+      evidence: List of evidence objects to process
+
+    Returns:
+        A list of tasks to schedule.
+    """
+    tasks = [BinaryExtractorTask() for _ in evidence]
+    return tasks
+
+
+manager.JobsManager.RegisterJob(BinaryExtractorJob)
diff --git a/turbinia/lib/google_cloud.py b/turbinia/lib/google_cloud.py
index b78cdc0..0cc9e2d 100644
--- a/turbinia/lib/google_cloud.py
+++ b/turbinia/lib/google_cloud.py
@@ -191,7 +191,7 @@ class GoogleCloudProject(object):
         zone = instance['zone']
       return GoogleComputeInstance(project=self, zone=zone, name=instance_name)
     except KeyError:
-      raise TurbiniaException('Unknown instance {0:s}'.format(instance_name))
+      raise TurbiniaException('Unknown instance: {0!s}'.format(instance_name))
 
 
 class GoogleCloudFunction(GoogleCloudProject):
diff --git a/turbinia/lib/utils.py b/turbinia/lib/utils.py
index 0605e70..691ceea 100644
--- a/turbinia/lib/utils.py
+++ b/turbinia/lib/utils.py
@@ -75,8 +75,8 @@ def extract_artifacts(artifact_names, disk_path, output_dir):
   # Plaso image_export expects artifact names as a comma separated string.
   artifacts = ','.join(artifact_names)
   image_export_cmd = [
-      'image_export.py', '--artifact_filters', artifacts, '--write', output_dir,
-      '--partitions', 'all', disk_path
+      'sudo', 'image_export.py', '--artifact_filters', artifacts, '--write',
+      output_dir, '--partitions', 'all', disk_path
   ]
 
   return _image_export(image_export_cmd, output_dir)
diff --git a/turbinia/notify.py b/turbinia/notify.py
new file mode 100644
index 0000000..5f86908
--- /dev/null
+++ b/turbinia/notify.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Sends email notifications"""
+
+import logging
+import smtplib
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from turbinia import config
+
+log = logging.getLogger('turbinia')
+
+
+#Sends notfications via email
+def sendmail(address, subject, message):
+  """Sends an Email notification via SMTP
+     Args:
+         address: This is the address that the email will be sent to
+         subject: This will be the subject of the email
+         message: This will be the message of the email
+  """
+  try:
+    if config.EMAIL_NOTIFICATIONS is True:
+
+      #Puts message in MIME format
+      msg = MIMEMultipart()
+      msg['From'] = config.EMAIL_ADDRESS
+      msg['To'] = address
+      msg['Subject'] = subject
+      msg.attach(MIMEText(message))
+
+      #start SMTP
+      server = smtplib.SMTP(config.EMAIL_HOST_ADDRESS, config.EMAIL_PORT)
+
+      # identify to mail server
+      server.ehlo()
+
+      # secure email using TLS
+      server.starttls()
+
+      # reidentify as using TLS
+      server.ehlo()
+
+      #Attempt to login
+      if config.EMAIL_PASSWORD != '':
+        server.login(config.EMAIL_ADDRESS, config.EMAIL_PASSWORD)
+
+      else:
+        log.info(
+            'Email password is blank, '
+            'attempting to continue without logging in')
+
+      server.sendmail(config.EMAIL_ADDRESS, address, msg.as_string())
+      log.info('Email notification sent to ' + address)
+    else:
+      log.info('Email notifications are disabled')
+
+  except smtplib.SMTPException as e:
+    log.error(e)
+    log.error(
+        'Email failed to send, SMTP has raised an error,'
+        ' this likely means that there is a problem with the config')
+  except TypeError:
+    log.error('Email failed to send, there is likely a problem with the config')
+  except NameError:
+    log.error(
+        'Email failed to send, A value which is required'
+        ' for email notifications is not defined in the config')
+
+  finally:
+    #Terminate connection to server if active
+    try:
+      server.quit()
+    except UnboundLocalError:
+      pass
diff --git a/turbinia/output_manager.py b/turbinia/output_manager.py
index 768b258..6e39f1b 100644
--- a/turbinia/output_manager.py
+++ b/turbinia/output_manager.py
@@ -170,7 +170,7 @@ class OutputManager(object):
     evidence_.saved_path = path
     evidence_.saved_path_type = path_type
     log.info(
-        'Saved copyable evidence data to {0:s}'.format(evidence_.saved_path))
+        'Saved copyable evidence data to {0!s}'.format(evidence_.saved_path))
     return evidence_
 
   def save_local_file(self, file_, result):
@@ -194,10 +194,15 @@ class OutputManager(object):
     local_path = None
     for writer in self._output_writers:
       new_path = writer.copy_to(file_)
-      if new_path and result:
-        result.saved_paths.append(new_path)
-        saved_path = new_path
-        saved_path_type = writer.name
+      if result:
+        if new_path:
+          result.saved_paths.append(new_path)
+          saved_path = new_path
+          saved_path_type = writer.name
+        elif os.path.exists(file_) and os.path.getsize(file_) > 0:
+          # We want to save the old path if the path is still valid.
+          result.saved_paths.append(file_)
+
       if writer.name == LocalOutputWriter.NAME:
         local_path = new_path
 
@@ -350,18 +355,20 @@ class LocalOutputWriter(OutputWriter):
     if self.local_output_dir in os.path.commonprefix([file_path,
                                                       destination_file]):
       log.debug(
-          'Not copying file {0:s} in output dir {1:s}'.format(
+          'Not copying source file {0:s} already in output dir {1:s}'.format(
               file_path, self.local_output_dir))
       return None
     if not os.path.exists(file_path):
-      log.warning('File [{0:s}] does not exist.'.format(file_path))
+      log.warning('Source file [{0:s}] does not exist.'.format(file_path))
       return None
     if os.path.exists(destination_file):
       log.warning(
-          'New file path [{0:s}] already exists.'.format(destination_file))
+          'Target output file path [{0:s}] already exists.'.format(
+              destination_file))
       return None
 
     shutil.copy(file_path, destination_file)
+    log.debug('Copied file {0:s} to {1:s}'.format(file_path, destination_file))
     return destination_file
 
   def copy_to(self, source_file):
diff --git a/turbinia/processors/google_cloud.py b/turbinia/processors/google_cloud.py
index ef25133..563c9f3 100644
--- a/turbinia/processors/google_cloud.py
+++ b/turbinia/processors/google_cloud.py
@@ -16,6 +16,7 @@
 
 from __future__ import unicode_literals
 
+import glob
 import logging
 import os
 import stat
@@ -73,12 +74,17 @@ def PreprocessAttachDisk(disk_name):
     disk_name(str): The name of the Cloud Disk to attach.
 
   Returns:
-    str: the path to the attached block device.
+    (str, list(str)): a tuple consisting of the path to the 'disk' block device
+      and a list of paths to partition block devices. For example:
+      (
+       '/dev/disk/by-id/google-disk0',
+       ['/dev/disk/by-id/google-disk0-part1', '/dev/disk/by-id/google-disk0-p2']
+      )
   """
   path = '/dev/disk/by-id/google-{0:s}'.format(disk_name)
   if IsBlockDevice(path):
     log.info('Disk {0:s} already attached!'.format(disk_name))
-    return path
+    return (path, glob.glob('{0:s}-part*'.format(path)))
 
   config.LoadConfig()
   instance_name = GetLocalInstanceName()
@@ -102,7 +108,7 @@ def PreprocessAttachDisk(disk_name):
                                                   os.stat(path).st_mode))
     time.sleep(1)
 
-  return path
+  return (path, glob.glob('{0:s}-part*'.format(path)))
 
 
 def PostprocessDetachDisk(disk_name, local_path):
diff --git a/turbinia/processors/mount_local.py b/turbinia/processors/mount_local.py
index bde60c2..4d40a24 100644
--- a/turbinia/processors/mount_local.py
+++ b/turbinia/processors/mount_local.py
@@ -16,10 +16,12 @@
 
 from __future__ import unicode_literals
 
+import glob
 import logging
 import os
 import subprocess
 import tempfile
+import time
 
 from turbinia import config
 from turbinia import TurbiniaException
@@ -34,15 +36,29 @@ def PreprocessLosetup(source_path):
     source_path(str): the source path to run losetup on.
 
   Raises:
-    TurbiniaException: if the losetup command failed to run.
+    TurbiniaException: if source_path doesn't exist or if the losetup command
+      failed to run in anyway.
 
   Returns:
-    str: the path to the created loopdevice (ie: /dev/loopX)
+    (str, list(str)): a tuple consisting of the path to the 'disk' block device
+      and a list of paths to partition block devices. For example:
+      ('/dev/loop0', ['/dev/loop0p1', '/dev/loop0p2'])
   """
   losetup_device = None
+
+  if not os.path.exists(source_path):
+    raise TurbiniaException(
+        'Cannot process non-existing source_path {0!s}'.format(source_path))
+
   # TODO(aarontp): Remove hard-coded sudo in commands:
   # https://github.com/google/turbinia/issues/73
-  losetup_command = ['sudo', 'losetup', '--show', '--find', '-P', source_path]
+  if not os.path.exists(source_path):
+    raise TurbiniaException(
+        'Cannot mount non-existing source_path {0!s}'.format(source_path))
+
+  losetup_command = [
+      'sudo', 'losetup', '--show', '--find', '-P', '-r', source_path
+  ]
   log.info('Running command {0:s}'.format(' '.join(losetup_command)))
   try:
     losetup_device = subprocess.check_output(
@@ -50,15 +66,22 @@ def PreprocessLosetup(source_path):
   except subprocess.CalledProcessError as e:
     raise TurbiniaException('Could not set losetup devices {0!s}'.format(e))
 
-  return losetup_device
+  partitions = glob.glob('{0:s}p*'.format(losetup_device))
+  if not partitions:
+    # In this case, the image was of a partition, and not a full disk with a
+    # partition table
+    return (losetup_device, [losetup_device])
 
+  return (losetup_device, partitions)
 
-def PreprocessMountDisk(loopdevice_path, partition_number):
+
+def PreprocessMountDisk(partition_paths, partition_number):
   """Locally mounts disk in an instance.
 
   Args:
-    loopdevice_path(str): The path to the block device to mount.
-    partition_number(int): The partition number.
+    partition_paths(list(str)): A list of paths to partition block devices;
+    partition_number(int): the number of the partition to mount. Remember these
+      are 1-indexed (first partition is 1).
 
   Raises:
     TurbiniaException: if the mount command failed to run.
@@ -69,6 +92,24 @@ def PreprocessMountDisk(loopdevice_path, partition_number):
   config.LoadConfig()
   mount_prefix = config.MOUNT_DIR_PREFIX
 
+  if partition_number > len(partition_paths):
+    raise TurbiniaException(
+        'Can not mount partition {0:d}: found only {1:d} partitions in '
+        'Evidence.'.format(partition_number, len(partition_paths)))
+
+  # Partitions are 1-indexed for the user and the system
+  if partition_number < 1:
+    raise TurbiniaException(
+        'Can not mount partition {0:d}: partition numbering starts at 1'.format(
+            partition_number))
+
+  partition_path = partition_paths[partition_number - 1]
+
+  if not os.path.exists(partition_path):
+    raise TurbiniaException(
+        'Could not mount partition {0:s}, the path does not exist'.format(
+            partition_path))
+
   if os.path.exists(mount_prefix) and not os.path.isdir(mount_prefix):
     raise TurbiniaException(
         'Mount dir {0:s} exists, but is not a directory'.format(mount_prefix))
@@ -83,20 +124,14 @@ def PreprocessMountDisk(loopdevice_path, partition_number):
 
   mount_path = tempfile.mkdtemp(prefix='turbinia', dir=mount_prefix)
 
-  if not partition_number:
-    # The first partition loop-device made by losetup is loopXp1
-    partition_number = 1
-
-  path_to_partition = '{0:s}p{1:d}'.format(loopdevice_path, partition_number)
+  mount_cmd = ['sudo', 'mount', '-o', 'ro']
+  fstype = GetFilesystem(partition_path)
+  if fstype in ['ext3', 'ext4']:
+    # This is in case the underlying filesystem is dirty, as we want to mount
+    # everything read-only.
+    mount_cmd.extend(['-o', 'noload'])
+  mount_cmd.extend([partition_path, mount_path])
 
-  if not os.path.exists(path_to_partition):
-    log.info(
-        'Could not find {0:s}, trying {1:s}'.format(
-            path_to_partition, loopdevice_path))
-    # Else, the partition's block device is actually /dev/loopX
-    path_to_partition = loopdevice_path
-
-  mount_cmd = ['sudo', 'mount', path_to_partition, mount_path]
   log.info('Running: {0:s}'.format(' '.join(mount_cmd)))
   try:
     subprocess.check_call(mount_cmd)
@@ -106,11 +141,34 @@ def PreprocessMountDisk(loopdevice_path, partition_number):
   return mount_path
 
 
-def PostprocessDeleteLosetup(loopdevice_path):
+def GetFilesystem(path):
+  """Uses lsblk to detect the filesystem of a partition block device.
+
+  Args:
+    path(str): the full path to the block device.
+  Returns:
+    str: the filesystem detected (for example: 'ext4')
+  """
+  cmd = ['lsblk', path, '-f', '-o', 'FSTYPE', '-n']
+  log.info('Running {0!s}'.format(cmd))
+  fstype = subprocess.check_output(cmd).split()
+  if not fstype:
+    # Lets wait a bit for any previous blockdevice operation to settle
+    time.sleep(2)
+    fstype = subprocess.check_output(cmd).split()
+
+  if len(fstype) != 1:
+    raise TurbiniaException(
+        '{0:s} should contain exactly one partition, found {1:d}'.format(
+            path, len(fstype)))
+  return fstype[0].decode('utf-8').strip()
+
+
+def PostprocessDeleteLosetup(device_path):
   """Removes a loop device.
 
   Args:
-    loopdevice_path(str): the path to the block device to remove
+    device_path(str): the path to the block device to remove
       (ie: /dev/loopX).
 
   Raises:
@@ -118,7 +176,7 @@ def PostprocessDeleteLosetup(loopdevice_path):
   """
   # TODO(aarontp): Remove hard-coded sudo in commands:
   # https://github.com/google/turbinia/issues/73
-  losetup_cmd = ['sudo', 'losetup', '-d', loopdevice_path]
+  losetup_cmd = ['sudo', 'losetup', '-d', device_path]
   log.info('Running: {0:s}'.format(' '.join(losetup_cmd)))
   try:
     subprocess.check_call(losetup_cmd)
diff --git a/turbinia/task_manager.py b/turbinia/task_manager.py
index 7eddb60..85fc96e 100644
--- a/turbinia/task_manager.py
+++ b/turbinia/task_manager.py
@@ -297,18 +297,16 @@ class BaseTaskManager(object):
     Args:
       task: An instantiated Turbinia Task
       evidence_: An Evidence object to be processed.
-
-    Raises:
-      TurbiniaException: If the request_id cannot be found.
     """
     if evidence_.request_id:
       task.request_id = evidence_.request_id
     elif job and job.request_id:
       task.request_id = job.request_id
     else:
-      raise TurbiniaException(
-          'Request ID not found in Evidence {0!s} or Task {1!s}'.format(
-              evidence_, task))
+      log.error(
+          'Request ID not found in Evidence {0!s} or Task {1!s}. Not adding '
+          'new Task because of undefined state'.format(evidence_, task))
+      return
 
     evidence_.config = job.evidence.config
     task.base_output_dir = config.OUTPUT_DIR
diff --git a/turbinia/turbiniactl.py b/turbinia/turbiniactl.py
index 40f5700..6d6b404 100644
--- a/turbinia/turbiniactl.py
+++ b/turbinia/turbiniactl.py
@@ -136,6 +136,10 @@ def main():
   parser_config.add_argument(
       '-f', '--file_only', action='store_true', help='Print out file path only')
 
+  #Sends Test Notification
+  parser_testnotify = subparsers.add_parser(
+      'testnotify', help='Sends test notification')
+
   # TODO(aarontp): Find better way to specify these that allows for multiple
   # pieces of evidence to be submitted. Maybe automagically create different
   # commands based on introspection of evidence objects?
@@ -143,7 +147,7 @@ def main():
   parser_rawdisk = subparsers.add_parser(
       'rawdisk', help='Process RawDisk as Evidence')
   parser_rawdisk.add_argument(
-      '-l', '--local_path', help='Local path to the evidence', required=True)
+      '-l', '--source_path', help='Local path to the evidence', required=True)
   parser_rawdisk.add_argument(
       '-P', '--mount_partition', default=1, type=int,
       help='The partition number to use when mounting this disk.  Defaults to '
@@ -159,7 +163,7 @@ def main():
   parser_apfs = subparsers.add_parser(
       'apfs', help='Process APFSEncryptedDisk as Evidence')
   parser_apfs.add_argument(
-      '-l', '--local_path', help='Local path to the encrypted APFS evidence',
+      '-l', '--source_path', help='Local path to the encrypted APFS evidence',
       required=True)
   parser_apfs.add_argument(
       '-r', '--recovery_key', help='Recovery key for the APFS evidence.  '
@@ -178,7 +182,7 @@ def main():
   parser_bitlocker = subparsers.add_parser(
       'bitlocker', help='Process Bitlocker Disk as Evidence')
   parser_bitlocker.add_argument(
-      '-l', '--local_path',
+      '-l', '--source_path',
       help='Local path to the encrypted Bitlocker evidence', required=True)
   parser_bitlocker.add_argument(
       '-r', '--recovery_key', help='Recovery key for the Bitlocker evidence.  '
@@ -208,7 +212,7 @@ def main():
       'with. If this is different from the project that Turbinia is running '
       'in, it will be copied to the Turbinia project.')
   parser_googleclouddisk.add_argument(
-      '-P', '--mount_partition', default=0, type=int,
+      '-P', '--mount_partition', default=1, type=int,
       help='The partition number to use when mounting this disk.  Defaults to '
       'the entire raw disk.  Only affects mounting, and not what gets '
       'processed.')
@@ -240,10 +244,14 @@ def main():
       'with. If this is different from the project that Turbinia is running '
       'in, it will be copied to the Turbinia project.')
   parser_googleclouddiskembedded.add_argument(
-      '-P', '--mount_partition', default=0, type=int,
+      '-P', '--mount_partition', default=1, type=int,
       help='The partition number to use when mounting this disk.  Defaults to '
       'the entire raw disk.  Only affects mounting, and not what gets '
       'processed.')
+  parser_googleclouddiskembedded.add_argument(
+      '--embedded_mount_partition', default=1, type=int,
+      help='The partition number to use when mounting this embedded disk image.'
+      ' Defaults to the first partition')
   parser_googleclouddiskembedded.add_argument(
       '-z', '--zone', help='Geographic zone the disk exists in')
   parser_googleclouddiskembedded.add_argument(
@@ -256,7 +264,7 @@ def main():
   parser_rawmemory = subparsers.add_parser(
       'rawmemory', help='Process RawMemory as Evidence')
   parser_rawmemory.add_argument(
-      '-l', '--local_path', help='Local path to the evidence', required=True)
+      '-l', '--source_path', help='Local path to the evidence', required=True)
   parser_rawmemory.add_argument(
       '-P', '--profile', help='Profile to use with Volatility', required=True)
   parser_rawmemory.add_argument(
@@ -269,7 +277,7 @@ def main():
   parser_directory = subparsers.add_parser(
       'directory', help='Process a directory as Evidence')
   parser_directory.add_argument(
-      '-l', '--local_path', help='Local path to the evidence', required=True)
+      '-l', '--source_path', help='Local path to the evidence', required=True)
   parser_directory.add_argument(
       '-s', '--source', help='Description of the source of the evidence',
       required=False)
@@ -291,7 +299,7 @@ def main():
   parser_hindsight = subparsers.add_parser(
       'hindsight', help='Process ChromiumProfile as Evidence')
   parser_hindsight.add_argument(
-      '-l', '--local_path', help='Local path to the evidence', required=True)
+      '-l', '--source_path', help='Local path to the evidence', required=True)
   parser_hindsight.add_argument(
       '-f', '--format', help='Output format (supported types are '
       'xlsx, sqlite, jsonl)', default='sqlite')
@@ -327,7 +335,7 @@ def main():
       help='When used with --statistics, the output will be in CSV format',
       required=False)
   parser_status.add_argument(
-      '-d', '--days_history', default=0, type=int,
+      '-d', '--days_history', default=1000, type=int,
       help='Number of days of history to show', required=False)
   parser_status.add_argument(
       '-f', '--force', help='Gatekeeper for --close_tasks', action='store_true',
@@ -395,6 +403,7 @@ def main():
   # config is loaded by these modules at load time, and we want to wait to load
   # the config until after we parse the args so that we can use those arguments
   # to point to config paths.
+  from turbinia import notify
   from turbinia.client import TurbiniaClient
   from turbinia.client import TurbiniaCeleryClient
   from turbinia.client import TurbiniaServer
@@ -418,6 +427,12 @@ def main():
           "Failed to read config file {0:s}: {1!s}".format(
               config.configSource, exception))
       sys.exit(1)
+  #sends test notification
+  if args.command == 'testnotify':
+    notify.sendmail(
+        config.EMAIL_ADDRESS, 'Turbinia test notification',
+        'This is a test notification')
+    sys.exit(0)
 
   if args.jobs_whitelist and args.jobs_blacklist:
     log.error(
@@ -487,40 +502,40 @@ def main():
   # Start Evidence configuration
   evidence_ = None
   if args.command == 'rawdisk':
-    args.name = args.name if args.name else args.local_path
-    local_path = os.path.abspath(args.local_path)
+    args.name = args.name if args.name else args.source_path
+    source_path = os.path.abspath(args.source_path)
     evidence_ = evidence.RawDisk(
-        name=args.name, local_path=local_path,
+        name=args.name, source_path=source_path,
         mount_partition=args.mount_partition, source=args.source)
   elif args.command == 'apfs':
     if not args.password and not args.recovery_key:
       log.error('Neither recovery key nor password is specified.')
       sys.exit(1)
-    args.name = args.name if args.name else args.local_path
-    local_path = os.path.abspath(args.local_path)
+    args.name = args.name if args.name else args.source_path
+    source_path = os.path.abspath(args.source_path)
     evidence_ = evidence.APFSEncryptedDisk(
-        name=args.name, local_path=local_path, recovery_key=args.recovery_key,
+        name=args.name, source_path=source_path, recovery_key=args.recovery_key,
         password=args.password, source=args.source)
   elif args.command == 'bitlocker':
     if not args.password and not args.recovery_key:
       log.error('Neither recovery key nor password is specified.')
       sys.exit(1)
-    args.name = args.name if args.name else args.local_path
-    local_path = os.path.abspath(args.local_path)
+    args.name = args.name if args.name else args.source_path
+    source_path = os.path.abspath(args.source_path)
     evidence_ = evidence.BitlockerDisk(
-        name=args.name, local_path=local_path, recovery_key=args.recovery_key,
+        name=args.name, source_path=source_path, recovery_key=args.recovery_key,
         password=args.password, source=args.source)
   elif args.command == 'directory':
-    args.name = args.name if args.name else args.local_path
-    local_path = os.path.abspath(args.local_path)
+    args.name = args.name if args.name else args.source_path
+    source_path = os.path.abspath(args.source_path)
     evidence_ = evidence.Directory(
-        name=args.name, local_path=local_path, source=args.source)
+        name=args.name, source_path=source_path, source=args.source)
   elif args.command == 'compressedirectory':
-    archive.ValidateTarFile(args.local_path)
-    args.name = args.name if args.name else args.local_path
-    local_path = os.path.abspath(args.local_path)
+    archive.ValidateTarFile(args.source_path)
+    args.name = args.name if args.name else args.source_path
+    source_path = os.path.abspath(args.source_path)
     evidence_ = evidence.CompressedDirectory(
-        name=args.name, local_path=local_path, source=args.source)
+        name=args.name, source_path=source_path, source=args.source)
   elif args.command == 'googleclouddisk':
     args.name = args.name if args.name else args.disk_name
     evidence_ = evidence.GoogleCloudDisk(
@@ -529,10 +544,16 @@ def main():
         source=args.source)
   elif args.command == 'googleclouddiskembedded':
     args.name = args.name if args.name else args.disk_name
+    parent_evidence_ = evidence.GoogleCloudDisk(
+        name=args.name, disk_name=args.disk_name, project=args.project,
+        mount_partition=args.mount_partition, zone=args.zone,
+        source=args.source)
     evidence_ = evidence.GoogleCloudDiskRawEmbedded(
-        name=args.name, disk_name=args.disk_name,
-        embedded_path=args.embedded_path, mount_partition=args.mount_partition,
-        project=args.project, zone=args.zone, source=args.source)
+        name=args.name, disk_name=args.disk_name, project=args.project,
+        mount_partition=args.mount_partition, zone=args.zone,
+        embedded_path=args.embedded_path,
+        embedded_partition=args.embedded_mount_partition)
+    evidence_.parent_evidence = parent_evidence_
   elif args.command == 'hindsight':
     if args.format not in ['xlsx', 'sqlite', 'jsonl']:
       log.error('Invalid output format.')
@@ -540,16 +561,16 @@ def main():
     if args.browser_type not in ['Chrome', 'Brave']:
       log.error('Browser type not supported.')
       sys.exit(1)
-    args.name = args.name if args.name else args.local_path
-    local_path = os.path.abspath(args.local_path)
+    args.name = args.name if args.name else args.source_path
+    source_path = os.path.abspath(args.source_path)
     evidence_ = evidence.ChromiumProfile(
-        name=args.name, local_path=local_path, output_format=args.format,
+        name=args.name, source_path=source_path, output_format=args.format,
         browser_type=args.browser_type)
   elif args.command == 'rawmemory':
-    args.name = args.name if args.name else args.local_path
-    local_path = os.path.abspath(args.local_path)
+    args.name = args.name if args.name else args.source_path
+    source_path = os.path.abspath(args.source_path)
     evidence_ = evidence.RawMemory(
-        name=args.name, local_path=local_path, profile=args.profile,
+        name=args.name, source_path=source_path, profile=args.profile,
         module_list=args.module_list)
   elif args.command == 'psqworker':
     # Set up root logger level which is normally set by the psqworker command
@@ -610,7 +631,7 @@ def main():
     log.info('Available Jobs:')
     client.list_jobs()
   else:
-    log.warning('Command {0:s} not implemented.'.format(args.command))
+    log.warning('Command {0!s} not implemented.'.format(args.command))
 
   if evidence_ and not args.force_evidence:
     if config.SHARED_FILESYSTEM and evidence_.cloud_only:
diff --git a/turbinia/workers/__init__.py b/turbinia/workers/__init__.py
index 138e1fd..9516f62 100644
--- a/turbinia/workers/__init__.py
+++ b/turbinia/workers/__init__.py
@@ -168,15 +168,14 @@ class TurbiniaTaskResult(object):
     self.status = status
 
     for evidence in self.evidence:
-      if evidence.local_path and os.path.exists(evidence.local_path):
-        self.saved_paths.append(evidence.local_path)
-        if not task.run_local:
-          if evidence.copyable and not config.SHARED_FILESYSTEM:
-            task.output_manager.save_evidence(evidence, self)
+      if evidence.source_path and os.path.exists(evidence.source_path):
+        self.saved_paths.append(evidence.source_path)
+        if not task.run_local and evidence.copyable:
+          task.output_manager.save_evidence(evidence, self)
       else:
         self.log(
-            'Evidence {0:s} has empty or missing file at local_path {1:s} so '
-            'not saving.'.format(evidence.name, evidence.local_path))
+            'Evidence {0!s} has empty or missing file at source_path {1!s} so '
+            'not saving.'.format(evidence.name, evidence.source_path))
 
       if not evidence.request_id:
         evidence.request_id = self.request_id
@@ -420,6 +419,7 @@ class TurbiniaTask(object):
     log_files = log_files if log_files else []
     new_evidence = new_evidence if new_evidence else []
     success_codes = success_codes if success_codes else [0]
+
     if shell:
       proc = subprocess.Popen(cmd, shell=True)
     else:
@@ -440,7 +440,7 @@ class TurbiniaTask(object):
             'Log file {0:s} is empty. Not saving'.format(file_),
             level=logging.DEBUG)
         continue
-      result.log('Output file at {0:s}'.format(file_))
+      result.log('Output log file found at {0:s}'.format(file_))
       if not self.run_local:
         self.output_manager.save_local_file(file_, result)
 
@@ -457,23 +457,23 @@ class TurbiniaTask(object):
               'Output file {0:s} is empty. Not saving'.format(file_),
               level=logging.DEBUG)
           continue
-        result.log('Output file at {0:s}'.format(file_))
+        result.log('Output save file at {0:s}'.format(file_))
         if not self.run_local:
           self.output_manager.save_local_file(file_, result)
 
       for evidence in new_evidence:
         # If the local path is set in the Evidence, we check to make sure that
         # the path exists and is not empty before adding it.
-        if evidence.local_path and not os.path.exists(evidence.local_path):
+        if evidence.source_path and not os.path.exists(evidence.source_path):
           message = (
-              'Evidence {0:s} local_path {1:s} does not exist. Not returning '
-              'empty Evidence.'.format(evidence.name, evidence.local_path))
+              'Evidence {0:s} source_path {1:s} does not exist. Not returning '
+              'empty Evidence.'.format(evidence.name, evidence.source_path))
           result.log(message, level=logging.WARN)
-        elif (evidence.local_path and os.path.exists(evidence.local_path) and
-              os.path.getsize(evidence.local_path) == 0):
+        elif (evidence.source_path and os.path.exists(evidence.source_path) and
+              os.path.getsize(evidence.source_path) == 0):
           message = (
-              'Evidence {0:s} local_path {1:s} is empty. Not returning '
-              'empty new Evidence.'.format(evidence.name, evidence.local_path))
+              'Evidence {0:s} source_path {1:s} is empty. Not returning '
+              'empty new Evidence.'.format(evidence.name, evidence.source_path))
           result.log(message, level=logging.WARN)
         else:
           result.add_evidence(evidence, self._evidence_config)
@@ -511,10 +511,10 @@ class TurbiniaTask(object):
       if evidence.copyable and not config.SHARED_FILESYSTEM:
         self.output_manager.retrieve_evidence(evidence)
 
-    if evidence.local_path and not os.path.exists(evidence.local_path):
+    if evidence.source_path and not os.path.exists(evidence.source_path):
       raise TurbiniaException(
-          'Evidence local path {0:s} does not exist'.format(
-              evidence.local_path))
+          'Evidence source path {0:s} does not exist'.format(
+              evidence.source_path))
     evidence.preprocess(self.tmp_dir)
     return self.result
 
diff --git a/turbinia/workers/analysis/jenkins.py b/turbinia/workers/analysis/jenkins.py
index bec4390..f70b9df 100644
--- a/turbinia/workers/analysis/jenkins.py
+++ b/turbinia/workers/analysis/jenkins.py
@@ -41,15 +41,13 @@ class JenkinsAnalysisTask(TurbiniaTask):
     Returns:
         TurbiniaTaskResult object.
     """
-    # What type of evidence we should output.
-    output_evidence = ReportText()
 
     # Where to store the resulting output file.
     output_file_name = 'jenkins_analysis.txt'
     output_file_path = os.path.join(self.output_dir, output_file_name)
 
-    # Set the output file as the data source for the output evidence.
-    output_evidence.local_path = output_file_path
+    # What type of evidence we should output.
+    output_evidence = ReportText(source_path=output_file_path)
 
     # TODO(aarontp): We should find a more optimal solution for this because
     # this requires traversing the entire filesystem and extracting more files
diff --git a/turbinia/workers/analysis/wordpress.py b/turbinia/workers/analysis/wordpress.py
index 8f05212..94ee869 100644
--- a/turbinia/workers/analysis/wordpress.py
+++ b/turbinia/workers/analysis/wordpress.py
@@ -47,14 +47,11 @@ class WordpressAccessLogAnalysisTask(TurbiniaTask):
     Returns:
       TurbiniaTaskResult object.
     """
-    # What type of evidence we should output.
-    output_evidence = ReportText()
-
     # Where to store the resulting output file.
     output_file_name = 'wp_acces_log_analysis.txt'
     output_file_path = os.path.join(self.output_dir, output_file_name)
     # Set the output file as the data source for the output evidence.
-    output_evidence.local_path = output_file_path
+    output_evidence = ReportText(source_path=output_file_path)
 
     # Change open function if file is GZIP compressed.
     open_function = open
diff --git a/turbinia/workers/artifact.py b/turbinia/workers/artifact.py
index 5a30c83..878ad34 100644
--- a/turbinia/workers/artifact.py
+++ b/turbinia/workers/artifact.py
@@ -34,8 +34,8 @@ class FileArtifactExtractionTask(TurbiniaTask):
     """Extracts artifacts using Plaso image_export.py.
 
     Args:
-        evidence: evidence to be processed.
-        result: A TurbiniaTaskResult object to place task results into.
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
 
     Returns:
         TurbiniaTaskResult object.
@@ -47,6 +47,7 @@ class FileArtifactExtractionTask(TurbiniaTask):
         self.output_dir, '{0:s}.log'.format(self.id))
 
     cmd = [
+        'sudo',
         'image_export.py',
         '--logfile',
         image_export_log,
@@ -75,8 +76,8 @@ class FileArtifactExtractionTask(TurbiniaTask):
     for dirpath, _, filenames in os.walk(export_directory):
       for filename in filenames:
         exported_artifact = ExportedFileArtifact(
-            artifact_name=self.artifact_name)
-        exported_artifact.local_path = os.path.join(dirpath, filename)
+            artifact_name=self.artifact_name, source_path=os.path.join(
+                dirpath, filename))
         result.log('Adding artifact {0:s}'.format(filename))
         result.add_evidence(exported_artifact, evidence.config)
 
diff --git a/turbinia/workers/binary_extractor.py b/turbinia/workers/binary_extractor.py
new file mode 100644
index 0000000..6b89655
--- /dev/null
+++ b/turbinia/workers/binary_extractor.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Task to extract binary files from an evidence object provided."""
+
+from __future__ import unicode_literals
+
+import logging
+import json
+import os
+
+from turbinia import config
+from turbinia.workers import TurbiniaTask
+from turbinia.evidence import BinaryExtraction
+
+
+class BinaryExtractorTask(TurbiniaTask):
+  """Extract binaries out of evidence and provide JSON file with hashes.
+  
+  Attributes:
+    json_path(str): path to output JSON file.
+    binary_extraction_dir(str): path to extraction directory. 
+  """
+
+  def __init__(self, *args, **kwargs):
+    """Initializes BinaryExtractorTask."""
+    super(BinaryExtractorTask, self).__init__(*args, **kwargs)
+    self.json_path = None
+    self.binary_extraction_dir = None
+
+  def check_extraction(self):
+    """Checks counts for extracted binaries and hashes.
+
+    Returns:
+      Tuple(
+        binary_cnt(int): Number of extracted binaries.
+        hash_cnt(int): Number of extracted hashes.
+      )
+    """
+
+    with open(self.json_path) as json_file:
+      hashes = json.load(json_file)
+
+    binary_cnt = sum(
+        len(files) for _, _, files in os.walk(self.binary_extraction_dir)) - 1
+    hash_cnt = len(hashes)
+
+    return (binary_cnt, hash_cnt)
+
+  def run(self, evidence, result):
+    """Task that extracts binaries with image_export.py.
+
+    Args:
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
+
+    Returns:
+        TurbiniaTaskResult object.
+    """
+
+    config.LoadConfig()
+    binary_extraction_evidence = BinaryExtraction()
+
+    binary_extraction_evidence.local_path = self.output_dir
+    image_export_log = os.path.join(self.output_dir, 'binary_extraction.log')
+    self.binary_extraction_dir = os.path.join(
+        self.output_dir, 'extracted_binaries')
+    self.json_path = os.path.join(self.binary_extraction_dir, 'hashes.json')
+
+    cmd = [
+        'image_export.py', '--partitions', 'all', '--no_vss', '--signatures',
+        'elf,exe_mz', '--logfile', image_export_log
+    ]
+    if config.DEBUG_TASKS:
+      cmd.append('-d')
+    cmd.extend(['-w', self.binary_extraction_dir, evidence.local_path])
+
+    result.log('Running image_export as [{0:s}]'.format(' '.join(cmd)))
+    self.execute(
+        cmd, result, log_files=[image_export_log, self.json_path],
+        new_evidence=[binary_extraction_evidence], close=True)
+
+    binary_cnt, hash_cnt = self.check_extraction()
+
+    result.status = (
+        'Extracted {0:d} hashes and {1:d} binaries from the '
+        'evidence.'.format(hash_cnt, binary_cnt))
+
+    if hash_cnt != binary_cnt:
+      result.log(
+          'Number of extracted binaries is not equal to the number '
+          'of extracted hashes. This might indicate issues with '
+          'image_export.py. Check binary_extraction.log for more '
+          'details.', logging.WARNING)
+
+    binary_extraction_evidence.compress()
+
+    return result
diff --git a/turbinia/workers/bulk_extractor.py b/turbinia/workers/bulk_extractor.py
index 4817e46..6704559 100644
--- a/turbinia/workers/bulk_extractor.py
+++ b/turbinia/workers/bulk_extractor.py
@@ -72,6 +72,23 @@ class BulkExtractorTask(TurbiniaTask):
       return result
     return result
 
+  def check_xml_attrib(self, xml_key):
+    """Checks if a key exists within the xml report.
+
+    Args:
+      xml_key(str): the xml key to check for.
+
+    Returns:
+      xml_hit(str): the xml value else return N/A.
+    """
+    xml_hit = 'N/A'
+    xml_search = self.xml.find(xml_key)
+
+    # If exists, return the text value.
+    if xml_search is not None:
+      xml_hit = xml_search.text
+    return xml_hit
+
   def generate_summary_report(self, output_file_path):
     """Generate a summary report from the resulting bulk extractor run.
 
@@ -93,7 +110,7 @@ class BulkExtractorTask(TurbiniaTask):
       return (report, report)
 
     # Parse existing XML file.
-    xml = xml_tree.parse(report_path)
+    self.xml = xml_tree.parse(report_path)
 
     # Place in try/except statement to continue execution when
     # an attribute is not found and NoneType is returned.
@@ -104,23 +121,25 @@ class BulkExtractorTask(TurbiniaTask):
       findings.append(
           fmt.bullet(
               'Program: {0} - {1}'.format(
-                  xml.find('creator/program').text,
-                  xml.find('creator/version').text)))
+                  self.check_xml_attrib('creator/program'),
+                  self.check_xml_attrib('creator/version'))))
       findings.append(
           fmt.bullet(
               'Command Line: {0}'.format(
-                  xml.find('creator/execution_environment/command_line').text)))
+                  self.check_xml_attrib(
+                      'creator/execution_environment/command_line'))))
       findings.append(
           fmt.bullet(
               'Start Time: {0}'.format(
-                  xml.find('creator/execution_environment/start_time').text)))
+                  self.check_xml_attrib(
+                      'creator/execution_environment/start_time'))))
       findings.append(
           fmt.bullet(
               'Elapsed Time: {0}'.format(
-                  xml.find('report/elapsed_seconds').text)))
+                  self.check_xml_attrib('report/elapsed_seconds'))))
 
       # Retrieve results from each of the scanner runs
-      feature_files = xml.find('feature_files')
+      feature_files = self.xml.find('feature_files')
       if feature_files is not None:
         feature_iter = feature_files.iter()
         findings.append(fmt.heading5('Scanner Results'))
diff --git a/turbinia/workers/finalize_request.py b/turbinia/workers/finalize_request.py
index 263ae3c..bb90463 100644
--- a/turbinia/workers/finalize_request.py
+++ b/turbinia/workers/finalize_request.py
@@ -41,12 +41,11 @@ class FinalizeRequestTask(TurbiniaTask):
     """
     # Doing a delayed import to avoid circular dependencies.
     from turbinia.client import TurbiniaClient
-    report = FinalReport()
     client = TurbiniaClient()
 
     report_file = os.path.join(
         self.tmp_dir, 'final_turbinia_report_{0:s}.md'.format(self.id))
-    report.local_path = report_file
+    report = FinalReport(source_path=report_file)
     report_data = client.format_task_status(
         config.INSTANCE_ID, config.TURBINIA_PROJECT, config.TURBINIA_REGION,
         request_id=evidence.request_id, full_report=True)
diff --git a/turbinia/workers/grep.py b/turbinia/workers/grep.py
index f462389..dabcd54 100644
--- a/turbinia/workers/grep.py
+++ b/turbinia/workers/grep.py
@@ -36,7 +36,6 @@ class GrepTask(TurbiniaTask):
     Returns:
         TurbiniaTaskResult object.
     """
-    output_evidence = FilteredTextFile()
 
     patterns = evidence.config.get('filter_patterns')
     if not patterns:
@@ -55,7 +54,7 @@ class GrepTask(TurbiniaTask):
     output_file_path = os.path.join(
         self.output_dir, '{0:s}.filtered'.format(base_name))
 
-    output_evidence.local_path = output_file_path
+    output_evidence = FilteredTextFile(source_path=output_file_path)
     cmd = 'grep -E -b -n -f {0:s} {1:s} > {2:s}'.format(
         patterns_file_path, evidence.local_path, output_file_path)
 
diff --git a/turbinia/workers/hadoop.py b/turbinia/workers/hadoop.py
index 6ac9253..7e8f345 100644
--- a/turbinia/workers/hadoop.py
+++ b/turbinia/workers/hadoop.py
@@ -106,21 +106,19 @@ class HadoopAnalysisTask(TurbiniaTask):
         TurbiniaTaskResult object.
     """
 
-    # What type of evidence we should output.
-    output_evidence = ReportText()
-
     # Where to store the resulting output file.
     output_file_name = 'hadoop_analysis.txt'
     output_file_path = os.path.join(self.output_dir, output_file_name)
 
-    output_evidence.local_path = output_file_path
+    # What type of evidence we should output.
+    output_evidence = ReportText(source_path=output_file_path)
 
     try:
       # We don't use FileArtifactExtractionTask as it export one evidence per
       # file extracted
       output_dir = os.path.join(self.output_dir, 'artifacts')
       collected_artifacts = extract_artifacts(
-          artifact_names=['HadoopAppRoot'], disk_path=evidence.local_path,
+          artifact_names=['HadoopAppRoot'], disk_path=evidence.device_path,
           output_dir=output_dir)
 
       (report, priority, summary) = self._AnalyzeHadoopAppRoot(
diff --git a/turbinia/workers/hindsight.py b/turbinia/workers/hindsight.py
index a00da10..178b821 100644
--- a/turbinia/workers/hindsight.py
+++ b/turbinia/workers/hindsight.py
@@ -29,14 +29,21 @@ class HindsightTask(TurbiniaTask):
     super(HindsightTask, self).__init__()
 
   def run(self, evidence, result):
-    """Task to execute hindsight."""
+    """Task to execute hindsight.
 
-    # Create the new Evidence object that will be generated by this Task.
-    output_evidence = TextFile()
+    Args:
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
+
+    Returns:
+        TurbiniaTaskResult object.
+    """
 
     # Create a path that we can write the new file to.
     output_file_path = os.path.join(self.output_dir, 'hindsight_report')
-    output_evidence.local_path = output_file_path
+
+    # Create the new Evidence object that will be generated by this Task.
+    output_evidence = TextFile(source_path=output_file_path)
 
     # Create a path for the log file
     hindsight_log = os.path.join(self.output_dir, 'hindsight.log')
diff --git a/turbinia/workers/plaso.py b/turbinia/workers/plaso.py
index 0215f09..1c740de 100644
--- a/turbinia/workers/plaso.py
+++ b/turbinia/workers/plaso.py
@@ -32,19 +32,18 @@ class PlasoTask(TurbiniaTask):
     """Task that process data with Plaso.
 
     Args:
-        evidence: Path to data to process.
-        result: A TurbiniaTaskResult object to place task results into.
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
 
     Returns:
         TurbiniaTaskResult object.
     """
     config.LoadConfig()
-    plaso_evidence = PlasoFile()
 
     # Write plaso file into tmp_dir because sqlite has issues with some shared
     # filesystems (e.g NFS).
     plaso_file = os.path.join(self.tmp_dir, '{0:s}.plaso'.format(self.id))
-    plaso_evidence.local_path = plaso_file
+    plaso_evidence = PlasoFile(source_path=plaso_file)
     plaso_log = os.path.join(self.output_dir, '{0:s}.log'.format(self.id))
 
     # TODO(aarontp): Move these flags into a recipe
@@ -69,7 +68,7 @@ class PlasoTask(TurbiniaTask):
         return result
 
     cmd.extend(['--logfile', plaso_log])
-    cmd.extend([plaso_file, evidence.local_path])
+    cmd.extend([plaso_file, evidence.device_path])
 
     result.log('Running plaso as [{0:s}]'.format(' '.join(cmd)))
 
diff --git a/turbinia/workers/psort.py b/turbinia/workers/psort.py
index 7c92af4..d02ec9a 100644
--- a/turbinia/workers/psort.py
+++ b/turbinia/workers/psort.py
@@ -30,23 +30,22 @@ class PsortTask(TurbiniaTask):
     """Task that processes Plaso storage files with Psort.
 
     Args:
-        evidence: Path to data to process.
-        result: A TurbiniaTaskResult object to place task results into.
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
 
     Returns:
         TurbiniaTaskResult object.
     """
     config.LoadConfig()
-    psort_evidence = PlasoCsvFile()
 
     psort_file = os.path.join(self.output_dir, '{0:s}.csv'.format(self.id))
-    psort_evidence.local_path = psort_file
+    psort_evidence = PlasoCsvFile(source_path=psort_file)
     psort_log = os.path.join(self.output_dir, '{0:s}.log'.format(self.id))
 
     cmd = ['psort.py', '--status_view', 'none', '--logfile', psort_log]
     if config.DEBUG_TASKS:
       cmd.append('-d')
-    cmd.extend(['-w', psort_file, evidence.local_path])
+    cmd.extend(['-w', psort_file, evidence.source_path])
 
     result.log('Running psort as [{0:s}]'.format(' '.join(cmd)))
 
diff --git a/turbinia/workers/sshd.py b/turbinia/workers/sshd.py
index c2822e8..36c618a 100644
--- a/turbinia/workers/sshd.py
+++ b/turbinia/workers/sshd.py
@@ -32,20 +32,17 @@ class SSHDAnalysisTask(TurbiniaTask):
     """Run the sshd_config analysis worker.
 
     Args:
-       evidence (Evidence object):  The evidence to process
-       result (TurbiniaTaskResult): The object to place task results into.
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
 
     Returns:
-      TurbiniaTaskResult object.
+        TurbiniaTaskResult object.
     """
-    # What type of evidence we should output.
-    output_evidence = ReportText()
-
     # Where to store the resulting output file.
     output_file_name = 'sshd_config_analysis.txt'
     output_file_path = os.path.join(self.output_dir, output_file_name)
     # Set the output file as the data source for the output evidence.
-    output_evidence.local_path = output_file_path
+    output_evidence = ReportText(source_path=output_file_path)
 
     # Read the input file
     with open(evidence.local_path, 'r') as input_file:
diff --git a/turbinia/workers/strings.py b/turbinia/workers/strings.py
index e55037d..a19a54a 100644
--- a/turbinia/workers/strings.py
+++ b/turbinia/workers/strings.py
@@ -29,25 +29,22 @@ class StringsAsciiTask(TurbiniaTask):
     """Run strings binary.
 
     Args:
-        evidence (Evidence object):  The evidence we will process
+        evidence (Evidence object):  The evidence we will process.
         result (TurbiniaTaskResult): The object to place task results into.
 
     Returns:
         TurbiniaTaskResult object.
     """
-    # Create the new Evidence object that will be generated by this Task.
-    output_evidence = TextFile()
     # Create a path that we can write the new file to.
-    base_name = os.path.basename(evidence.local_path)
+    base_name = os.path.basename(evidence.device_path)
     output_file_path = os.path.join(
         self.output_dir, '{0:s}.ascii'.format(base_name))
-    # Add the output path to the evidence so we can automatically save it
-    # later.
-    output_evidence.local_path = output_file_path
+    # Create the new Evidence object that will be generated by this Task.
+    output_evidence = TextFile(source_path=output_file_path)
 
     # Generate the command we want to run.
     cmd = 'strings -a -t d {0:s} > {1:s}'.format(
-        evidence.local_path, output_file_path)
+        evidence.device_path, output_file_path)
     # Add a log line to the result that will be returned.
     result.log('Running strings as [{0:s}]'.format(cmd))
     # Actually execute the binary
@@ -70,19 +67,16 @@ class StringsUnicodeTask(TurbiniaTask):
     Returns:
         TurbiniaTaskResult object.
     """
-    # Create the new Evidence object that will be generated by this Task.
-    output_evidence = TextFile()
     # Create a path that we can write the new file to.
-    base_name = os.path.basename(evidence.local_path)
+    base_name = os.path.basename(evidence.device_path)
     output_file_path = os.path.join(
         self.output_dir, '{0:s}.uni'.format(base_name))
-    # Add the output path to the evidence so we can automatically save it
-    # later.
-    output_evidence.local_path = output_file_path
+    # Create the new Evidence object that will be generated by this Task.
+    output_evidence = TextFile(source_path=output_file_path)
 
     # Generate the command we want to run.
     cmd = 'strings -a -t d -e l {0:s} > {1:s}'.format(
-        evidence.local_path, output_file_path)
+        evidence.device_path, output_file_path)
     # Add a log line to the result that will be returned.
     result.log('Running strings as [{0:s}]'.format(cmd))
     # Actually execute the binary
diff --git a/turbinia/workers/tomcat.py b/turbinia/workers/tomcat.py
index e2e6281..348c2f2 100644
--- a/turbinia/workers/tomcat.py
+++ b/turbinia/workers/tomcat.py
@@ -30,20 +30,20 @@ class TomcatAnalysisTask(TurbiniaTask):
 
   def run(self, evidence, result):
     """Run the Tomcat analysis worker.
+
     Args:
-       evidence (Evidence object):  The evidence to process
-       result (TurbiniaTaskResult): The object to place task results into.
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
+
     Returns:
-      TurbiniaTaskResult object.
+        TurbiniaTaskResult object.
     """
-    # What type of evidence we should output.
-    output_evidence = ReportText()
 
     # Where to store the resulting output file.
     output_file_name = 'tomcat_analysis.txt'
     output_file_path = os.path.join(self.output_dir, output_file_name)
     # Set the output file as the data source for the output evidence.
-    output_evidence.local_path = output_file_path
+    output_evidence = ReportText(source_path=output_file_path)
 
     # Read the input file
     with open(evidence.local_path, 'r') as input_file:
diff --git a/turbinia/workers/volatility.py b/turbinia/workers/volatility.py
index 6810d21..893f1b0 100644
--- a/turbinia/workers/volatility.py
+++ b/turbinia/workers/volatility.py
@@ -40,7 +40,7 @@ class VolatilityTask(TurbiniaTask):
     """Run volatility against evidence.
 
     Args:
-        evidence (Evidence object): The evidence we will process
+        evidence (Evidence object):  The evidence we will process.
         result (TurbiniaTaskResult): The object to place task results into.
 
     Returns:
@@ -48,13 +48,11 @@ class VolatilityTask(TurbiniaTask):
     """
     config.LoadConfig()
 
-    # Create the new Evidence object that will be generated by this Task.
-    output_evidence = VolatilityReport()
-
     # Create a path that we can write the new file to.
     output_file_path = os.path.join(
         self.output_dir, '{0:s}.txt'.format(self.id))
-    output_evidence.local_path = output_file_path
+    # Create the new Evidence object that will be generated by this Task.
+    output_evidence = VolatilityReport(source_path=output_file_path)
 
     # TODO: Add in config options for Turbinia
     cmd = (
diff --git a/turbinia/workers/worker_stat.py b/turbinia/workers/worker_stat.py
index b21d708..e241ac8 100644
--- a/turbinia/workers/worker_stat.py
+++ b/turbinia/workers/worker_stat.py
@@ -40,10 +40,11 @@ class StatTask(TurbiniaTask):
     Returns:
         TurbiniaTaskResult: object.
     """
-    report = ReportText()
-    result.log('Running stat on evidence {0:s}'.format(evidence.local_path))
-    report.text_data = str(os.stat(evidence.local_path))
-    with open(os.path.join(self.output_dir, 'report.txt'), 'w') as f:
+    result.log('Running stat on evidence {0:s}'.format(evidence.source_path))
+    report_path = os.path.join(self.output_dir, 'report.txt')
+    report = ReportText(source_path=report_path)
+    report.text_data = str(os.stat(evidence.source_path))
+    with open(report_path, 'w') as f:
       f.write(report.text_data)
 
     result.add_evidence(report, evidence.config)


Test patch:
diff --git a/turbinia/evidence_test.py b/turbinia/evidence_test.py
index 9759be1..846ec95 100644
--- a/turbinia/evidence_test.py
+++ b/turbinia/evidence_test.py
@@ -28,20 +28,17 @@ class TestTurbiniaEvidence(unittest.TestCase):
 
   def testEvidenceSerialization(self):
     """Test that evidence serializes/unserializes."""
-    rawdisk = evidence.RawDisk(
-        name='My Evidence', local_path='/tmp/foo', mount_path='/mnt/foo')
+    rawdisk = evidence.RawDisk(name='My Evidence', source_path='/tmp/foo')
     rawdisk_json = rawdisk.to_json()
     self.assertTrue(isinstance(rawdisk_json, str))
 
     rawdisk_new = evidence.evidence_decode(json.loads(rawdisk_json))
     self.assertIsInstance(rawdisk_new, evidence.RawDisk)
     self.assertEqual(rawdisk_new.name, 'My Evidence')
-    self.assertEqual(rawdisk_new.mount_path, '/mnt/foo')
 
   def testEvidenceCollectionDeserialization(self):
     """Test that EvidenceCollection deserializes."""
-    rawdisk = evidence.RawDisk(
-        name='My Evidence', local_path='/tmp/foo', mount_path='/mnt/foo')
+    rawdisk = evidence.RawDisk(name='My Evidence', source_path='/tmp/foo.img')
     collection = evidence.EvidenceCollection()
     collection.name = 'testCollection'
     collection.add_evidence(rawdisk)
@@ -56,13 +53,12 @@ class TestTurbiniaEvidence(unittest.TestCase):
     self.assertIsInstance(rawdisk_new, evidence.RawDisk)
     self.assertEqual(collection_new.name, 'testCollection')
     self.assertEqual(rawdisk_new.name, 'My Evidence')
-    self.assertEqual(rawdisk_new.mount_path, '/mnt/foo')
+    self.assertEqual(rawdisk_new.source_path, '/tmp/foo.img')
 
   def testEvidenceCollectionSerialization(self):
     """Test that EvidenceCollection serializes/unserializes."""
     evidence_ = evidence.EvidenceCollection()
-    rawdisk = evidence.RawDisk(
-        name='My Evidence', local_path='/tmp/foo', mount_path='/mnt/foo')
+    rawdisk = evidence.RawDisk(name='My Evidence', source_path='/tmp/foo.img')
     evidence_.add_evidence(rawdisk)
     serialized_evidence = evidence_.serialize()
     collection_evidence = serialized_evidence['collection'][0]
@@ -81,21 +77,18 @@ class TestTurbiniaEvidence(unittest.TestCase):
 
   def testEvidenceValidation(self):
     """Test successful evidence validation."""
-    rawdisk = evidence.RawDisk(
-        name='My Evidence', local_path='/tmp/foo', mount_path='/mnt/foo')
-    rawdisk.REQUIRED_ATTRIBUTES = ['name', 'local_path', 'mount_path']
+    rawdisk = evidence.RawDisk(name='My Evidence', source_path='/tmp/foo')
+    rawdisk.REQUIRED_ATTRIBUTES = ['name', 'source_path']
     rawdisk.validate()
 
   def testEvidenceValidationEmptyAttribute(self):
     """Test failed evidence validation with an empty attribute."""
-    rawdisk = evidence.RawDisk(
-        name='My Evidence', local_path=None, mount_path='/mnt/foo')
-    rawdisk.REQUIRED_ATTRIBUTES = ['name', 'local_path', 'mount_path']
+    rawdisk = evidence.RawDisk(name='My Evidence', source_path=None)
+    rawdisk.REQUIRED_ATTRIBUTES = ['name', 'source_path']
     self.assertRaises(TurbiniaException, rawdisk.validate)
 
   def testEvidenceValidationNoAttribute(self):
     """Test failed evidence validation with no attribute."""
-    rawdisk = evidence.RawDisk(
-        name='My Evidence', local_path='/tmp/foo', mount_path='/mnt/foo')
+    rawdisk = evidence.RawDisk(name='My Evidence', source_path='/tmp/foo')
     rawdisk.REQUIRED_ATTRIBUTES = ['doesnotexist']
     self.assertRaises(TurbiniaException, rawdisk.validate)
diff --git a/turbinia/pubsub_test.py b/turbinia/pubsub_test.py
index 0ea1065..e4a60da 100644
--- a/turbinia/pubsub_test.py
+++ b/turbinia/pubsub_test.py
@@ -37,8 +37,7 @@ def getTurbiniaRequest():
   """
   request = message.TurbiniaRequest(
       request_id='deadbeef', context={'kw': [1, 2]})
-  rawdisk = evidence.RawDisk(
-      name='My Evidence', local_path='/tmp/foo', mount_path='/mnt/foo')
+  rawdisk = evidence.RawDisk(name='My Evidence', source_path='/tmp/foo')
   request.evidence.append(rawdisk)
   return request
 
@@ -77,7 +76,7 @@ class TestTurbiniaRequest(unittest.TestCase):
 
   def testTurbiniaRequestSerializationBadJSON(self):
     """Tests that TurbiniaRequest will raise error on wrong JSON object."""
-    rawdisk = evidence.RawDisk(name='My Evidence', local_path='/tmp/foo')
+    rawdisk = evidence.RawDisk(name='My Evidence', source_path='/tmp/foo')
     rawdisk_json = rawdisk.to_json()
     self.assertTrue(isinstance(rawdisk_json, str))
 
diff --git a/turbinia/workers/binary_extractor_test.py b/turbinia/workers/binary_extractor_test.py
new file mode 100644
index 0000000..2126669
--- /dev/null
+++ b/turbinia/workers/binary_extractor_test.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# Copyright 2016 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for the Binary Extractor task."""
+
+from __future__ import unicode_literals
+
+import json
+import mock
+import os
+import shutil
+import tempfile
+import unittest
+
+from turbinia import config
+from turbinia import evidence
+from turbinia.evidence import BinaryExtraction
+from turbinia.workers import binary_extractor
+from turbinia.workers.workers_test import TestTurbiniaTaskBase
+from turbinia.workers import TurbiniaTaskResult
+
+
+class BinaryExtractorTaskTest(TestTurbiniaTaskBase):
+  """Test for the Binary Extractor task."""
+
+  def setUp(self):
+    super(BinaryExtractorTaskTest, self).setUp(
+        task_class=binary_extractor.BinaryExtractorTask,
+        evidence_class=BinaryExtraction)
+    self.task.output_dir = self.task.base_output_dir
+    self.setResults(mock_run=False)
+    self.extraction_dir = os.path.join(
+        self.task.output_dir, 'extracted_binaries')
+
+    os.mkdir(self.extraction_dir)
+    hashes = [{
+        "sha256":
+            "553c231c45eda751710eabb479d08668f70464c14e60064190a7ec206f26b5f5",
+        "paths": ["bin/bzcat"]
+    }, {
+        "sha256":
+            "a106276270db8d3fe80a96dbb52f14f23f42a29bea12c68ac0f88d2e916471af",
+        "paths": ["bin/echo", "home/echo"]
+    }, {
+        "sha256":
+            "e21de6c5af94fa9d4e7f3295c8d25b93ab3d2d65982f5ef53c801669cc82dc47",
+        "paths": ["sbin/visudo"]
+    }, {
+        "sha256":
+            "129f4d0e36b38742fdfa8f1ea9a014818e4ce5c41d4a889435aecee58a1c7c39",
+        "paths": ["sbin/tune2fs"]
+    }]
+    with open(os.path.join(self.extraction_dir, 'hashes.json'),
+              'w') as json_file:
+      json.dump(hashes, json_file)
+
+  def tearDown(self):
+    if os.path.exists(self.base_output_dir):
+      shutil.rmtree(self.base_output_dir)
+
+  def testBinaryExtractorRun(self):
+    """Test BinaryExtractor task run."""
+    self.task.execute = mock.MagicMock(return_value=0)
+    result = self.task.run(self.evidence, self.result)
+
+    self.task.execute.assert_called_once()
+    self.assertIsInstance(result, TurbiniaTaskResult)
+
+  def test_check_extraction(self):
+    """Tests the check_extraction method."""
+
+    test_files = {
+        'bin': ['bzcat', 'echo'],
+        'sbin': ['visudo', 'tune2fs'],
+        'home': ['echo']
+    }
+
+    for subfolder, files in test_files.items():
+      os.makedirs(os.path.join(self.extraction_dir, subfolder))
+      for file in files:
+        os.mknod(os.path.join(self.extraction_dir, subfolder, file))
+
+    self.task.json_path = os.path.join(self.extraction_dir, 'hashes.json')
+    self.task.binary_extraction_dir = self.extraction_dir
+
+    binary_cnt, hash_cnt = self.task.check_extraction()
+
+    self.assertEqual(binary_cnt, 5)
+    self.assertEqual(hash_cnt, 4)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/turbinia/workers/bulk_extractor_test.py b/turbinia/workers/bulk_extractor_test.py
index df81094..978e002 100644
--- a/turbinia/workers/bulk_extractor_test.py
+++ b/turbinia/workers/bulk_extractor_test.py
@@ -20,8 +20,8 @@ from io import StringIO
 
 import os
 import unittest
-import mock
 import textwrap
+import mock
 
 from turbinia.evidence import BulkExtractorOutput
 from turbinia.workers import bulk_extractor
@@ -66,7 +66,7 @@ class BulkExtractorTaskTest(TestTurbiniaTaskBase):
         * Program: BULK_EXTRACTOR - 1.6.0-dev
         * Command Line: bulk_extractor /tmp/test-small.img -o /output/test-small.img
         * Start Time: 2019-09-27T16:34:48Z
-        * Elapsed Time: 2.389155
+        * Elapsed Time: N/A
         ##### There are no findings to report.""")
 
     summary_sample = "0 artifacts have been extracted."
@@ -82,9 +82,6 @@ class BulkExtractorTaskTest(TestTurbiniaTaskBase):
             <start_time>2019-09-27T16:34:48Z</start_time>
           </execution_environment>
         </creator>
-        <report>
-          <elapsed_seconds>2.389155</elapsed_seconds>
-        </report>
         </dfxml>""")
 
     str_io = StringIO(xml_sample)
diff --git a/turbinia/workers/workers_test.py b/turbinia/workers/workers_test.py
index 1b45874..b3cb350 100644
--- a/turbinia/workers/workers_test.py
+++ b/turbinia/workers/workers_test.py
@@ -58,10 +58,10 @@ class TestTurbiniaTaskBase(unittest.TestCase):
     self.task.output_manager = mock.MagicMock()
     self.task.output_manager.get_local_output_dirs.return_value = (None, None)
 
-    # Set up Evidence
-    test_artifact_path = tempfile.mkstemp(dir=self.base_output_dir)[1]
-    self.remove_files.append(test_artifact_path)
-    self.evidence = self.evidence_class(local_path=test_artifact_path)
+    # Set up RawDisk Evidence
+    test_disk_path = tempfile.mkstemp(dir=self.base_output_dir)[1]
+    self.remove_files.append(test_disk_path)
+    self.evidence = evidence.RawDisk(source_path=test_disk_path)
 
     # Set up TurbiniaTaskResult
     self.result = TurbiniaTaskResult(base_output_dir=self.base_output_dir)
@@ -246,7 +246,7 @@ class TestTurbiniaTask(TestTurbiniaTaskBase):
 
   @mock.patch('turbinia.workers.subprocess.Popen')
   def testTurbiniaTaskExecuteEvidenceExists(self, popen_mock):
-    """Test execution with new evidence that has valid a local_path."""
+    """Test execution with new evidence that has valid a source_path."""
     cmd = 'test cmd'
     output = ('test stdout', 'test stderr')
 
@@ -257,7 +257,7 @@ class TestTurbiniaTask(TestTurbiniaTaskBase):
     popen_mock.return_value = proc_mock
 
     # Create our evidence local path file
-    with open(self.evidence.local_path, 'w') as evidence_path:
+    with open(self.evidence.source_path, 'w') as evidence_path:
       evidence_path.write('test')
 
     self.task.execute(
@@ -266,7 +266,7 @@ class TestTurbiniaTask(TestTurbiniaTaskBase):
 
   @mock.patch('turbinia.workers.subprocess.Popen')
   def testTurbiniaTaskExecuteEvidenceDoesNotExist(self, popen_mock):
-    """Test execution with new evidence that does not have a local_path."""
+    """Test execution with new evidence that does not have a source_path."""
     cmd = 'test cmd'
     output = ('test stdout', 'test stderr')
 
@@ -276,15 +276,13 @@ class TestTurbiniaTask(TestTurbiniaTaskBase):
     proc_mock.returncode = 0
     popen_mock.return_value = proc_mock
 
-    os.remove(self.evidence.local_path)
-
     self.task.execute(
         cmd, self.result, new_evidence=[self.evidence], close=True)
     self.assertNotIn(self.evidence, self.result.evidence)
 
   @mock.patch('turbinia.workers.subprocess.Popen')
   def testTurbiniaTaskExecuteEvidenceExistsButEmpty(self, popen_mock):
-    """Test execution with new evidence local_path that exists but is empty."""
+    """Test execution with new evidence source_path that exists but is empty."""
     cmd = 'test cmd'
     output = ('test stdout', 'test stderr')
 
@@ -295,8 +293,8 @@ class TestTurbiniaTask(TestTurbiniaTaskBase):
     popen_mock.return_value = proc_mock
 
     # Exists and is empty
-    self.assertTrue(os.path.exists(self.evidence.local_path))
-    self.assertEqual(os.path.getsize(self.evidence.local_path), 0)
+    self.assertTrue(os.path.exists(self.evidence.source_path))
+    self.assertEqual(os.path.getsize(self.evidence.source_path), 0)
 
     self.task.execute(
         cmd, self.result, new_evidence=[self.evidence], close=True)