Repository: google/turbinia
Pull request #448 (2019-10-02 14:03:13)

Binary extractor job
Create job to extract binary files from evidence using image_export.py.

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 with `@googlebot 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%2F448) for more info**.

<!-- need_sender_cla -->
meeehow: @googlebot  I signed it!
googlebot: CLAs look good, thanks!

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

<!-- ok -->

Patch:
diff --git a/setup.py b/setup.py
index eb0181e..c13a91e 100644
--- a/setup.py
+++ b/setup.py
@@ -16,6 +16,8 @@
 # limitations under the License.
 """This is the setup file for the project."""
 
+# yapf: disable
+
 from __future__ import unicode_literals
 
 import sys
diff --git a/turbinia/client.py b/turbinia/client.py
index 024d25c..8c58e15 100644
--- a/turbinia/client.py
+++ b/turbinia/client.py
@@ -49,6 +49,7 @@ from turbinia.workers.strings import StringsUnicodeTask
 from turbinia.workers.tomcat import TomcatAnalysisTask
 from turbinia.workers.volatility import VolatilityTask
 from turbinia.workers.worker_stat import StatTask
+from turbinia.workers.binary_extractor import BinaryExtractorTask
 from turbinia.workers.bulk_extractor import BulkExtractorTask
 
 # TODO(aarontp): Remove this map after
@@ -69,6 +70,7 @@ TASK_MAP = {
     'tomcatanalysistask': TomcatAnalysisTask,
     'volatilitytask': VolatilityTask,
     'stattask': StatTask,
+    'binaryextractor': BinaryExtractorTask,
     'bulkextractortask': BulkExtractorTask
 }
 
diff --git a/turbinia/evidence.py b/turbinia/evidence.py
index e814ae2..c9bb84f 100644
--- a/turbinia/evidence.py
+++ b/turbinia/evidence.py
@@ -554,3 +554,8 @@ class RawMemory(Evidence):
     super(RawMemory, self).__init__(*args, **kwargs)
     self.profile = profile
     self.module_list = module_list
+
+
+class BinaryExtraction(CompressedDirectory):
+  """Binaries extracted from evidence."""
+  pass
diff --git a/turbinia/jobs/__init__.py b/turbinia/jobs/__init__.py
index 8e51312..b60b1f2 100644
--- a/turbinia/jobs/__init__.py
+++ b/turbinia/jobs/__init__.py
@@ -26,4 +26,5 @@ from turbinia.jobs import strings
 from turbinia.jobs import tomcat
 from turbinia.jobs import volatility
 from turbinia.jobs import worker_stat
+from turbinia.jobs import binary_extractor
 from turbinia.jobs import bulk_extractor
diff --git a/turbinia/jobs/binary_extractor.py b/turbinia/jobs/binary_extractor.py
new file mode 100644
index 0000000..3fb4346
--- /dev/null
+++ b/turbinia/jobs/binary_extractor.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Job to execute binary_extractor task."""
+
+from __future__ import unicode_literals
+
+from turbinia.evidence import Directory
+from turbinia.evidence import GoogleCloudDisk
+from turbinia.evidence import GoogleCloudDiskRawEmbedded
+from turbinia.evidence import RawDisk
+from turbinia.evidence import BinaryExtraction
+from turbinia.jobs import interface
+from turbinia.jobs import manager
+from turbinia.workers.binary_extractor import BinaryExtractorTask
+
+
+class BinaryExtractorJob(interface.TurbiniaJob):
+  """Run image_export.py on evidence to extract binaries."""
+
+  # The types of evidence that this Job will process.
+  evidence_input = [
+      Directory, RawDisk, GoogleCloudDisk, GoogleCloudDiskRawEmbedded
+  ]
+  evidence_output = [BinaryExtraction]
+
+  NAME = 'BinaryExtractorJob'
+
+  def create_tasks(self, evidence):
+    """Create task.
+
+    Args:
+      evidence: List of evidence objects to process
+
+    Returns:
+        A list of tasks to schedule.
+    """
+    tasks = [BinaryExtractorTask() for _ in evidence]
+    return tasks
+
+
+manager.JobsManager.RegisterJob(BinaryExtractorJob)
diff --git a/turbinia/output_manager.py b/turbinia/output_manager.py
index 768b258..d6b8d17 100644
--- a/turbinia/output_manager.py
+++ b/turbinia/output_manager.py
@@ -194,10 +194,15 @@ class OutputManager(object):
     local_path = None
     for writer in self._output_writers:
       new_path = writer.copy_to(file_)
-      if new_path and result:
-        result.saved_paths.append(new_path)
-        saved_path = new_path
-        saved_path_type = writer.name
+      if result:
+        if new_path:
+          result.saved_paths.append(new_path)
+          saved_path = new_path
+          saved_path_type = writer.name
+        elif os.path.exists(file_) and os.path.getsize(file_) > 0:
+          # We want to save the old path if the path is still valid.
+          result.saved_paths.append(file_)
+
       if writer.name == LocalOutputWriter.NAME:
         local_path = new_path
 
@@ -350,18 +355,20 @@ class LocalOutputWriter(OutputWriter):
     if self.local_output_dir in os.path.commonprefix([file_path,
                                                       destination_file]):
       log.debug(
-          'Not copying file {0:s} in output dir {1:s}'.format(
+          'Not copying source file {0:s} already in output dir {1:s}'.format(
               file_path, self.local_output_dir))
       return None
     if not os.path.exists(file_path):
-      log.warning('File [{0:s}] does not exist.'.format(file_path))
+      log.warning('Source file [{0:s}] does not exist.'.format(file_path))
       return None
     if os.path.exists(destination_file):
       log.warning(
-          'New file path [{0:s}] already exists.'.format(destination_file))
+          'Target output file path [{0:s}] already exists.'.format(
+              destination_file))
       return None
 
     shutil.copy(file_path, destination_file)
+    log.debug('Copied file {0:s} to {1:s}'.format(file_path, destination_file))
     return destination_file
 
   def copy_to(self, source_file):
diff --git a/turbinia/workers/__init__.py b/turbinia/workers/__init__.py
index 138e1fd..b0381b4 100644
--- a/turbinia/workers/__init__.py
+++ b/turbinia/workers/__init__.py
@@ -170,9 +170,8 @@ class TurbiniaTaskResult(object):
     for evidence in self.evidence:
       if evidence.local_path and os.path.exists(evidence.local_path):
         self.saved_paths.append(evidence.local_path)
-        if not task.run_local:
-          if evidence.copyable and not config.SHARED_FILESYSTEM:
-            task.output_manager.save_evidence(evidence, self)
+        if not task.run_local and evidence.copyable:
+          task.output_manager.save_evidence(evidence, self)
       else:
         self.log(
             'Evidence {0:s} has empty or missing file at local_path {1:s} so '
@@ -420,6 +419,7 @@ class TurbiniaTask(object):
     log_files = log_files if log_files else []
     new_evidence = new_evidence if new_evidence else []
     success_codes = success_codes if success_codes else [0]
+
     if shell:
       proc = subprocess.Popen(cmd, shell=True)
     else:
@@ -440,7 +440,7 @@ class TurbiniaTask(object):
             'Log file {0:s} is empty. Not saving'.format(file_),
             level=logging.DEBUG)
         continue
-      result.log('Output file at {0:s}'.format(file_))
+      result.log('Output log file found at {0:s}'.format(file_))
       if not self.run_local:
         self.output_manager.save_local_file(file_, result)
 
@@ -457,7 +457,7 @@ class TurbiniaTask(object):
               'Output file {0:s} is empty. Not saving'.format(file_),
               level=logging.DEBUG)
           continue
-        result.log('Output file at {0:s}'.format(file_))
+        result.log('Output save file at {0:s}'.format(file_))
         if not self.run_local:
           self.output_manager.save_local_file(file_, result)
 
diff --git a/turbinia/workers/artifact.py b/turbinia/workers/artifact.py
index 5a30c83..2e43acb 100644
--- a/turbinia/workers/artifact.py
+++ b/turbinia/workers/artifact.py
@@ -34,8 +34,8 @@ class FileArtifactExtractionTask(TurbiniaTask):
     """Extracts artifacts using Plaso image_export.py.
 
     Args:
-        evidence: evidence to be processed.
-        result: A TurbiniaTaskResult object to place task results into.
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
 
     Returns:
         TurbiniaTaskResult object.
diff --git a/turbinia/workers/binary_extractor.py b/turbinia/workers/binary_extractor.py
new file mode 100644
index 0000000..6b89655
--- /dev/null
+++ b/turbinia/workers/binary_extractor.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+# Copyright 2015 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Task to extract binary files from an evidence object provided."""
+
+from __future__ import unicode_literals
+
+import logging
+import json
+import os
+
+from turbinia import config
+from turbinia.workers import TurbiniaTask
+from turbinia.evidence import BinaryExtraction
+
+
+class BinaryExtractorTask(TurbiniaTask):
+  """Extract binaries out of evidence and provide JSON file with hashes.
+  
+  Attributes:
+    json_path(str): path to output JSON file.
+    binary_extraction_dir(str): path to extraction directory. 
+  """
+
+  def __init__(self, *args, **kwargs):
+    """Initializes BinaryExtractorTask."""
+    super(BinaryExtractorTask, self).__init__(*args, **kwargs)
+    self.json_path = None
+    self.binary_extraction_dir = None
+
+  def check_extraction(self):
+    """Checks counts for extracted binaries and hashes.
+
+    Returns:
+      Tuple(
+        binary_cnt(int): Number of extracted binaries.
+        hash_cnt(int): Number of extracted hashes.
+      )
+    """
+
+    with open(self.json_path) as json_file:
+      hashes = json.load(json_file)
+
+    binary_cnt = sum(
+        len(files) for _, _, files in os.walk(self.binary_extraction_dir)) - 1
+    hash_cnt = len(hashes)
+
+    return (binary_cnt, hash_cnt)
+
+  def run(self, evidence, result):
+    """Task that extracts binaries with image_export.py.
+
+    Args:
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
+
+    Returns:
+        TurbiniaTaskResult object.
+    """
+
+    config.LoadConfig()
+    binary_extraction_evidence = BinaryExtraction()
+
+    binary_extraction_evidence.local_path = self.output_dir
+    image_export_log = os.path.join(self.output_dir, 'binary_extraction.log')
+    self.binary_extraction_dir = os.path.join(
+        self.output_dir, 'extracted_binaries')
+    self.json_path = os.path.join(self.binary_extraction_dir, 'hashes.json')
+
+    cmd = [
+        'image_export.py', '--partitions', 'all', '--no_vss', '--signatures',
+        'elf,exe_mz', '--logfile', image_export_log
+    ]
+    if config.DEBUG_TASKS:
+      cmd.append('-d')
+    cmd.extend(['-w', self.binary_extraction_dir, evidence.local_path])
+
+    result.log('Running image_export as [{0:s}]'.format(' '.join(cmd)))
+    self.execute(
+        cmd, result, log_files=[image_export_log, self.json_path],
+        new_evidence=[binary_extraction_evidence], close=True)
+
+    binary_cnt, hash_cnt = self.check_extraction()
+
+    result.status = (
+        'Extracted {0:d} hashes and {1:d} binaries from the '
+        'evidence.'.format(hash_cnt, binary_cnt))
+
+    if hash_cnt != binary_cnt:
+      result.log(
+          'Number of extracted binaries is not equal to the number '
+          'of extracted hashes. This might indicate issues with '
+          'image_export.py. Check binary_extraction.log for more '
+          'details.', logging.WARNING)
+
+    binary_extraction_evidence.compress()
+
+    return result
diff --git a/turbinia/workers/bulk_extractor.py b/turbinia/workers/bulk_extractor.py
index 4817e46..6704559 100644
--- a/turbinia/workers/bulk_extractor.py
+++ b/turbinia/workers/bulk_extractor.py
@@ -72,6 +72,23 @@ class BulkExtractorTask(TurbiniaTask):
       return result
     return result
 
+  def check_xml_attrib(self, xml_key):
+    """Checks if a key exists within the xml report.
+
+    Args:
+      xml_key(str): the xml key to check for.
+
+    Returns:
+      xml_hit(str): the xml value else return N/A.
+    """
+    xml_hit = 'N/A'
+    xml_search = self.xml.find(xml_key)
+
+    # If exists, return the text value.
+    if xml_search is not None:
+      xml_hit = xml_search.text
+    return xml_hit
+
   def generate_summary_report(self, output_file_path):
     """Generate a summary report from the resulting bulk extractor run.
 
@@ -93,7 +110,7 @@ class BulkExtractorTask(TurbiniaTask):
       return (report, report)
 
     # Parse existing XML file.
-    xml = xml_tree.parse(report_path)
+    self.xml = xml_tree.parse(report_path)
 
     # Place in try/except statement to continue execution when
     # an attribute is not found and NoneType is returned.
@@ -104,23 +121,25 @@ class BulkExtractorTask(TurbiniaTask):
       findings.append(
           fmt.bullet(
               'Program: {0} - {1}'.format(
-                  xml.find('creator/program').text,
-                  xml.find('creator/version').text)))
+                  self.check_xml_attrib('creator/program'),
+                  self.check_xml_attrib('creator/version'))))
       findings.append(
           fmt.bullet(
               'Command Line: {0}'.format(
-                  xml.find('creator/execution_environment/command_line').text)))
+                  self.check_xml_attrib(
+                      'creator/execution_environment/command_line'))))
       findings.append(
           fmt.bullet(
               'Start Time: {0}'.format(
-                  xml.find('creator/execution_environment/start_time').text)))
+                  self.check_xml_attrib(
+                      'creator/execution_environment/start_time'))))
       findings.append(
           fmt.bullet(
               'Elapsed Time: {0}'.format(
-                  xml.find('report/elapsed_seconds').text)))
+                  self.check_xml_attrib('report/elapsed_seconds'))))
 
       # Retrieve results from each of the scanner runs
-      feature_files = xml.find('feature_files')
+      feature_files = self.xml.find('feature_files')
       if feature_files is not None:
         feature_iter = feature_files.iter()
         findings.append(fmt.heading5('Scanner Results'))
diff --git a/turbinia/workers/hindsight.py b/turbinia/workers/hindsight.py
index a00da10..f679aa3 100644
--- a/turbinia/workers/hindsight.py
+++ b/turbinia/workers/hindsight.py
@@ -29,7 +29,15 @@ class HindsightTask(TurbiniaTask):
     super(HindsightTask, self).__init__()
 
   def run(self, evidence, result):
-    """Task to execute hindsight."""
+    """Task to execute hindsight.
+
+    Args:
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
+
+    Returns:
+        TurbiniaTaskResult object.
+    """
 
     # Create the new Evidence object that will be generated by this Task.
     output_evidence = TextFile()
diff --git a/turbinia/workers/plaso.py b/turbinia/workers/plaso.py
index 0215f09..2906ca7 100644
--- a/turbinia/workers/plaso.py
+++ b/turbinia/workers/plaso.py
@@ -32,8 +32,8 @@ class PlasoTask(TurbiniaTask):
     """Task that process data with Plaso.
 
     Args:
-        evidence: Path to data to process.
-        result: A TurbiniaTaskResult object to place task results into.
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
 
     Returns:
         TurbiniaTaskResult object.
diff --git a/turbinia/workers/psort.py b/turbinia/workers/psort.py
index 7c92af4..df44dd1 100644
--- a/turbinia/workers/psort.py
+++ b/turbinia/workers/psort.py
@@ -30,8 +30,8 @@ class PsortTask(TurbiniaTask):
     """Task that processes Plaso storage files with Psort.
 
     Args:
-        evidence: Path to data to process.
-        result: A TurbiniaTaskResult object to place task results into.
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
 
     Returns:
         TurbiniaTaskResult object.
diff --git a/turbinia/workers/sshd.py b/turbinia/workers/sshd.py
index c2822e8..d8ff667 100644
--- a/turbinia/workers/sshd.py
+++ b/turbinia/workers/sshd.py
@@ -32,11 +32,11 @@ class SSHDAnalysisTask(TurbiniaTask):
     """Run the sshd_config analysis worker.
 
     Args:
-       evidence (Evidence object):  The evidence to process
-       result (TurbiniaTaskResult): The object to place task results into.
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
 
     Returns:
-      TurbiniaTaskResult object.
+        TurbiniaTaskResult object.
     """
     # What type of evidence we should output.
     output_evidence = ReportText()
diff --git a/turbinia/workers/strings.py b/turbinia/workers/strings.py
index e55037d..447958e 100644
--- a/turbinia/workers/strings.py
+++ b/turbinia/workers/strings.py
@@ -29,7 +29,7 @@ class StringsAsciiTask(TurbiniaTask):
     """Run strings binary.
 
     Args:
-        evidence (Evidence object):  The evidence we will process
+        evidence (Evidence object):  The evidence we will process.
         result (TurbiniaTaskResult): The object to place task results into.
 
     Returns:
diff --git a/turbinia/workers/tomcat.py b/turbinia/workers/tomcat.py
index e2e6281..ee83db3 100644
--- a/turbinia/workers/tomcat.py
+++ b/turbinia/workers/tomcat.py
@@ -30,11 +30,13 @@ class TomcatAnalysisTask(TurbiniaTask):
 
   def run(self, evidence, result):
     """Run the Tomcat analysis worker.
+
     Args:
-       evidence (Evidence object):  The evidence to process
-       result (TurbiniaTaskResult): The object to place task results into.
+        evidence (Evidence object):  The evidence we will process.
+        result (TurbiniaTaskResult): The object to place task results into.
+
     Returns:
-      TurbiniaTaskResult object.
+        TurbiniaTaskResult object.
     """
     # What type of evidence we should output.
     output_evidence = ReportText()
diff --git a/turbinia/workers/volatility.py b/turbinia/workers/volatility.py
index 6810d21..a8384c6 100644
--- a/turbinia/workers/volatility.py
+++ b/turbinia/workers/volatility.py
@@ -40,7 +40,7 @@ class VolatilityTask(TurbiniaTask):
     """Run volatility against evidence.
 
     Args:
-        evidence (Evidence object): The evidence we will process
+        evidence (Evidence object):  The evidence we will process.
         result (TurbiniaTaskResult): The object to place task results into.
 
     Returns:


Test patch:
diff --git a/turbinia/workers/binary_extractor_test.py b/turbinia/workers/binary_extractor_test.py
new file mode 100644
index 0000000..2126669
--- /dev/null
+++ b/turbinia/workers/binary_extractor_test.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# Copyright 2016 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for the Binary Extractor task."""
+
+from __future__ import unicode_literals
+
+import json
+import mock
+import os
+import shutil
+import tempfile
+import unittest
+
+from turbinia import config
+from turbinia import evidence
+from turbinia.evidence import BinaryExtraction
+from turbinia.workers import binary_extractor
+from turbinia.workers.workers_test import TestTurbiniaTaskBase
+from turbinia.workers import TurbiniaTaskResult
+
+
+class BinaryExtractorTaskTest(TestTurbiniaTaskBase):
+  """Test for the Binary Extractor task."""
+
+  def setUp(self):
+    super(BinaryExtractorTaskTest, self).setUp(
+        task_class=binary_extractor.BinaryExtractorTask,
+        evidence_class=BinaryExtraction)
+    self.task.output_dir = self.task.base_output_dir
+    self.setResults(mock_run=False)
+    self.extraction_dir = os.path.join(
+        self.task.output_dir, 'extracted_binaries')
+
+    os.mkdir(self.extraction_dir)
+    hashes = [{
+        "sha256":
+            "553c231c45eda751710eabb479d08668f70464c14e60064190a7ec206f26b5f5",
+        "paths": ["bin/bzcat"]
+    }, {
+        "sha256":
+            "a106276270db8d3fe80a96dbb52f14f23f42a29bea12c68ac0f88d2e916471af",
+        "paths": ["bin/echo", "home/echo"]
+    }, {
+        "sha256":
+            "e21de6c5af94fa9d4e7f3295c8d25b93ab3d2d65982f5ef53c801669cc82dc47",
+        "paths": ["sbin/visudo"]
+    }, {
+        "sha256":
+            "129f4d0e36b38742fdfa8f1ea9a014818e4ce5c41d4a889435aecee58a1c7c39",
+        "paths": ["sbin/tune2fs"]
+    }]
+    with open(os.path.join(self.extraction_dir, 'hashes.json'),
+              'w') as json_file:
+      json.dump(hashes, json_file)
+
+  def tearDown(self):
+    if os.path.exists(self.base_output_dir):
+      shutil.rmtree(self.base_output_dir)
+
+  def testBinaryExtractorRun(self):
+    """Test BinaryExtractor task run."""
+    self.task.execute = mock.MagicMock(return_value=0)
+    result = self.task.run(self.evidence, self.result)
+
+    self.task.execute.assert_called_once()
+    self.assertIsInstance(result, TurbiniaTaskResult)
+
+  def test_check_extraction(self):
+    """Tests the check_extraction method."""
+
+    test_files = {
+        'bin': ['bzcat', 'echo'],
+        'sbin': ['visudo', 'tune2fs'],
+        'home': ['echo']
+    }
+
+    for subfolder, files in test_files.items():
+      os.makedirs(os.path.join(self.extraction_dir, subfolder))
+      for file in files:
+        os.mknod(os.path.join(self.extraction_dir, subfolder, file))
+
+    self.task.json_path = os.path.join(self.extraction_dir, 'hashes.json')
+    self.task.binary_extraction_dir = self.extraction_dir
+
+    binary_cnt, hash_cnt = self.task.check_extraction()
+
+    self.assertEqual(binary_cnt, 5)
+    self.assertEqual(hash_cnt, 4)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/turbinia/workers/bulk_extractor_test.py b/turbinia/workers/bulk_extractor_test.py
index df81094..978e002 100644
--- a/turbinia/workers/bulk_extractor_test.py
+++ b/turbinia/workers/bulk_extractor_test.py
@@ -20,8 +20,8 @@ from io import StringIO
 
 import os
 import unittest
-import mock
 import textwrap
+import mock
 
 from turbinia.evidence import BulkExtractorOutput
 from turbinia.workers import bulk_extractor
@@ -66,7 +66,7 @@ class BulkExtractorTaskTest(TestTurbiniaTaskBase):
         * Program: BULK_EXTRACTOR - 1.6.0-dev
         * Command Line: bulk_extractor /tmp/test-small.img -o /output/test-small.img
         * Start Time: 2019-09-27T16:34:48Z
-        * Elapsed Time: 2.389155
+        * Elapsed Time: N/A
         ##### There are no findings to report.""")
 
     summary_sample = "0 artifacts have been extracted."
@@ -82,9 +82,6 @@ class BulkExtractorTaskTest(TestTurbiniaTaskBase):
             <start_time>2019-09-27T16:34:48Z</start_time>
           </execution_environment>
         </creator>
-        <report>
-          <elapsed_seconds>2.389155</elapsed_seconds>
-        </report>
         </dfxml>""")
 
     str_io = StringIO(xml_sample)