diff --git a/volatility/cli/__init__.py b/volatility/cli/__init__.py index ce685808d..953cbc16c 100644 --- a/volatility/cli/__init__.py +++ b/volatility/cli/__init__.py @@ -167,7 +167,8 @@ class CommandLine: partial_args, _ = parser.parse_known_args(known_args) banner_output = sys.stdout - if renderers[partial_args.renderer].structured_output: + renderer = renderers[partial_args.renderer] + if renderer.structured_output: banner_output = sys.stderr banner_output.write("Volatility 3 Framework {}\n".format(constants.PACKAGE_VERSION)) @@ -227,6 +228,8 @@ class CommandLine: if isinstance(amagic, interfaces.configuration.ConfigurableInterface): self.populate_requirements_argparse(parser, amagic.__class__) + self.populate_requirements_renderer_options(parser, renderers) + subparser = parser.add_subparsers(title = "Plugins", dest = "plugin", description = "For plugin specific options, run '{} --help'".format( @@ -311,8 +314,9 @@ class CommandLine: try: # Construct and run the plugin + options = [] if constructed: - renderers[args.renderer]().render(constructed.run()) + renderer(options).render(constructed.run()) except (exceptions.VolatilityException) as excp: self.process_exceptions(excp) @@ -363,7 +367,7 @@ class CommandLine: detail = "{}".format(excp) caused_by = [ "An invalid symbol table", "A plugin requesting a bad symbol", - "A plugin requesting a symbol from the wrong table" + "A plugin requesting a symbol from the wrong table" ] elif isinstance(excp, exceptions.LayerException): general = "Volatility experienced a layer-related issue: {}".format(excp.layer_name) @@ -575,6 +579,24 @@ class CommandLine: required = not requirement.optional, **additional) + def populate_requirements_renderer_options(self, parser: argparse.ArgumentParser, + renderers: Dict[str, Type[text_renderer.CLIRenderer]]): + renderer_parser = parser.add_argument_group('renderer', 'Renderer options') + for renderer_name in renderers: + renderer = renderers[renderer_name] + for option in renderer.get_render_options(): + config_name = '-'.join([renderer.name, option.name]) + extra_options = { + 'help': option.description, + 'type': option.option_type, + 'dest': config_name.replace('-', '_') + } + if option.option_type == bool: + del extra_options['type'] + extra_options['action'] = 'store_true' + renderer_parser.add_argument("--" + config_name, + **extra_options) + def main(): """A convenience function for constructing and running the diff --git a/volatility/cli/text_renderer.py b/volatility/cli/text_renderer.py index 098e21d59..88e3e954f 100644 --- a/volatility/cli/text_renderer.py +++ b/volatility/cli/text_renderer.py @@ -128,6 +128,10 @@ class CLIRenderer(interfaces.renderers.Renderer): name = "unnamed" structured_output = False + @classmethod + def get_render_options(cls) -> List[RenderOption]: + return [] + class QuickTextRenderer(CLIRenderer): _type_renderers = { @@ -143,8 +147,12 @@ class QuickTextRenderer(CLIRenderer): name = "quick" - def get_render_options(self): - pass + @classmethod + def get_render_options(cls) -> List[RenderOption]: + return [ + RenderOption(name = 'skip-errors', description = 'Skips rows that would otherwise error', + option_type = bool, + default = False)] def render(self, grid: interfaces.renderers.TreeGrid) -> None: """Renders each column immediately to stdout. @@ -178,7 +186,7 @@ class QuickTextRenderer(CLIRenderer): return accumulator if not grid.populated: - grid.populate(visitor, outfd) + grid.populate(visitor, outfd, fail_on_errors = not self.options['skip-errors'].value) else: grid.visit(node = None, function = visitor, initial_accumulator = outfd) @@ -200,9 +208,6 @@ class CSVRenderer(CLIRenderer): name = "csv" structured_output = True - def get_render_options(self): - pass - def render(self, grid: interfaces.renderers.TreeGrid) -> None: """Renders each row immediately to stdout. @@ -242,8 +247,14 @@ class PrettyTextRenderer(CLIRenderer): name = "pretty" - def get_render_options(self): - pass + @classmethod + def get_render_options(cls) -> List[RenderOption]: + return [ + RenderOption(name = 'skip-errors', description = 'Skips rows that would otherwise error', + option_type = bool, + default = False), + RenderOption(name = 'separator', description = 'Dividing characters to separate columns', option_type = str, + default = ' | ')] def render(self, grid: interfaces.renderers.TreeGrid) -> None: """Renders each column immediately to stdout. @@ -260,13 +271,14 @@ class PrettyTextRenderer(CLIRenderer): sys.stderr.write("Formatting...\n") display_alignment = ">" - column_separator = " | " + column_separator = self.options['separator'].value tree_indent_column = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(20)) max_column_widths = dict([(column.name, len(column.name)) for column in grid.columns]) def visitor( - node: interfaces.renderers.TreeNode, accumulator: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] + node: interfaces.renderers.TreeNode, + accumulator: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] ) -> List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]]: # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case max_column_widths[tree_indent_column] = max(max_column_widths.get(tree_indent_column, 0), node.path_depth) @@ -283,7 +295,7 @@ class PrettyTextRenderer(CLIRenderer): final_output = [] # type: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] if not grid.populated: - grid.populate(visitor, final_output) + grid.populate(visitor, final_output, fail_on_errors = not self.options['skip-errors'].value) else: grid.visit(node = None, function = visitor, initial_accumulator = final_output) @@ -313,9 +325,6 @@ class JsonRenderer(CLIRenderer): name = 'JSON' structured_output = True - def get_render_options(self) -> List[RenderOption]: - pass - def output_result(self, outfd, result): """Outputs the JSON data to a file in a particular format""" outfd.write(json.dumps(result, indent = 2, sort_keys = True)) @@ -328,7 +337,7 @@ class JsonRenderer(CLIRenderer): {}, []) # type: Tuple[Dict[str, List[interfaces.renderers.TreeNode]], List[interfaces.renderers.TreeNode]] def visitor( - node: interfaces.renderers.TreeNode, accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]] + node: interfaces.renderers.TreeNode, accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]] ) -> Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]: # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case acc_map, final_tree = accumulator diff --git a/volatility/framework/constants/__init__.py b/volatility/framework/constants/__init__.py index b39fc2b7a..36c861be7 100644 --- a/volatility/framework/constants/__init__.py +++ b/volatility/framework/constants/__init__.py @@ -39,7 +39,7 @@ BANG = "!" # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 0 # Number of changes that only add to the interface +VERSION_MINOR = 1 # Number of changes that only add to the interface VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "-beta.1" diff --git a/volatility/framework/interfaces/__init__.py b/volatility/framework/interfaces/__init__.py index 17fd6f011..3636dc609 100644 --- a/volatility/framework/interfaces/__init__.py +++ b/volatility/framework/interfaces/__init__.py @@ -12,5 +12,5 @@ components of volatility to write plugins. # Import the submodules we want people to be able to use without importing them themselves # This will also avoid namespace issues, because people can use interfaces.layers to # avoid clashing with the layers package -from volatility.framework.interfaces import renderers, configuration, context, layers, objects, plugins, symbols, \ +from volatility.framework.interfaces import configuration, renderers, context, layers, objects, plugins, symbols, \ automagic diff --git a/volatility/framework/interfaces/renderers.py b/volatility/framework/interfaces/renderers.py index 54b9f922f..e01bd09b6 100644 --- a/volatility/framework/interfaces/renderers.py +++ b/volatility/framework/interfaces/renderers.py @@ -12,11 +12,45 @@ suitable output. import datetime from abc import abstractmethod, ABCMeta from collections import abc -from typing import Any, Callable, ClassVar, Generator, List, NamedTuple, Optional, TypeVar, Type, Tuple, Union +from typing import Any, Callable, ClassVar, Generator, List, NamedTuple, Optional, TypeVar, Type, Tuple, Union, Dict + +from volatility.framework import interfaces Column = NamedTuple('Column', [('name', str), ('type', Any)]) -RenderOption = Any + +class RenderOption: + """Class to support simple type options for renderers""" + + def __init__(self, name: str, description: str, option_type: Type[interfaces.configuration.SimpleTypes], + default: interfaces.configuration.SimpleTypes): + self._value = default + self._name = name + self._description = description + self._option_type = option_type + + @property + def name(self) -> str: + return self._name + + @property + def description(self) -> str: + return self._description + + @property + def option_type(self) -> Type[interfaces.configuration.SimpleTypes]: + return self._option_type + + @property + def value(self): + return self._value + + @value.setter + def value(self, val): + if isinstance(val, self.option_type): + self._value = val + else: + raise TypeError("Expected type {} cannot be filled by {}".format(repr(self.option_type), repr(type(val)))) class Renderer(metaclass = ABCMeta): @@ -25,11 +59,30 @@ class Renderer(metaclass = ABCMeta): def __init__(self, options: Optional[List[RenderOption]] = None) -> None: """Accepts an options object to configure the renderers.""" - # FIXME: Once the config option objects are in place, put the _type_check in place + self._options = self._create_named_dict(self.get_render_options()) + if options: + for option in options: + if option.name not in self._options: + raise ValueError("Unknown option provided to renderer") + self._options[option.name].value = option.value - @abstractmethod - def get_render_options(self) -> List[RenderOption]: + def _create_named_dict(self, option_list: List[RenderOption]) -> Dict[str, RenderOption]: + result = {} + for option in option_list: + if option.name in result: + raise KeyError("Option defined twice with the same renderer") + result[option.name] = option + return result + + @classmethod + def get_render_options(cls) -> List[RenderOption]: """Returns a list of rendering options.""" + return [] + + @property + def options(self) -> Dict[str, RenderOption]: + """Returns the dictionary of the available options""" + return self._options @abstractmethod def render(self, grid: 'TreeGrid') -> None: