Add in file producer/consumer API.

This commit is contained in:
Mike Auty
2018-04-14 19:51:49 +01:00
parent 90641befd2
commit f25d059d49
3 changed files with 79 additions and 41 deletions
+25 -2
View File
@@ -12,6 +12,7 @@ import argparse
import inspect
import json
import logging
import os
import sys
import typing
@@ -53,11 +54,11 @@ class PrintedProgress(object):
print(message, end = ' ' * (self._max_message_len - message_len))
class CommandLine(object):
class CommandLine(interfaces.plugins.FileConsumerInterface):
"""Constructs a command-line interface object for users to run plugins"""
def __init__(self):
pass
self.output_dir = None
def run(self):
"""Executes the command line module, taking the system arguments, determining the plugin to run and then running it"""
@@ -75,6 +76,8 @@ class CommandLine(object):
parser.add_argument("-p", "--plugins", help = "Semi-colon separated list of paths to find plugins",
default = "", type = str)
parser.add_argument("-v", "--verbosity", help = "Increase output verbosity", default = 0, action = "count")
parser.add_argument("-o", "--output-dir", help = "Directory in which to output any generated files",
default = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')), type = str)
parser.add_argument("-q", "--quiet", help = "Remove progress feedback", default = False, action = 'store_true')
parser.add_argument("-l", "--log", help = "Log output to a file as well as the console", default = None,
type = str)
@@ -204,9 +207,29 @@ class CommandLine(object):
with open("config.json", "w") as f:
json.dump(dict(constructed.build_configuration()), f, sort_keys = True, indent = 2)
self.output_dir = args.output_dir
constructed.set_file_consumer(self)
# Construct and run the plugin
text.QuickTextRenderer().render(constructed.run())
def consume_file(self, filedata: interfaces.plugins.FileInterface):
"""Consumes a file as produced by a plugin"""
if self.output_dir is None:
raise ValueError("Output directory has not been correctly specified")
os.makedirs(self.output_dir, exist_ok = True)
pref_name_array = filedata.preferred_filename.split('.')
filename, extension = os.path.join(self.output_dir, '.'.join(pref_name_array[:-1])), pref_name_array[-1]
output_filename = "{}.{}".format(filename, extension)
if not os.path.exists(output_filename):
with open(output_filename, "wb") as current_file:
current_file.write(filedata.data.getbuffer())
vollog.log(logging.INFO, "Saved stored plugin file: {}".format(output_filename))
else:
vollog.warning("Refusing to overwrite an existing file: {}".format(output_filename))
def populate_requirements_argparse(self,
parser: argparse.ArgumentParser,
configurable: typing.Type[interfaces.configuration.ConfigurableInterface]):
@@ -4,6 +4,7 @@ They are called and carry out some algorithms on data stored in layers using obj
"""
# Configuration interfaces must be imported separately, since we're part of interfaces and can't import ourselves
import io
import logging
import typing
from abc import ABCMeta, abstractmethod
@@ -18,6 +19,25 @@ if typing.TYPE_CHECKING:
from volatility.framework import interfaces, renderers
class FileInterface(validity.ValidityRoutines, metaclass = ABCMeta):
"""Class for storing Files in the plugin as a means to output a file or files when necessary"""
def __init__(self, filename: str, data: bytes = None):
self.preferred_filename = filename
self.data = io.BytesIO(data)
class FileConsumerInterface(object):
"""Class for consuming files potentially produced by plugins
We use the producer/consumer model to ensure we can avoid running out of memory by storing every file produced.
The downside is, we can't provide much feedback to the producer about what happened to their file (other than exceptions).
"""
def consume_file(self, file: FileInterface) -> None:
"""Consumes a file as passed back to a UI by a plugin"""
#
# Plugins
# - Take in relevant number of TranslationLayers (of specified type)
@@ -49,6 +69,17 @@ class PluginInterface(interfaces_configuration.ConfigurableInterface, validity.V
if self.unsatisfied(context, config_path):
vollog.warning("Plugin failed validation")
raise exceptions.PluginRequirementException("The plugin configuration failed to validate")
self._file_consumer = None # type: FileConsumerInterface
def set_file_consumer(self, consumer: FileConsumerInterface) -> None:
self._file_consumer = self._check_type(consumer, FileConsumerInterface)
def produce_file(self, filedata: FileInterface) -> None:
"""Adds a file to the plugin's file store and returns the chosen filename for the file"""
if self._file_consumer:
self._file_consumer.consume_file(filedata)
else:
vollog.debug("No file consumer specified to consume: {}".format(filedata.preferred_filename))
@classmethod
def get_requirements(cls) -> typing.List['interfaces.configuration.RequirementInterface']:
+23 -39
View File
@@ -1,24 +1,21 @@
import os
import volatility.framework.interfaces.plugins as interfaces_plugins
import volatility.plugins.windows.vadinfo as vadinfo
import volatility.plugins.windows.pslist as pslist
from volatility.framework import renderers
from volatility.framework.objects import utility
from volatility.framework.configuration import requirements
import logging
import volatility.framework.interfaces.plugins as interfaces_plugins
import volatility.plugins.windows.pslist as pslist
import volatility.plugins.windows.vadinfo as vadinfo
from volatility.framework import renderers
from volatility.framework.objects import utility
vollog = logging.getLogger()
class VadDump(interfaces_plugins.PluginInterface):
"""Dumps process memory ranges"""
@classmethod
def get_requirements(cls):
# Since we're calling the plugin, make sure we have the plugin's requirements
return vadinfo.VadInfo.get_requirements() + [requirements.StringRequirement(name = "outdir",
description = "Output directory",
default = None,
optional = False)]
return vadinfo.VadInfo.get_requirements()
def _generator(self, procs):
@@ -34,26 +31,23 @@ class VadDump(interfaces_plugins.PluginInterface):
for vad in plugin.list_vads(proc):
try:
file_name = os.path.join(self.config["outdir"],
"pid.{0}.vad.{1:#x}-{2:#x}.dmp".format(proc.UniqueProcessId, vad.get_start(), vad.get_end()))
filedata = interfaces_plugins.FileInterface(
"pid.{0}.vad.{1:#x}-{2:#x}.dmp".format(proc.UniqueProcessId,
vad.get_start(),
vad.get_end()))
if os.path.exists(file_name):
raise FileExistsError
offset = vad.get_start()
out_of_range = vad.get_start() + vad.get_end()
while offset < out_of_range:
to_read = min(chunk_size, out_of_range - offset)
data = proc_layer.read(offset, to_read, pad = True)
if not data:
break
filedata.data.write(data)
offset += to_read
with open(file_name, "wb") as handle:
offset = vad.get_start()
out_of_range = vad.get_start() + vad.get_end()
while offset < out_of_range:
to_read = min(chunk_size, out_of_range - offset)
data = proc_layer.read(offset, to_read, pad = True)
if not data:
break
handle.write(data)
offset += to_read
result_text = "Saved to {}".format(os.path.basename(file_name))
except FileExistsError:
result_text = "Refusing to overwrite the existing {}".format(file_name)
self.produce_file(filedata)
result_text = "Stored {}".format(filedata.preferred_filename)
except Exception:
result_text = "Unable to dump {0:#x} - {1:#x}".format(vad.get_start(), vad.get_end())
@@ -62,16 +56,6 @@ class VadDump(interfaces_plugins.PluginInterface):
result_text))
def run(self):
try:
# the optional=False requirement should make sure this always exists
os.makedirs(self.config["outdir"])
except FileExistsError:
pass
except OSError:
# is this what we want to raise here?
raise OSError("Cannot create the desired output directory!")
plugin = pslist.PsList(self.context, "plugins.VadDump")
return renderers.TreeGrid([("PID", int),