Repository: google/turbinia
Pull request #361 (2019-03-15 09:51:06)

Add Volatility module
Volatility images can be submitted along with a profile and module to run:

`turbiniactl -C rawmemory -l /home/mwatkins/vol-images/data.lime -P MacSierra_10_12_6_16G29x64 -m mac_pslist`

googlebot: 
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

:memo: **Please visit <https://cla.developers.google.com/> to sign.**

Once you've signed (or fixed any issues), please reply here (e.g. `I signed it!`) and we'll verify it.

----

#### What to do if you already signed the CLA

##### Individual signers

*   It's possible we don't have your GitHub username or you're using a different email address on your commit. Check [your existing CLA data](https://cla.developers.google.com/clas) and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).

##### Corporate signers

*   Your company has a Point of Contact who decides which employees are authorized to participate. Ask your POC to be added to the group of authorized contributors. If you don't know who your Point of Contact is, direct the Google project maintainer to [go/cla#troubleshoot](http://go/cla#troubleshoot) ([Public version](https://opensource.google.com/docs/cla/#troubleshoot)).
*   The email used to register you as an authorized contributor must be the email used for the Git commit. Check [your existing CLA data](https://cla.developers.google.com/clas) and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).
*   The email used to register you as an authorized contributor must also be [attached to your GitHub account](https://github.com/settings/emails).
		

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogle%2Fturbinia%2Fpull%2F361) for more info**.

<!-- need_sender_cla -->
aarontp: Awesome!  I'm travelling tomorrow, but I'll take a closer look when I'm back in the office on Tuesday.
googlebot: We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for all the commit author(s) or Co-authors.  If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA ([login here](https://cla.developers.google.com/) to double check)?  If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google.
In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again. If the bot doesn't comment, it means it doesn't think anything has changed.

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogle%2Fturbinia%2Fpull%2F361) for more info**.

<!-- need_author_cla -->
mwatkins-fb: I signed it!
mwatkins-fb: Whoops, looks like some unwanted commits were included in the last force push. Let me speak with @ericzinnikas about cleaning this up!
aarontp: Ok, np.  Let me know when it's ready for review.

On Wed, Mar 20, 2019 at 3:34 AM Matt Watkins <notifications@github.com>
wrote:

> Whoops, looks like some unwanted commits were included in the last force
> push. Let me speak with @ericzinnikas <https://github.com/ericzinnikas>
> about cleaning this up!
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/google/turbinia/pull/361#issuecomment-474774474>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/ACiyu1W81KpJU2_eynF22iy_NzB3osvLks5vYg6tgaJpZM4b2FZ0>
> .
>

googlebot: CLAs look good, thanks!

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogle%2Fturbinia%2Fpull%2F361) for more info**.

<!-- ok -->
ericzinnikas: Ok, fixed the commit issues. Good to go here. @mwatkins-fb just looks like there are a couple of lint issues.

Patch:
diff --git a/CONTRIBUTING b/CONTRIBUTING
deleted file mode 100644
index 21e4c71..0000000
--- a/CONTRIBUTING
+++ /dev/null
@@ -1,25 +0,0 @@
-Want to contribute? Great! First, read this page (including the small print at the end).
-
-### Before you contribute
-Before we can use your code, you must sign the
-[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)
-(CLA), which you can do online. The CLA is necessary mainly because you own the
-copyright to your changes, even after your contribution becomes part of our
-codebase, so we need your permission to use and distribute your code. We also
-need to be sure of various other things—for instance that you'll tell us if you
-know that your code infringes on other people's patents. You don't have to sign
-the CLA until after you've submitted your code for review and a member has
-approved it, but you must do it before we can put your code into our codebase.
-Before you start working on a larger contribution, you should get in touch with
-us first through the issue tracker with your idea so that we can help out and
-possibly guide you. Coordinating up front makes it much easier to avoid
-frustration later on.
-
-### Code reviews
-All submissions, including submissions by project members, require review. We
-use Github pull requests for this purpose.
-
-### The small print
-Contributions made by corporations are covered by a different agreement than
-the one above, the
-[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..bd3f271
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,2 @@
+# Want to contribute? 
+Great! First read this [page](https://github.com/google/turbinia/blob/master/docs/contributing.md).
diff --git a/docs/install.md b/docs/install.md
index 814fe9f..3227f3f 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -1,10 +1,7 @@
 # High-Level Setup
 
-Turbinia can be run either in the Google Cloud, or on local machines. If you run
-Turbinia on local machines, it will still use
-[Cloud Pub/Sub](https://cloud.google.com/pubsub) and
-[Cloud Functions](https://cloud.google.com/functions) for the client to talk to
-the server, and for the server to talk to the worker nodes.
+Turbinia can be run either in the Google Cloud, on local machines, or in a
+hybrid mode.
 
 ## Local Setup
 
@@ -21,7 +18,7 @@ Turbinia can read Evidence from either cloud Persistent Disks, or from GCS
 objects. Turbinia can also write output to GCS (set the `GCS_OUTPUT_PATH`
 variable in the config). Note that you can operate multiple Turbinia instances
 within the same GCP Project as long as you're careful to make sure your config
-(Pub/Sub topics/subscriptions, output paths, etc) doesn't overlap.
+options (Pub/Sub topics/subscriptions, output paths, etc) don't overlap.
 
 ### Persistent Disks
 
@@ -30,38 +27,83 @@ Cloud. The account you run Turbinia as must have access to the Persistent Disks
 that you want to process. If you add a GoogleCloudDisk Evidence type, the worker
 node will attach the disk automatically before it runs its tasks.
 
-### Google Cloud Storage (GCS) Objects
+### Processing non-cloud disks in the GCP
 
-If you have raw disk images from physical machines or otherwise that you want to
-process in the cloud, you can process them directly from GCS. This can be used
+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.  This can be used
 when the images do not originate from VMs/containers that are running in the
-cloud. You can potentially convert raw images to Persistent Disks, but that
-requires processing the image first, and so using GCS is recommended in this
-case. Processing Evidence directly from GCS requires that you use
-[GCS FUSE](https://cloud.google.com/storage/docs/gcs-fuse) and mount your bucket
-at a common path on each worker node. Once your GCS bucket is mounted, you can
-process these images as 'rawdisk' Evidence.
+cloud.
 
-<!-- TODO(beamcodeup): Document how to use GoogleCloudDiskRawEmbedded. -->
+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.
+
+One last option is to copy the image into GCS and process them directly from
+there, but the other two options are 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.
 
 # Instructions
 
-## GCP Project Setup (Cloud Pub/Sub, Cloud Function, Cloud Datastore)
+## Local Turbinia
+
+The following is one possible configuration and setup for running Turbinia
+locally using Celery and Redis on Ubuntu 18.04. This setup does not require
+Google Cloud Platform, although a configuration to write output to GCS is
+possible.
+
+### 1. Install and configure Turbinia
+
+*    Follow [Core Installation Steps](#core-installation-steps).
+
+### 2. Install additional dependencies
+
+*    `pip install turbinia[worker]`
+*    `pip install turbinia[local]`
+
+### 3. Run Redis and Turbinia
+
+*    Start Redis server on the local machine by following the instructions [here](https://redis.io/topics/quickstart).
+*    Run `turbiniactl -C -S server` to start Turbinia server.
+*    Run `turbiniactl -C celeryworker` to start Turbinia Celery worker.
+
+## GCP Turbinia
+
+The following is a one possible configuration and setup for Turbinia in GCP.
+This is still a rough process and future versions will be containerized.
+
+**NOTE:** When running Turbinia on GCP, it's recommended that you have at least
+two Google Cloud Engine (GCE) instances, one for the server and one or
+more workers. In a small setup, you can also both the server and worker on a
+single instance.
+
+### 1. GCP Project Setup (Cloud Pub/Sub, Cloud Function, Cloud Datastore)
 
-**NOTE:** This section is mandatory for Turbinia running on GCP or local
-machines.
+**NOTE:** This section is mandatory for Turbinia running on GCP.
 
 *   Create or select a Google Cloud Platform project on the
     [Google Developers Console](https://console.developers.google.com)
+*   Select a GCP region that you wish to deploy Turbinia into.  Note
+    that one of the GCP dependencies is Cloud Functions, and that
+    only works in certain regions, so you will need to deploy in one
+    of [those supported regions](https://cloud.google.com/functions/docs/locations).     
 *   Enable
     [Cloud Functions](https://console.cloud.google.com/apis/library/cloudfunctions.googleapis.com)
 *   Follow the
     [instructions](https://cloud.google.com/pubsub/docs/quickstart-console) to:
     *   Enable
         [Cloud Pub/Sub](https://console.cloud.google.com/apis/library/pubsub.googleapis.com)
-    *   Create a new Pub/Sub topic and subscription **(pull type)**
-    *   Please take a note of the topic and subscription name for the
-        configuration steps
+    *   Create a new Pub/Sub topic and subscription **(pull type)**.  These
+        can use the same base name (the part after `topics/` and
+        `subscription/` in the paths).
+    *   Please take a note of the topic name for the configuration steps,
+        as this is what you will set the `PUBSUB_TOPIC` config variable to.
 *   Enable
     [Cloud Datastore](https://console.cloud.google.com/apis/api/datastore.googleapis.com)
     *   Go to Datastore in the cloud console
@@ -69,67 +111,26 @@ machines.
     *   Select the region that your data should be in. No need to create any
         Entities after selecting your region
 
-## Local Turbinia
-
-*   If Turbinia will run on local machines, jump to
-    [Core Installation Steps](#core-installation-steps)
-*   Otherwise, follow [GCP Turbinia](#gcp-turbinia)
-
-## GCP Turbinia
-
-The following is a one possible configuration and setup for Turbinia in GCP.
-This is still a rough process and future versions will be containerized.
-
-**NOTE:** When running Turbinia on GCP, it's recommended that you have at least
-two Google Cloud Engine (GCE) instances, respectively for the server and 1 or
-more workers. In a small setup, you can also both the server and worker on a
-single instance.
-
-### 1. Create a GCE Instance as Server
+### 2. Create a GCE Instance as Server
 
 *   Create a
     [new GCE instance](https://console.cloud.google.com/compute/instances) from
     a recent version of Debian or Ubuntu
-    *   This should work on other Linux flavors, but these are untested. Feel
-        free to test and fix them ;)
+    *   This should work on other Linux flavors, but these are untested.
 *   Follow [Core Installation Steps](#core-installation-steps)
 
-### 2. Create a Google Cloud Storage (GCS) Bucket
-
-**NOTE:** GCS FUSE is used here for convenience to keep scripts and log files,
-but this isn't strictly necessary for Turbinia to run if you have an alternate
-means to save logging data.
+### 3. Create a Google Cloud Storage (GCS) Bucket
 
 *   [Create a new GCS bucket](https://cloud.google.com/storage/docs/creating-buckets)
-    and create directories for `scripts` and `output/logs`
-*   Mount your GCS bucket on your server instance using
-    [GCS FUSE](https://cloud.google.com/storage/docs/gcs-fuse)
-*   cp `turbinia/tools/gcp_init/*.sh` into your locally mounted copy of
-    `$your_bucket/scripts`
-*   Edit the variables in `scripts/start-wrapper.sh` and
-    `scripts/start-turbinia-common.sh` as appropriate
-*   ***NOTE:*** The `start-wrapper.sh` script has a
-    `GOOGLE_APPLICATION_CREDENTIALS` environment var in the middle of the script
-    that needs to be updated
-
-### 3. Create an Instance Template (Prep Work for Worker)
+    and take note of the bucket name as this will be referenced by the
+    `GCS_OUTPUT_PATH` variable.
+
+### 4. Create an Instance Template (Prep Work for Worker)
 
 *   Stop the server instance
 *   Create a new image from the server VM's disk
 *   Create a new Instance Template using the newly created image
-
-### 4. Create a GCE Instance as Worker
-
 *   Create a new Managed Instance Group from the newly created Instance Template
-*   In your worker VM, add a new custom metadata key `startup-script-url`
-    pointing to `gs://$your_bucket/scripts/start-wrapper.sh`
-*   TODO(beamcodeup): Update this section when Issue #132 is resolved.
-*   Upon start, your VM should mount your GCS Bucket, and copy the start scripts
-    into the home directory of the Turbinia user and will then start the
-    Turbinia worker
-*   ***NOTE:*** If the GCS FUSE is used for log files, update the Turbinia
-    config file with a new `LOG_FILE` path to write worker logs directly into
-    GCS.
 
 ## Core Installation Steps
 
@@ -146,7 +147,15 @@ means to save logging data.
         `turbinia ALL=(ALL:ALL) NOPASSWD: ALL`
 *   Log in as turbinia
     *   `su - turbinia`
-*   Continue to [Google Cloud SDK](#2-google-cloud-sdk)
+*   Configure the init scripts to run Turbinia on start by copying the
+    file [turbinia/tools/turbinia@.service](https://github.com/google/turbinia/blob/master/tools/turbinia%40.service)
+    to `/etc/systemd/system/turbinia@server` for the server or
+    `/etc/systemd/system/turbinia@psqworker` for a GCP worker or 
+    `/etc/systemd/system/turbinia@celeryworker` for a local (non-cloud)
+    installation.
+*   If you are running Turbinia locally, skip to
+    [Inside the Virtualenv](#3-inside-the-virtualenv), otherwise continue to
+    [Google Cloud SDK](#2-google-cloud-sdk).
 
 ### 2. Google Cloud SDK
 
@@ -166,10 +175,10 @@ means to save logging data.
         instances
     *   `Service Account User`: Used when attaching disks
     *   `Cloud Functions Developer`: Used by turbiniactl to query task status
-*   Create a new key for your service account, and then point to it with an
-    environment variable:
-    *   `export
-        GOOGLE_APPLICATION_CREDENTIALS="/home/turbinia/turbinia-service-account-creds.json"`
+*   Create a new key for your service account, and save it on server/workers
+    and then configure init
+    scripts in `/etc/systemd/system/turbinia*` to point to it by setting the
+    `GOOGLE_APPLICATION_CREDENTIALS` var similar to `ExecStartPre=+/bin/sh -c '/bin/systemctl set-environment GOOGLE_APPLICATION_CREDENTIALS="/home/turbinia/turbinia-service-account-creds.json"'`
 *   Add the service account to the gcloud auth **(RECOMMENDED)**
     *   `gcloud auth list`
     *   `gcloud auth activate-service-account
@@ -178,52 +187,37 @@ means to save logging data.
     RECOMMENDED)**
     *   Run `gcloud auth login` (may require you to copy/paste url to browser)
     *   Or run `gcloud auth application-default login`
-*   Continue to [Inside the Virtualenv](#3-inside-the-virtualenv)
-
-### 3. Inside the Virtualenv
-
-#### Create a Virtualenv Instance
-
-*   Create (once) and activate Virtualenv
-    *   `virtualenv turbinia-env && . turbinia-env/bin/activate`
-    *   ***NOTE:*** The next time you need to use virtualenv, just log in as
-        turbinia and activiate virtualenv without recreating it
-*   Do not exit the Virtualenv until you have completed all the steps!
+*   Continue to [Build and Configure](#3-build-and-configure)
 
-#### Build and Configure
+### 3. Build and Configure
 
 *   Install Turbinia
-    *   `git clone https://github.com/google/turbinia.git`
-    *   `pip install --upgrade pip`
-    *   `pip install -r turbinia/requirements.txt`
+    *   `pip install turbinia` for the server
+    *   `pip install turbinia[worker]` for the worker
+    *   `pip install turbinia[dev]` if you want to run tests or get the
+        development dependencies.
 *   Install Plaso
-    *   `curl -o plaso-requirements.txt
-        https://raw.githubusercontent.com/log2timeline/plaso/master/requirements.txt`
-    *   `pip install -r plaso-requirements.txt`
-*   Update Python Path
-    *   Until [Issue #39](https://github.com/google/turbinia/issues/39) is
-        fixed, we need to adjust the `$PYTHONPATH` from the root of the Turbinia
-        repository:
-        *   ``cd turbinia; export PYTHONPATH=$PYTHONPATH:`pwd`; cd -``
-    *   And set it in your .bashrc file:
-        *   `cd turbinia; echo "export PYTHONPATH=$PYTHONPATH" >> ~/.bashrc ; cd
-            -`
-*   Choose one of the locations for storing the Turbinia config and configure
-    from there:
-    *   `/etc/turbinia/turbinia.conf` **(RECOMMENDED)**
-        *   `sudo mkdir /etc/turbinia`
-        *   `cp <localgitpath>/turbinia/config/turbinia_config.py
-            /etc/turbinia/turbinia.conf`
-    *   `/home/$USER/.turbiniarc`
-        *   `cp <localgitpath>/turbinia/config/turbinia_config.py
-            /home/$USER/.turbiniarc`
-    *   Directly configure `<localgitpath>/turbinia/config/turbinia_config.py`
+    *   You can install Plaso from the [GIFT PPA](https://launchpad.net/~gift/+archive/ubuntu/stable),
+        or [see here](https://github.com/log2timeline/plaso/blob/master/docs/sources/user/Users-Guide.md)
+        for other packaged installation.
+*   Create and configure the Turbinia configuration file in `/etc/turbinia/turbinia.conf`.
+    *   `sudo mkdir /etc/turbinia`
+    *   `wget -nd https://raw.githubusercontent.com/google/turbinia/master/turbinia/config/turbinia_config.py -O /etc/turbinia/turbinia.conf`
+        * ***Note*** This is the configuration file from HEAD, but
+          make sure to copy the correct version for the release you
+          are installing.
+    *   Alternately you can either put the file in `/home/$USER/.turbiniarc`
+        or in another directory and then point the `TURBINIA_CONFIG_PATH`
+        environment variable to that directory.
     *   ***NOTE***: Match the `PUBSUB_TOPIC` variable in the configuration to
-        the name of the topic and subscription you created in the GCP.
-*   Continue to [Deploy the Cloud Functions](#deploy-the-cloud-functions)
+        the name of the topic you created in GCP.
+    *   ***NOTE***: If you are running Turbinia locally, make sure to set `GCS_OUTPUT_PATH` to `None`.
+*   If you are running Turbinia locally, return to 
+    [Install additional dependencies](#2-install-additional-dependencies),
+    otherwise continue to [Deploy the Cloud Functions](#deploy-the-cloud-functions).
 
 #### Deploy the Cloud Functions
 
 *   `cd <localgitpath>/turbinia/tools/gcf_init && ./deploy_gcf.py`
-*   If you're doing GCP Turbinia, don't forget to return to
-    [GCP Turbinia](#gcp-turbinia) and finish the rest of it
+*   If you're configuring Turbinia for GCP, don't forget to return to
+    [GCP Turbinia](#gcp-turbinia) and finish the rest of the installation.
diff --git a/requirements.txt b/requirements.txt
index 51eef82..4441358 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,10 +1,10 @@
 filelock
-google-api-core>=1.2.1,<=1.4.0
-google-cloud-datastore==1.7.0
-google-api-python-client>=1.6.2
-google-cloud-pubsub==0.35.4
-google-cloud-storage==1.13.0
-google-cloud-core==0.28.1
+google-api-core
+google-cloud-datastore
+google-api-python-client
+google-cloud-pubsub
+google-cloud-storage
+google-cloud-core
 oauth2client
 psq
 six
diff --git a/tools/gcf_init/index.js b/tools/gcf_init/index.js
index 2919aca..2c07363 100644
--- a/tools/gcf_init/index.js
+++ b/tools/gcf_init/index.js
@@ -67,7 +67,7 @@ exports.gettasks = function gettasks(req, res) {
   }
   if (req.body.user) {
     console.log('Getting Turbinia Tasks by user: ' + req.body.user);
-    query = query.filter('user', '=', req.body.user);
+    query = query.filter('requester', '=', req.body.user);
   }
   if (req.body.start_time) {
     try {
@@ -150,8 +150,8 @@ exports.closetasks = function closetasks(req, res) {
         '__key__', '=', datastore.key([turbiniaKind, req.body.task_id]))
   }
   if (req.body.user) {
-    console.log('Adding filter - user: ' + req.body.user);
-    query = query.filter('user', '=', req.body.user)
+    console.log('Adding filter - requester: ' + req.body.user);
+    query = query.filter('requester', '=', req.body.user)
   }
 
   console.log(query);
diff --git a/tools/gcf_init/index.yaml b/tools/gcf_init/index.yaml
index c609e9e..6d617f8 100644
--- a/tools/gcf_init/index.yaml
+++ b/tools/gcf_init/index.yaml
@@ -45,7 +45,7 @@ indexes:
 - kind: TurbiniaTask
   ancestor: no
   properties:
-  - name: user
+  - name: requester
   - name: last_update
     direction: desc
 
@@ -54,7 +54,7 @@ indexes:
   properties:
   - name: instance
   - name: id
-  - name: user
+  - name: requester
   - name: last_update
     direction: desc
 
@@ -63,7 +63,7 @@ indexes:
   properties:
   - name: instance
   - name: request_id
-  - name: user
+  - name: requester
   - name: last_update
     direction: desc
 
diff --git a/turbinia/client.py b/turbinia/client.py
index d736023..b6d189c 100644
--- a/turbinia/client.py
+++ b/turbinia/client.py
@@ -31,6 +31,7 @@ from turbinia.config import logger
 from turbinia import task_manager
 from turbinia import TurbiniaException
 from turbinia.lib import text_formatter as fmt
+from turbinia.workers import Priority
 from turbinia.workers.artifact import FileArtifactExtractionTask
 from turbinia.workers.analysis.wordpress import WordpressAccessLogAnalysisTask
 from turbinia.workers.analysis.jenkins import JenkinsAnalysisTask
@@ -42,6 +43,7 @@ from turbinia.workers.sshd import SSHDAnalysisTask
 from turbinia.workers.strings import StringsAsciiTask
 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
 
 # TODO(aarontp): Remove this map after
@@ -58,6 +60,7 @@ TASK_MAP = {
     'stringsasciitask': StringsAsciiTask,
     'stringsunicodetask': StringsUnicodeTask,
     'tomcatanalysistask': TomcatAnalysisTask,
+    'volatilitytask': VolatilityTask,
     'stattask': StatTask,
 }
 
@@ -161,12 +164,13 @@ class TurbiniaClient(object):
       user (string): The user of the request we want tasks for.
       poll_interval (int): Interval of seconds between polling cycles.
     """
+    last_completed_count = -1
+    last_uncompleted_count = -1
     while True:
       task_results = self.get_task_data(
           instance, project, region, request_id=request_id, user=user)
       completed_tasks = []
       uncompleted_tasks = []
-      last_count = 0
       for task in task_results:
         if task.get('successful') is not None:
           completed_tasks.append(task)
@@ -185,12 +189,14 @@ class TurbiniaClient(object):
           'Tasks completed ({0:d}/{1:d}): [{2:s}], waiting for [{3:s}].'.format(
               len(completed_tasks), total_count, completed_names,
               uncompleted_names))
-      if len(completed_tasks) > last_count:
+      if (len(completed_tasks) > last_completed_count or
+          len(uncompleted_tasks) > last_uncompleted_count):
         log.info(msg)
       else:
         log.debug(msg)
 
-      last_count = len(completed_tasks)
+      last_completed_count = len(completed_tasks)
+      last_uncompleted_count = len(uncompleted_tasks)
       time.sleep(poll_interval)
 
     log.info('All {0:d} Tasks completed'.format(len(task_results)))
@@ -304,7 +310,8 @@ class TurbiniaClient(object):
 
   def format_task_status(
       self, instance, project, region, days=0, task_id=None, request_id=None,
-      user=None, all_fields=False, full_report=False, priority_filter=20):
+      user=None, all_fields=False, full_report=False,
+      priority_filter=Priority.HIGH):
     """Formats the recent history for Turbinia Tasks.
 
     Args:
@@ -328,8 +335,14 @@ class TurbiniaClient(object):
     """
     task_results = self.get_task_data(
         instance, project, region, days, task_id, request_id, user)
+    if not task_results:
+      return ''
     # Sort all tasks by the report_priority so that tasks with a higher
     # priority are listed first in the report.
+    for result in task_results:
+      # 0 is a valid value, so checking against specific values
+      if result.get('report_priority') in (None, ''):
+        result['report_priority'] = Priority.LOW
     task_results = sorted(task_results, key=itemgetter('report_priority'))
     num_results = len(task_results)
     if not num_results:
@@ -339,7 +352,7 @@ class TurbiniaClient(object):
 
     # Build up data
     report = []
-    user = task_results[0].get('user')
+    requester = task_results[0].get('requester')
     request_id = task_results[0].get('request_id')
     success_types = ['Successful', 'Failed', 'Scheduled or Running']
     success_values = [True, False, None]
@@ -358,7 +371,8 @@ class TurbiniaClient(object):
     report.append(fmt.heading1('Turbinia report {0:s}'.format(request_id)))
     report.append(
         fmt.bullet(
-            'Processed {0:d} Tasks for user {1:s}'.format(num_results, user)))
+            'Processed {0:d} Tasks for user {1:s}'.format(
+                num_results, requester)))
 
     # Print report data for tasks
     for success_type in success_types:
diff --git a/turbinia/config/turbinia_config.py b/turbinia/config/turbinia_config.py
index 6318f7a..2ea8426 100644
--- a/turbinia/config/turbinia_config.py
+++ b/turbinia/config/turbinia_config.py
@@ -26,10 +26,14 @@ from __future__ import unicode_literals
 # separate when running with the same Cloud projects or backend servers.
 INSTANCE_ID = 'turbinia-instance1'
 
-# Which state manager to use. Valid options are 'Datastore' or 'Redis'.
+# Which state manager to use. Valid options are 'Datastore' or 'Redis'.  Use
+# 'Datastore' for Cloud (GCP) or hybrid installations, and 'Redis' for local
+# installations.
 STATE_MANAGER = 'Datastore'
 
-# Which Task manager to use. Valid options are 'PSQ' and 'Celery'.
+# Which Task manager to use. Valid options are 'PSQ' and 'Celery'.  Use 'PSQ'
+# for Cloud (GCP) or hybrid installations, and 'Celery' for local
+# installations.
 TASK_MANAGER = 'PSQ'
 
 # Default base output directory for worker results and evidence.
@@ -98,7 +102,8 @@ PUBSUB_TOPIC = INSTANCE_ID
 
 # GCS Path to copy worker results and Evidence output to.
 # Otherwise, set this as 'None' if output will be stored in shared storage.
-GCS_OUTPUT_PATH = 'gs://%s/output' % BUCKET_NAME
+# GCS_OUTPUT_PATH = 'gs://%s/output' % BUCKET_NAME
+GCS_OUTPUT_PATH = None
 
 ################################################################################
 #                           Celery / Redis / Kombu
@@ -107,13 +112,13 @@ GCS_OUTPUT_PATH = 'gs://%s/output' % BUCKET_NAME
 ################################################################################
 
 # Method for communication between nodes
-CELERY_BROKER = None
+CELERY_BROKER = 'redis://localhost'
 
 # Storage for task results/status
-CELERY_BACKEND = None
+CELERY_BACKEND = 'redis://localhost'
 
 # Can be the same as CELERY_BROKER
-KOMBU_BROKER = None
+KOMBU_BROKER = CELERY_BROKER
 
 # Used to namespace communications.
 KOMBU_CHANNEL = '%s-kombu' % INSTANCE_ID
@@ -123,6 +128,6 @@ KOMBU_CHANNEL = '%s-kombu' % INSTANCE_ID
 KOMBU_DURABLE = True
 
 # Use Redis for state management
-REDIS_HOST = None
-REDIS_PORT = None
-REDIS_DB = None
+REDIS_HOST = 'localhost'
+REDIS_PORT = '6379'
+REDIS_DB = '0'
diff --git a/turbinia/evidence.py b/turbinia/evidence.py
index e5326a4..745353c 100644
--- a/turbinia/evidence.py
+++ b/turbinia/evidence.py
@@ -265,6 +265,24 @@ class BitlockerDisk(EncryptedDisk):
     self.encryption_type = self.__class__.__name__
 
 
+class APFSEncryptedDisk(EncryptedDisk):
+  """APFS encrypted disk file evidence.
+
+  Attributes:
+    recovery_key: A string of the recovery key for this disk
+    password: A string of the password used for this disk. If recovery key
+        is used, this argument is ignored
+    unencrypted_path: A string to the unencrypted local path
+  """
+
+  def __init__(self, recovery_key=None, password=None, *args, **kwargs):
+    """Initialization for Bitlocker disk evidence object"""
+    self.recovery_key = recovery_key
+    self.password = password
+    super(APFSEncryptedDisk, self).__init__(*args, **kwargs)
+    self.encryption_type = self.__class__.__name__
+
+
 class GoogleCloudDisk(RawDisk):
   """Evidence object for Google Cloud Disks.
 
@@ -374,3 +392,23 @@ class ExportedFileArtifact(Evidence):
     super(ExportedFileArtifact, self).__init__()
     self.artifact_name = artifact_name
     self.copyable = True
+
+
+class VolatilityReport(TextFile):
+  """Volatility output file data."""
+  pass
+
+
+class RawMemory(Evidence):
+  """Evidence object for Memory based evidence.
+
+  Attributes:
+    profile (string): Volatility profile used for the analysis
+    module_list (list): Module used for the analysis
+    """
+
+  def __init__(self, module_list=None, profile=None, *args, **kwargs):
+    """Initialization for raw memory evidence object."""
+    super(RawMemory, self).__init__(*args, **kwargs)
+    self.profile = profile
+    self.module_list = module_list
diff --git a/turbinia/jobs/__init__.py b/turbinia/jobs/__init__.py
index 93f08b4..fb238ea 100644
--- a/turbinia/jobs/__init__.py
+++ b/turbinia/jobs/__init__.py
@@ -22,4 +22,5 @@ from turbinia.jobs import psort
 from turbinia.jobs import sshd
 from turbinia.jobs import strings
 from turbinia.jobs import tomcat
+from turbinia.jobs import volatility
 from turbinia.jobs import worker_stat
diff --git a/turbinia/jobs/jenkins.py b/turbinia/jobs/jenkins.py
index 514a85f..c3a961b 100644
--- a/turbinia/jobs/jenkins.py
+++ b/turbinia/jobs/jenkins.py
@@ -22,6 +22,7 @@ from turbinia.evidence import GoogleCloudDisk
 from turbinia.evidence import GoogleCloudDiskRawEmbedded
 from turbinia.evidence import ReportText
 from turbinia.jobs import interface
+from turbinia.jobs import manager
 from turbinia.workers.analysis.jenkins import JenkinsAnalysisTask
 
 
@@ -46,3 +47,6 @@ class JenkinsAnalysisJob(interface.TurbiniaJob):
     """
     tasks = [JenkinsAnalysisTask() for _ in evidence]
     return tasks
+
+
+manager.JobsManager.RegisterJob(JenkinsAnalysisJob)
diff --git a/turbinia/jobs/plaso.py b/turbinia/jobs/plaso.py
index c5aa4ca..7f1dd56 100644
--- a/turbinia/jobs/plaso.py
+++ b/turbinia/jobs/plaso.py
@@ -16,6 +16,7 @@
 
 from __future__ import unicode_literals
 
+from turbinia.evidence import APFSEncryptedDisk
 from turbinia.evidence import BitlockerDisk
 from turbinia.evidence import Directory
 from turbinia.evidence import GoogleCloudDisk
@@ -32,7 +33,7 @@ class PlasoJob(interface.TurbiniaJob):
   # The types of evidence that this Job will process
   evidence_input = [
       Directory, RawDisk, GoogleCloudDisk, GoogleCloudDiskRawEmbedded,
-      BitlockerDisk
+      BitlockerDisk, APFSEncryptedDisk
   ]
   evidence_output = [PlasoFile]
 
diff --git a/turbinia/jobs/volatility.py b/turbinia/jobs/volatility.py
new file mode 100644
index 0000000..8dd1d0e
--- /dev/null
+++ b/turbinia/jobs/volatility.py
@@ -0,0 +1,55 @@
+# -*- 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 volatility tasks."""
+
+from __future__ import unicode_literals
+
+from turbinia.evidence import RawMemory
+from turbinia.evidence import VolatilityReport
+from turbinia.jobs import interface
+from turbinia.jobs import manager
+from turbinia.workers.volatility import VolatilityTask
+
+
+class VolatilityJob(interface.TurbiniaJob):
+  """Volatility analysis job.
+
+  This will generate a Volatility task for every module per each peice of
+  evidence.
+  """
+
+  evidence_input = [RawMemory]
+  evidence_output = [VolatilityReport]
+
+  NAME = 'VolatilityJob'
+
+  def create_tasks(self, evidence):
+    """Create task for Volatility.
+
+    Args:
+      evidence: List of evidence objects to process
+
+    Returns:
+      A list of tasks to schedule.
+    """
+
+    tasks = []
+    for evidence_item in evidence:
+      for mod in evidence_item.module_list:
+        tasks.append(VolatilityTask(mod))
+    return tasks
+
+
+manager.JobsManager.RegisterJob(VolatilityJob)
diff --git a/turbinia/lib/libcloudforensics.py b/turbinia/lib/libcloudforensics.py
index bd5a7b4..612dcd9 100644
--- a/turbinia/lib/libcloudforensics.py
+++ b/turbinia/lib/libcloudforensics.py
@@ -270,7 +270,8 @@ class GoogleCloudProject(object):
     # Max length of disk names in GCP is 63 characters
     project_id = snapshot.project.project_id
     disk_id = project_id + snapshot.disk.name
-    disk_id_crc32 = '{0:08x}'.format(binascii.crc32(disk_id) & 0xffffffff)
+    disk_id_crc32 = '{0:08x}'.format(
+        binascii.crc32(disk_id.encode()) & 0xffffffff)
     truncate_at = 62 - len(disk_id_crc32) - len('-copy') - len(project_id)
     if disk_name_prefix:
       disk_name_prefix += '-'
@@ -330,8 +331,9 @@ class GoogleCloudProject(object):
 
     # Analysis software to install.
     packages_to_install = [
-        'python-plaso', 'xmount', 'libfvde-tools', 'libbde-tools',
-        'plaso-tools', 'jq', 'ncdu', 'htop', 'binutils', 'upx-ucl'
+        'python-plaso', 'xmount', 'sleuthkit', 'libfvde-tools', 'libbde-tools',
+        'plaso-tools', 'jq', 'ncdu', 'htop', 'binutils', 'upx-ucl',
+        'docker-explorer-tools'
     ]
 
     startup_script = """
diff --git a/turbinia/lib/utils.py b/turbinia/lib/utils.py
index f943145..a1930cf 100644
--- a/turbinia/lib/utils.py
+++ b/turbinia/lib/utils.py
@@ -27,6 +27,37 @@ from turbinia import TurbiniaException
 log = logging.getLogger('turbinia')
 
 
+def _image_export(command, output_dir):
+  """Runs image_export command.
+
+  Args:
+    file_name: Name of file (without path) to be extracted.
+    output_dir: Path to directory to store the the extracted files.
+
+  Returns:
+    dict: file names and paths to extracted files.
+
+  Raises:
+    TurbiniaException: If an error occurs when running image_export.
+  """
+  # TODO: Consider using the exec helper to gather stdin/err.
+  log.debug('Running image_export as [{0:s}]'.format(' '.join(command)))
+  try:
+    subprocess.check_call(command)
+  except subprocess.CalledProcessError:
+    raise TurbiniaException('image_export.py failed.')
+
+  collected_file_paths = []
+  file_count = 0
+  for dirpath, _, filenames in os.walk(output_dir):
+    for filename in filenames:
+      collected_file_paths.append(os.path.join(dirpath, filename))
+      file_count += 1
+
+  log.debug('Collected {0:d} files with image_export'.format(file_count))
+  return collected_file_paths
+
+
 def extract_artifacts(artifact_names, disk_path, output_dir):
   """Extract artifacts using image_export from Plaso.
 
@@ -37,29 +68,39 @@ def extract_artifacts(artifact_names, disk_path, output_dir):
 
   Returns:
     dict: file names and paths to extracted files.
+
+  Raises:
+    TurbiniaException: If an error occurs when running image_export.
   """
   # 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
   ]
 
-  # TODO: Consider using the exec helper to gather stdin/err.
-  log.debug(
-      'Running image_export as [{0:s}]'.format(' '.join(image_export_cmd)))
-  try:
-    subprocess.check_call(image_export_cmd)
-  except subprocess.CalledProcessError:
-    raise TurbiniaException('image_export.py failed.')
+  return _image_export(image_export_cmd, output_dir)
 
-  collected_file_paths = []
-  for dirpath, _, filenames in os.walk(output_dir):
-    for filename in filenames:
-      collected_file_paths.append(os.path.join(dirpath, filename))
 
-  return collected_file_paths
+def extract_files(file_name, disk_path, output_dir):
+  """Extract files using image_export from Plaso.
+
+  Args:
+    file_name: Name of file (without path) to be extracted.
+    disk_path: Path to either a raw disk image or a block device.
+    output_dir: Path to directory to store the the extracted files.
+
+  Returns:
+    dict: file names and paths to extracted files.
+
+  Raises:
+    TurbiniaException: If an error occurs when running image_export.
+  """
+  image_export_cmd = [
+      'image_export.py', '--name', file_name, '--write', output_dir,
+      '--partitions', 'all', disk_path
+  ]
+  return _image_export(image_export_cmd, output_dir)
 
 
 def bruteforce_password_hashes(password_hashes, timeout=300):
diff --git a/turbinia/message.py b/turbinia/message.py
index 3ee736e..f9b3708 100644
--- a/turbinia/message.py
+++ b/turbinia/message.py
@@ -35,16 +35,19 @@ class TurbiniaRequest(object):
   """An object to request evidence to be processed.
 
   Attributes:
-    request_id: A client specified ID for this request.
-    recipe: Recipe to use when processing this request.
-    context: A Dict of context data to be passed around with this request.
-    evidence: A list of Evidence objects.
+    request_id(str): A client specified ID for this request.
+    requestor(str): The username of who made the request.
+    recipe(dict): Recipe to use when processing this request.
+    context(dict): A Dict of context data to be passed around with this request.
+    evidence(list): A list of Evidence objects.
   """
 
   def __init__(
-      self, request_id=None, recipe=None, context=None, evidence_=None):
+      self, request_id=None, requester=None, recipe=None, context=None,
+      evidence_=None):
     """Initialization for TurbiniaRequest."""
     self.request_id = request_id if request_id else uuid.uuid4().hex
+    self.requester = requester if requester else 'user_unspecified'
     self.recipe = recipe if recipe else {}
     self.context = context if context else {}
     self.evidence = evidence_ if evidence_ else []
diff --git a/turbinia/output_manager.py b/turbinia/output_manager.py
index 8ab388d..0c1c9e4 100644
--- a/turbinia/output_manager.py
+++ b/turbinia/output_manager.py
@@ -27,8 +27,9 @@ from turbinia import config
 from turbinia import TurbiniaException
 
 config.LoadConfig()
-if config.TASK_MANAGER.lower() == 'psq':
+if config.GCS_OUTPUT_PATH and config.GCS_OUTPUT_PATH.lower() is not 'none':
   from google.cloud import storage
+  from google.cloud import exceptions
 
 log = logging.getLogger('turbinia')
 
@@ -251,6 +252,10 @@ class OutputWriter(object):
 
     Returns:
       The path the file was saved to, or None if file was not written.
+
+    Raises:
+      TurbiniaException: When the source file is empty or there are problems
+          saving the file.
     """
     raise NotImplementedError
 
@@ -265,6 +270,9 @@ class OutputWriter(object):
 
     Returns:
       The path the file was saved to, or None if file was not written.
+
+    Raises:
+      TurbiniaException: When there are problems copying from storage.
     """
     raise NotImplementedError
 
@@ -292,12 +300,12 @@ class LocalOutputWriter(OutputWriter):
       try:
         log.debug('Creating new directory {0:s}'.format(output_dir))
         os.makedirs(output_dir)
-      except OSError as e:
-        if e.errno == errno.EACCES:
-          msg = 'Permission error ({0:s})'.format(str(e))
+      except OSError as exception:
+        if exception.errno == errno.EACCES:
+          message = 'Permission error ({0:s})'.format(str(exception))
         else:
-          msg = str(e)
-        raise TurbiniaException(msg)
+          message = str(exception)
+        raise TurbiniaException(message)
 
     return output_dir
 
@@ -383,13 +391,25 @@ class GCSOutputWriter(OutputWriter):
     pass
 
   def copy_to(self, source_path):
+    if os.path.getsize(source_path) == 0:
+      message = (
+          'Local source file {0:s} is empty.  Not uploading to GCS'.format(
+              source_path))
+      log.error(message)
+      raise TurbiniaException(message)
+
     bucket = self.client.get_bucket(self.bucket)
     destination_path = os.path.join(
         self.base_output_dir, self.unique_dir, os.path.basename(source_path))
     log.info(
         'Writing {0:s} to GCS path {1:s}'.format(source_path, destination_path))
-    blob = storage.Blob(destination_path, bucket, chunk_size=self.CHUNK_SIZE)
-    blob.upload_from_filename(source_path, client=self.client)
+    try:
+      blob = storage.Blob(destination_path, bucket, chunk_size=self.CHUNK_SIZE)
+      blob.upload_from_filename(source_path, client=self.client)
+    except exceptions.GoogleCloudError as exception:
+      message = 'File upload to GCS failed: {0!s}'.format(exception)
+      log.error(message)
+      raise TurbiniaException(message)
     return os.path.join('gs://', self.bucket, destination_path)
 
   def copy_from(self, source_path):
@@ -414,10 +434,24 @@ class GCSOutputWriter(OutputWriter):
     log.info(
         'Writing GCS file {0:s} to local path {1:s}'.format(
             source_path, destination_path))
-    blob = storage.Blob(gcs_path, bucket, chunk_size=self.CHUNK_SIZE)
-    blob.download_to_filename(destination_path, client=self.client)
+    try:
+      blob = storage.Blob(gcs_path, bucket, chunk_size=self.CHUNK_SIZE)
+      blob.download_to_filename(destination_path, client=self.client)
+    except exceptions.RequestRangeNotSatisfiable as exception:
+      message = (
+          'File retrieval from GCS failed, file may be empty: {0!s}'.format(
+              exception))
+      log.error(message)
+      raise TurbiniaException(message)
+    except exceptions.GoogleCloudError as exception:
+      message = 'File retrieval from GCS failed: {0!s}'.format(exception)
+      log.error(message)
+      raise TurbiniaException(message)
+
     if not os.path.exists(destination_path):
-      raise TurbiniaException(
+      message = (
           'File retrieval from GCS failed: Local file {0:s} does not '
           'exist'.format(destination_path))
+      log.error(message)
+      raise TurbiniaException(message)
     return destination_path
diff --git a/turbinia/task_manager.py b/turbinia/task_manager.py
index af6ecd1..df22a5c 100644
--- a/turbinia/task_manager.py
+++ b/turbinia/task_manager.py
@@ -168,6 +168,7 @@ class BaseTaskManager(object):
         job_count += 1
         for task in job.create_tasks([evidence_]):
           task.base_output_dir = config.OUTPUT_DIR
+          task.requester = evidence_.config.get('requester')
           self.add_task(task, evidence_)
 
     if not job_count:
@@ -352,6 +353,7 @@ class CeleryTaskManager(BaseTaskManager):
         if not evidence_.request_id:
           evidence_.request_id = request.request_id
         evidence_.config = request.recipe
+        evidence_.config['requester'] = request.requester
         log.info(
             'Received evidence [{0:s}] from Kombu message.'.format(
                 str(evidence_)))
@@ -436,6 +438,7 @@ class PSQTaskManager(BaseTaskManager):
         if not evidence_.request_id:
           evidence_.request_id = request.request_id
         evidence_.config = request.recipe
+        evidence_.config['requester'] = request.requester
         log.info(
             'Received evidence [{0:s}] from PubSub message.'.format(
                 str(evidence_)))
diff --git a/turbinia/turbiniactl.py b/turbinia/turbiniactl.py
index 28279c5..0d22e3a 100644
--- a/turbinia/turbiniactl.py
+++ b/turbinia/turbiniactl.py
@@ -35,6 +35,7 @@ from turbinia.config import logger
 from turbinia import evidence
 from turbinia import __version__
 from turbinia.message import TurbiniaRequest
+from turbinia.workers import Priority
 
 log = logging.getLogger('turbinia')
 logger.setup()
@@ -85,10 +86,6 @@ def main():
   parser.add_argument(
       '-S', '--server', action='store_true',
       help='Run Turbinia Server indefinitely')
-  parser.add_argument(
-      '-C', '--use_celery', action='store_true',
-      help='Pass this flag when using Celery/Kombu for task queuing and '
-      'messaging (instead of Google PSQ/pubsub)')
   parser.add_argument(
       '-V', '--version', action='version', version=__version__,
       help='Show the version')
@@ -145,6 +142,25 @@ def main():
   parser_rawdisk.add_argument(
       '-n', '--name', help='Descriptive name of the evidence', required=False)
 
+  # Parser options for APFS Disk Evidence type
+  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',
+      required=True)
+  parser_apfs.add_argument(
+      '-r', '--recovery_key', help='Recovery key for the APFS evidence.  '
+      'Either recovery key or password must be specified.', required=False)
+  parser_apfs.add_argument(
+      '-p', '--password', help='Password for the APFS evidence.  '
+      'If a recovery key is specified concurrently, password will be ignored.',
+      required=False)
+  parser_apfs.add_argument(
+      '-s', '--source', help='Description of the source of the evidence',
+      required=False)
+  parser_apfs.add_argument(
+      '-n', '--name', help='Descriptive name of the evidence', required=False)
+
   # Parser options for Bitlocker Disk Evidence type
   parser_bitlocker = subparsers.add_parser(
       'bitlocker', help='Process Bitlocker Disk as Evidence')
@@ -213,6 +229,19 @@ def main():
   parser_googleclouddiskembedded.add_argument(
       '-n', '--name', help='Descriptive name of the evidence', required=False)
 
+  # RawMemory
+  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)
+  parser_rawmemory.add_argument(
+      '-P', '--profile', help='Profile to use with Volatility', required=True)
+  parser_rawmemory.add_argument(
+      '-n', '--name', help='Descriptive name of the evidence', required=False)
+  parser_rawmemory.add_argument(
+      '-m', '--module_list', type=csv_list,
+      help='Volatility module(s) to execute', required=True)
+
   # Parser options for Directory evidence type
   parser_directory = subparsers.add_parser(
       'directory', help='Process a directory as Evidence')
@@ -255,7 +284,8 @@ def main():
       '-r', '--request_id', help='Show tasks with this Request ID',
       required=False)
   parser_status.add_argument(
-      '-p', '--priority_filter', default=20, type=int, required=False,
+      '-p', '--priority_filter', default=Priority.HIGH, type=int,
+      required=False,
       help='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 '
@@ -276,12 +306,10 @@ def main():
   args = parser.parse_args()
   if args.quiet:
     log.setLevel(logging.ERROR)
-  elif args.verbose:
-    log.setLevel(logging.INFO)
   elif args.debug:
     log.setLevel(logging.DEBUG)
   else:
-    log.setLevel(logging.WARNING)
+    log.setLevel(logging.INFO)
 
   log.info('Turbinia version: {0:s}'.format(__version__))
 
@@ -306,7 +334,7 @@ def main():
   # Client
   config.LoadConfig()
   if args.command not in ('psqworker', 'server'):
-    if args.use_celery:
+    if config.TASK_MANAGER.lower() == 'celery':
       client = TurbiniaCeleryClient()
     elif args.run_local:
       client = TurbiniaClient(run_local=True)
@@ -315,9 +343,8 @@ def main():
   else:
     client = None
 
-  server_flags_set = args.server or args.use_celery or args.command == 'server'
-  worker_flags_set = (
-      args.use_celery or args.command in ('psqworker', 'celeryworker'))
+  server_flags_set = args.server or args.command == 'server'
+  worker_flags_set = args.command in ('psqworker', 'celeryworker')
   if args.run_local and (server_flags_set or worker_flags_set):
     log.error('--run_local flag is not compatible with server/worker flags')
     sys.exit(1)
@@ -339,6 +366,15 @@ def main():
     evidence_ = evidence.RawDisk(
         name=args.name, local_path=local_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)
+    evidence_ = evidence.APFSEncryptedDisk(
+        name=args.name, local_path=local_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.')
@@ -367,6 +403,12 @@ def main():
         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)
+  elif args.command == 'rawmemory':
+    args.name = args.name if args.name else args.local_path
+    local_path = os.path.abspath(args.local_path)
+    evidence_ = evidence.RawMemory(
+        name=args.name, local_path=local_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
     # which we are bypassing.
@@ -457,7 +499,8 @@ def main():
     server.add_evidence(evidence_)
     server.start()
   elif evidence_:
-    request = TurbiniaRequest(request_id=args.request_id)
+    request = TurbiniaRequest(
+        request_id=args.request_id, requester=getpass.getuser())
     request.evidence.append(evidence_)
     if filter_patterns:
       request.recipe['filter_patterns'] = filter_patterns
@@ -467,14 +510,14 @@ def main():
       request.recipe['jobs_whitelist'] = args.jobs_whitelist
     if args.dump_json:
       print(request.to_json().encode('utf-8'))
+      sys.exit(0)
     else:
       log.info(
           'Creating request {0:s} with evidence {1:s}'.format(
               request.request_id, evidence_.name))
       log.info(
-          'Run command "turbiniactl {0:s} status -r {1:s}" to see the status of'
-          ' this request and associated tasks'.format(
-              '-C' if args.use_celery else '', request.request_id))
+          'Run command "turbiniactl status -r {0:s}" to see the status of'
+          ' this request and associated tasks'.format(request.request_id))
       if not args.run_local:
         client.send_request(request)
       else:
diff --git a/turbinia/workers/__init__.py b/turbinia/workers/__init__.py
index fbfe6ff..ce65023 100644
--- a/turbinia/workers/__init__.py
+++ b/turbinia/workers/__init__.py
@@ -18,6 +18,7 @@ from __future__ import unicode_literals
 
 from copy import deepcopy
 from datetime import datetime, timedelta
+from enum import IntEnum
 import getpass
 import logging
 import os
@@ -40,6 +41,18 @@ from turbinia import TurbiniaException
 log = logging.getLogger('turbinia')
 
 
+class Priority(IntEnum):
+  """Reporting priority enum to store common values.
+
+  Priorities can be anything in the range of 0-100, where 0 is the highest
+  priority.
+  """
+  LOW = 80
+  MEDIUM = 50
+  HIGH = 20
+  CRITICAL = 10
+
+
 class TurbiniaTaskResult(object):
   """Object to store task results to be returned by a TurbiniaTask.
 
@@ -62,7 +75,7 @@ class TurbiniaTaskResult(object):
       successful: Bool indicating success status.
       task_id: Task ID of the parent task.
       task_name: Name of parent task.
-      user: The user who requested the task.
+      requester: The user who requested the task.
       worker_name: Name of worker task executed on.
       _log: A list of log messages
   """
@@ -87,11 +100,11 @@ class TurbiniaTaskResult(object):
 
     self.task_id = None
     self.task_name = None
-    self.user = None
+    self.requester = None
     self.output_dir = None
 
     self.report_data = None
-    self.report_priority = 50
+    self.report_priority = Priority.MEDIUM
     self.start_time = datetime.now()
     self.run_time = None
     self.saved_paths = []
@@ -117,7 +130,7 @@ class TurbiniaTaskResult(object):
 
     self.task_id = task.id
     self.task_name = task.name
-    self.user = task.user
+    self.requester = task.requester
     if task.output_manager.is_setup:
       _, self.output_dir = task.output_manager.get_local_output_dirs()
     else:
@@ -151,11 +164,16 @@ class TurbiniaTaskResult(object):
     self.status = status
 
     for evidence in self.evidence:
-      if evidence.local_path:
+      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)
+      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))
+
       if not evidence.request_id:
         evidence.request_id = self.request_id
 
@@ -165,13 +183,17 @@ class TurbiniaTaskResult(object):
     # to clean things up even after other failures in the task, so this could
     # also fail.
     # pylint: disable=broad-except
-    except Exception as e:
-      msg = 'Evidence post-processing for {0:s} failed: {1!s}'.format(
-          self.input_evidence.name, e)
-      self.log(msg, level=logging.ERROR)
+    except Exception as exception:
+      message = 'Evidence post-processing for {0:s} failed: {1!s}'.format(
+          self.input_evidence.name, exception)
+      self.log(message, level=logging.ERROR)
 
     # Write result log info to file
     logfile = os.path.join(self.output_dir, 'worker-log.txt')
+    # Create default log text just so that the worker log is created to
+    # avoid confusion if it doesn't exist.
+    if not self._log:
+      self._log.append('No worker messages were logged.')
     if self.output_dir and os.path.exists(self.output_dir):
       with open(logfile, 'w') as f:
         f.write('\n'.join(self._log))
@@ -293,16 +315,16 @@ class TurbiniaTask(object):
           this is a task result object, but other implementations have their
           own stub objects.
       tmp_dir: Temporary directory for Task to write to.
-      user: The user who requested the task.
+      requester: The user who requested the task.
       _evidence_config (dict): The config that we want to pass to all new
             evidence created from this task.
   """
 
   # The list of attributes that we will persist into storage
-  STORED_ATTRIBUTES = ['id', 'last_update', 'name', 'request_id', 'user']
+  STORED_ATTRIBUTES = ['id', 'last_update', 'name', 'request_id', 'requester']
 
   def __init__(
-      self, name=None, base_output_dir=None, request_id=None, user=None):
+      self, name=None, base_output_dir=None, request_id=None, requester=None):
     """Initialization for TurbiniaTask."""
     if base_output_dir:
       self.base_output_dir = base_output_dir
@@ -320,7 +342,7 @@ class TurbiniaTask(object):
     self.stub = None
     self.tmp_dir = None
     self.turbinia_version = turbinia.__version__
-    self.user = user if user else getpass.getuser()
+    self.requester = requester if requester else 'user_unspecified'
     self._evidence_config = {}
 
   def serialize(self):
@@ -350,11 +372,11 @@ class TurbiniaTask(object):
     try:
       task = getattr(sys.modules['turbinia.client'], type_)()
     except AttributeError:
-      msg = (
+      message = (
           "Could not import {0:s} object! Make sure it is imported where "
           "this method is defined.".format(type_))
-      log.error(msg)
-      raise TurbiniaException(msg)
+      log.error(message)
+      raise TurbiniaException(message)
     task.__dict__.update(input_dict)
     task.output_manager = output_manager.OutputManager()
     task.output_manager.__dict__.update(input_dict['output_manager'])
@@ -401,35 +423,46 @@ class TurbiniaTask(object):
             'Log file {0:s} does not exist to save'.format(file_),
             level=logging.DEBUG)
         continue
+      if os.path.getsize(file_) == 0:
+        result.log(
+            'Log file {0:s} is empty. Not saving'.format(file_),
+            level=logging.DEBUG)
+        continue
       result.log('Output file at {0:s}'.format(file_))
       if not self.run_local:
         self.output_manager.save_local_file(file_, result)
 
     if ret not in success_codes:
-      msg = 'Execution of [{0!s}] failed with status {1:d}'.format(cmd, ret)
-      result.log(msg)
+      message = 'Execution of [{0!s}] failed with status {1:d}'.format(cmd, ret)
+      result.log(message)
       if close:
-        result.close(self, success=False, status=msg)
+        result.close(self, success=False, status=message)
     else:
       result.log('Execution of [{0!s}] succeeded'.format(cmd))
       for file_ in save_files:
+        if os.path.getsize(file_) == 0:
+          result.log(
+              'Output file {0:s} is empty. Not saving'.format(file_),
+              level=logging.DEBUG)
+          continue
         result.log('Output 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):
-          msg = (
+          message = (
               'Evidence {0:s} local_path {1:s} does not exist. Not returning '
               'empty Evidence.'.format(evidence.name, evidence.local_path))
-          result.log(msg, level=logging.WARN)
+          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):
-          msg = (
+          message = (
               'Evidence {0:s} local_path {1:s} is empty. Not returning '
               'empty new Evidence.'.format(evidence.name, evidence.local_path))
-          result.log(msg, level=logging.WARN)
+          result.log(message, level=logging.WARN)
         else:
           result.add_evidence(evidence, self._evidence_config)
 
@@ -504,11 +537,11 @@ class TurbiniaTask(object):
       try:
         log.debug('Checking TurbiniaTaskResult for serializability')
         pickle.dumps(result)
-      except (TypeError, pickle.PicklingError) as e:
+      except (TypeError, pickle.PicklingError) as exception:
         bad_message = (
             'Error pickling TurbiniaTaskResult object. Returning a new result '
             'with the pickling error, and all previous result data will be '
-            'lost. Pickle Error: {0!s}'.format(e))
+            'lost. Pickle Error: {0!s}'.format(exception))
 
     if bad_message:
       log.error(bad_message)
@@ -561,30 +594,32 @@ class TurbiniaTask(object):
         original_result_id = self.result.id
 
         if self.turbinia_version != turbinia.__version__:
-          msg = (
+          message = (
               'Worker and Server versions do not match: {0:s} != {1:s}'.format(
                   self.turbinia_version, turbinia.__version__))
-          self.result.log(msg, level=logging.ERROR)
-          self.result.status = msg
+          self.result.log(message, level=logging.ERROR)
+          self.result.status = message
           return self.result
 
         self._evidence_config = evidence.config
         self.result = self.run(evidence, self.result)
       # pylint: disable=broad-except
-      except Exception as e:
-        msg = '{0:s} Task failed with exception: [{1!s}]'.format(self.name, e)
+      except Exception as exception:
+        message = (
+            '{0:s} Task failed with exception: [{1!s}]'.format(
+                self.name, exception))
         # Logging explicitly here because the result is in an unknown state
         trace = traceback.format_exc()
-        log.error(msg)
+        log.error(message)
         log.error(trace)
         if self.result:
-          self.result.log(msg, level=logging.ERROR)
+          self.result.log(message, level=logging.ERROR)
           self.result.log(trace)
-          if hasattr(e, 'message'):
-            self.result.set_error(e.message, traceback.format_exc())
+          if hasattr(exception, 'message'):
+            self.result.set_error(exception.message, traceback.format_exc())
           else:
-            self.result.set_error(e.__class__, traceback.format_exc())
-          self.result.status = msg
+            self.result.set_error(exception.__class__, traceback.format_exc())
+          self.result.status = message
         else:
           log.error('No TurbiniaTaskResult object found after task execution.')
 
@@ -594,28 +629,28 @@ class TurbiniaTask(object):
       # This has a higher likelihood of failing because something must have gone
       # wrong as the Task should have already closed this.
       if self.result and not self.result.closed:
-        msg = 'Trying last ditch attempt to close result'
-        log.warning(msg)
-        self.result.log(msg)
+        message = 'Trying last ditch attempt to close result'
+        log.warning(message)
+        self.result.log(message)
 
         if self.result.status:
           status = self.result.status
         else:
           status = 'No previous status'
-        msg = (
+        message = (
             'Task Result was auto-closed from task executor on {0:s} likely '
             'due to previous failures.  Previous status: [{1:s}]'.format(
                 self.result.worker_name, status))
-        self.result.log(msg)
+        self.result.log(message)
         try:
-          self.result.close(self, False, msg)
+          self.result.close(self, False, message)
         # Using broad except here because lots can go wrong due to the reasons
         # listed above.
         # pylint: disable=broad-except
-        except Exception as e:
-          log.error('TurbiniaTaskResult close failed: {0!s}'.format(e))
+        except Exception as exception:
+          log.error('TurbiniaTaskResult close failed: {0!s}'.format(exception))
           if not self.result.status:
-            self.result.status = msg
+            self.result.status = message
         # Check the result again after closing to make sure it's still good.
         self.result = self.validate_result(self.result)
 
diff --git a/turbinia/workers/analysis/jenkins.py b/turbinia/workers/analysis/jenkins.py
index a7bb3f2..2eaad94 100644
--- a/turbinia/workers/analysis/jenkins.py
+++ b/turbinia/workers/analysis/jenkins.py
@@ -23,7 +23,8 @@ from turbinia import TurbiniaException
 from turbinia.evidence import ReportText
 from turbinia.lib import text_formatter as fmt
 from turbinia.workers import TurbiniaTask
-from turbinia.lib.utils import extract_artifacts
+from turbinia.workers import Priority
+from turbinia.lib.utils import extract_files
 from turbinia.lib.utils import bruteforce_password_hashes
 
 
@@ -50,17 +51,26 @@ class JenkinsAnalysisTask(TurbiniaTask):
     # Set the output file as the data source for the output evidence.
     output_evidence.local_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
+    # than we need.  Tracked in https://github.com/google/turbinia/issues/402
     try:
-      collected_artifacts = extract_artifacts(
-          artifact_names=['JenkinsConfigFile'], disk_path=evidence.local_path,
+      collected_artifacts = extract_files(
+          file_name='config.xml', disk_path=evidence.local_path,
           output_dir=os.path.join(self.output_dir, 'artifacts'))
     except TurbiniaException as e:
       result.close(self, success=False, status=str(e))
       return result
 
+    jenkins_artifacts = []
+    jenkins_re = re.compile(r'^.*jenkins[^\/]*(\/users\/[^\/]+)*\/config\.xml$')
+    for collected_artifact in collected_artifacts:
+      if re.match(jenkins_re, collected_artifact):
+        jenkins_artifacts.append(collected_artifact)
+
     version = None
     credentials = []
-    for filepath in collected_artifacts:
+    for filepath in jenkins_artifacts:
       with open(filepath, 'r') as input_file:
         config = input_file.read()
 
@@ -148,7 +158,7 @@ class JenkinsAnalysisTask(TurbiniaTask):
     """
     report = []
     summary = ''
-    priority = 50
+    priority = Priority.LOW
     credentials_registry = {hash: username for username, hash in credentials}
     # TODO: Add timeout parameter when dynamic configuration is ready.
     # Ref: https://github.com/google/turbinia/issues/244
@@ -159,7 +169,7 @@ class JenkinsAnalysisTask(TurbiniaTask):
     report.append(fmt.bullet('Jenkins version: {0:s}'.format(version)))
 
     if weak_passwords:
-      priority = 10
+      priority = Priority.CRITICAL
       summary = 'Jenkins analysis found potential issues'
       report.insert(0, fmt.heading4(fmt.bold(summary)))
       line = '{0:n} weak password(s) found:'.format(len(weak_passwords))
@@ -168,9 +178,14 @@ class JenkinsAnalysisTask(TurbiniaTask):
         line = 'User "{0:s}" with password "{1:s}"'.format(
             credentials_registry.get(password_hash), plaintext)
         report.append(fmt.bullet(line, level=2))
+    elif credentials_registry or version != 'Unknown':
+      summary = (
+          'Jenkins version {0:s} found with {1:d} credentials, but no issues '
+          'detected'.format(version, len(credentials_registry)))
+      report.insert(0, fmt.heading4(summary))
+      priority = Priority.MEDIUM
     else:
-      summary = 'Jenkins analysis found no issues'
-      priority = 80
+      summary = 'No Jenkins instance found'
       report.insert(0, fmt.heading4(summary))
 
     report = '\n'.join(report)
diff --git a/turbinia/workers/analysis/wordpress.py b/turbinia/workers/analysis/wordpress.py
index 74c723e..b8395b1 100644
--- a/turbinia/workers/analysis/wordpress.py
+++ b/turbinia/workers/analysis/wordpress.py
@@ -23,6 +23,7 @@ import re
 from turbinia.evidence import ReportText
 from turbinia.lib import text_formatter as fmt
 from turbinia.workers import TurbiniaTask
+from turbinia.workers import Priority
 
 
 class WordpressAccessLogAnalysisTask(TurbiniaTask):
@@ -123,7 +124,7 @@ class WordpressAccessLogAnalysisTask(TurbiniaTask):
 
       report.insert(0, fmt.heading4(fmt.bold(summary)))
       report_text = '\n'.join(report)
-      return (report_text, 20, summary)
+      return (report_text, Priority.HIGH, summary)
 
     report_text = 'No Wordpress install or theme editing found in access logs'
-    return (fmt.heading4(report_text), 80, report_text)
+    return (fmt.heading4(report_text), Priority.LOW, report_text)
diff --git a/turbinia/workers/hadoop.py b/turbinia/workers/hadoop.py
index 95cecf8..0324d8f 100644
--- a/turbinia/workers/hadoop.py
+++ b/turbinia/workers/hadoop.py
@@ -27,6 +27,7 @@ from turbinia.lib import text_formatter as fmt
 from turbinia.evidence import ReportText
 from turbinia.lib.utils import extract_artifacts
 from turbinia.workers import TurbiniaTask
+from turbinia.workers import Priority
 
 log = logging.getLogger('turbinia')
 
@@ -57,7 +58,7 @@ class HadoopAnalysisTask(TurbiniaTask):
     report = []
     evil_commands = []
     strings_count = 0
-    priority = 50
+    priority = Priority.MEDIUM
     summary = ''
     for filepath in collected_artifacts:
       relpath = os.path.relpath(filepath, output_dir)
@@ -76,7 +77,7 @@ class HadoopAnalysisTask(TurbiniaTask):
       msg = 'Found suspicious commands!'
       report.append(fmt.heading4(fmt.bold(msg)))
       summary = msg
-      priority = 10
+      priority = Priority.CRITICAL
     else:
       msg = 'Did not find any suspicious commands.'
       report.append(fmt.heading4(msg))
diff --git a/turbinia/workers/plaso.py b/turbinia/workers/plaso.py
index 5e42f05..0215f09 100644
--- a/turbinia/workers/plaso.py
+++ b/turbinia/workers/plaso.py
@@ -19,6 +19,7 @@ from __future__ import unicode_literals
 import os
 
 from turbinia import config
+from turbinia.evidence import APFSEncryptedDisk
 from turbinia.evidence import BitlockerDisk
 from turbinia.evidence import PlasoFile
 from turbinia.workers import TurbiniaTask
@@ -53,7 +54,7 @@ class PlasoTask(TurbiniaTask):
     if config.DEBUG_TASKS:
       cmd.append('-d')
 
-    if isinstance(evidence, BitlockerDisk):
+    if isinstance(evidence, (APFSEncryptedDisk, BitlockerDisk)):
       if evidence.recovery_key:
         cmd.extend([
             '--credential', 'recovery_password:{0:s}'.format(
diff --git a/turbinia/workers/sshd.py b/turbinia/workers/sshd.py
index 3317adc..c2822e8 100644
--- a/turbinia/workers/sshd.py
+++ b/turbinia/workers/sshd.py
@@ -22,6 +22,7 @@ import re
 from turbinia.evidence import ReportText
 from turbinia.lib import text_formatter as fmt
 from turbinia.workers import TurbiniaTask
+from turbinia.workers import Priority
 
 
 class SSHDAnalysisTask(TurbiniaTask):
@@ -99,7 +100,7 @@ class SSHDAnalysisTask(TurbiniaTask):
       summary = 'Insecure SSH configuration found.'
       findings.insert(0, fmt.heading4(fmt.bold(summary)))
       report = '\n'.join(findings)
-      return (report, 20, summary)
+      return (report, Priority.HIGH, summary)
 
     report = 'No issues found in SSH configuration'
-    return (report, 60, report)
+    return (report, Priority.LOW, report)
diff --git a/turbinia/workers/tomcat.py b/turbinia/workers/tomcat.py
index 2daeb9e..e2e6281 100644
--- a/turbinia/workers/tomcat.py
+++ b/turbinia/workers/tomcat.py
@@ -22,6 +22,7 @@ import re
 from turbinia.evidence import ReportText
 from turbinia.lib import text_formatter as fmt
 from turbinia.workers import TurbiniaTask
+from turbinia.workers import Priority
 
 
 class TomcatAnalysisTask(TurbiniaTask):
@@ -104,7 +105,7 @@ class TomcatAnalysisTask(TurbiniaTask):
       msg = 'Tomcat analysis found {0:d} results'.format(count)
       findings.insert(0, fmt.heading4(fmt.bold(msg)))
       report = '\n'.join(findings)
-      return (report, 20, msg)
+      return (report, Priority.HIGH, msg)
 
     report = 'No Tomcat findings to report'
-    return (report, 80, report)
+    return (report, Priority.LOW, report)
diff --git a/turbinia/workers/volatility.py b/turbinia/workers/volatility.py
new file mode 100644
index 0000000..e54cc0c
--- /dev/null
+++ b/turbinia/workers/volatility.py
@@ -0,0 +1,81 @@
+# -*- 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 for executing volatility."""
+
+from __future__ import unicode_literals
+
+import os
+
+from turbinia import config
+from turbinia.evidence import VolatilityReport
+from turbinia.workers import TurbiniaTask
+
+
+class VolatilityTask(TurbiniaTask):
+  """Task to execute volatility."""
+
+  def __init__(self, module='test'):
+    super(VolatilityTask, self).__init__()
+    self.module = module
+
+  def run(self, evidence, result):
+    """Run volatility against evidence.
+
+    Args:
+        evidence (Evidence object): The evidence we will process
+        result (TurbiniaTaskResult): The object to place task results into.
+
+    Returns:
+        TurbiniaTaskResult object.
+    """
+
+    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
+    debug_log = os.path.join(self.output_dir, '{0:s}.log'.format(self.id))
+
+    # TODO: Add in config options for Turbinia
+    cmd = (
+        'vol2 -f {0:s} --profile={1:s} {2:s} --output=text '
+        '--output-file={3:s}').format(
+            evidence.local_path, evidence.profile, self.module,
+            output_file_path).split()
+
+    result.log('Running vol as [{0:s}]'.format(' '.join(cmd)))
+    res = self.execute(
+        cmd, result, log_files=[debug_log], new_evidence=[output_evidence],
+        close=True)
+
+    if res == 0:
+      # Write the report to the output file.
+      with open(output_file_path, 'w') as fh:
+        fh.write(output_evidence.text_data.encode('utf-8'))
+
+      # Add the resulting evidence to the result object.
+      result.add_evidence(output_evidence, evidence.config)
+      result.report_data = output_evidence
+      summary = 'Volatility module {0} successfully ran.'.format(self.module)
+      result.close(self, success=True, status=summary)
+    else:
+      summary = 'Volatility module {0} failed to run.'.format(self.module)
+      result.close(self, success=False, status=summary)
+
+    return result


Test patch:
diff --git a/turbinia/client_test.py b/turbinia/client_test.py
index d57cb34..e2bd8d4 100644
--- a/turbinia/client_test.py
+++ b/turbinia/client_test.py
@@ -127,7 +127,7 @@ class TestTurbiniaClient(unittest.TestCase):
             'saved_paths': ['/no/path/', '/fake/path'],
             'status': 'This fake task executed',
             'successful': True,
-            'user': 'myuser',
+            'requester': 'myuser',
             'worker_name': 'fake_worker'
         }, {
             'id': '0xfakeTaskId2',
@@ -140,7 +140,7 @@ class TestTurbiniaClient(unittest.TestCase):
             'saved_paths': ['/no/path/2', '/fake/path/2'],
             'status': 'This second fake task executed',
             'successful': True,
-            'user': 'myuser',
+            'requester': 'myuser',
             'worker_name': 'fake_worker'
         }, {
             'id': '0xfakeTaskId3',
@@ -153,7 +153,7 @@ class TestTurbiniaClient(unittest.TestCase):
             'saved_paths': ['/no/path/3', '/fake/path/3'],
             'status': 'Third Task Failed...',
             'successful': False,
-            'user': 'myuser',
+            'requester': 'myuser',
             'worker_name': 'fake_worker'
         }
     ] # yapf: disable
@@ -200,6 +200,20 @@ class TestTurbiniaClient(unittest.TestCase):
     self.assertRaises(
         TurbiniaException, client.get_task_data, "inst", "proj", "reg")
 
+  @mock.patch('turbinia.client.GoogleCloudFunction.ExecuteFunction')
+  @mock.patch('turbinia.client.task_manager.PSQTaskManager._backend_setup')
+  @mock.patch('turbinia.state_manager.get_state_manager')
+  def testClientFormatTaskStatus(self, _, __, ___):
+    """Tests format_task_status() with empty report_priority."""
+    client = TurbiniaClient()
+    client.get_task_data = mock.MagicMock()
+    self.task_data[0]['report_priority'] = None
+    self.task_data[1]['report_priority'] = ''
+    self.task_data[2].pop('report_priority')
+    client.get_task_data.return_value = self.task_data
+    result = client.format_task_status('inst', 'proj', 'reg')
+    self.assertIn('Processed 3 Tasks', result.strip())
+
   @mock.patch('turbinia.client.GoogleCloudFunction.ExecuteFunction')
   @mock.patch('turbinia.client.task_manager.PSQTaskManager._backend_setup')
   @mock.patch('turbinia.state_manager.get_state_manager')
diff --git a/turbinia/output_manager_test.py b/turbinia/output_manager_test.py
index 65dfaac..caec1e8 100644
--- a/turbinia/output_manager_test.py
+++ b/turbinia/output_manager_test.py
@@ -59,6 +59,7 @@ class TestLocalOutputManager(unittest.TestCase):
 
   def testGetOutputWriters(self):
     """Tests get_output_writers function for valid response."""
+    output_manager.storage = mock.MagicMock()
     writers = output_manager.OutputManager.get_output_writers(self.task)
     self.assertEquals(len(writers), 2)
     for writer in writers:
@@ -66,6 +67,7 @@ class TestLocalOutputManager(unittest.TestCase):
 
   def testGetLocalOutputDirs(self):
     """Tests get_local_output_dirs function for valid response."""
+    output_manager.storage = mock.MagicMock()
     self.task.output_manager.setup(self.task)
     tmp_dir, local_dir = self.task.output_manager.get_local_output_dirs()
 
diff --git a/turbinia/workers/analysis/jenkins_test.py b/turbinia/workers/analysis/jenkins_test.py
index 53850d9..cba7245 100644
--- a/turbinia/workers/analysis/jenkins_test.py
+++ b/turbinia/workers/analysis/jenkins_test.py
@@ -111,7 +111,7 @@ class JenkinsAnalysisTaskTest(unittest.TestCase):
     * User \"admin\" with password \"weakpassword\"
 """
 
-  JENKINS_ANALYSIS_EMPTY_REPORT = """#### Jenkins analysis found no issues
+  JENKINS_ANALYSIS_EMPTY_REPORT = """#### Jenkins version 2.121.2 found with 1 credentials, but no issues detected
 * Jenkins version: 2.121.2"""
 
   EXPECTED_VERSION = '2.121.2'
@@ -166,8 +166,11 @@ class JenkinsAnalysisTaskTest(unittest.TestCase):
         self.EXPECTED_VERSION, self.EXPECTED_CREDENTIALS)
 
     self.assertEqual(report, self.JENKINS_ANALYSIS_EMPTY_REPORT)
-    self.assertEqual(priority, 80)
-    self.assertEqual(summary, 'Jenkins analysis found no issues')
+    self.assertEqual(priority, 50)
+    self.assertEqual(
+        summary,
+        'Jenkins version 2.121.2 found with 1 credentials, but no issues '
+        'detected')
 
 
 if __name__ == '__main__':