Renderers: Add in very rudimentary renderer options

This allows the CLI to expose options that the renderers can offer.
These options are limited (string, int, bool and bytes), so no lists
or choices, etc.

Each renderer's options are also global options, meaning they're always
present (although grouped).  There's two ways of handling this:

a) Prefix each option with the renderer's name, duplicate options are
unique
b) Deduplicate options with duplicate names and prefix 'renderer'

The current choice is a) because b) might confuse people that an option
which only applied to one renderer would be available for all, but
comes with the inconvenience that users must adapt the option name for
those that are duplicated with the same meaning.

This is at least functional and extendable in the future but still a bit
clunky.  We could have brought the entire configuration mechanism into
play, but each renderer would require a context and a config_path and
so on, meaning massive overkill and a non-addition-only API change.
This commit is contained in:
Mike Auty
2020-11-01 22:51:39 +00:00
parent 1d8081e409
commit 64c139095f
5 changed files with 109 additions and 25 deletions
+25 -3
View File
@@ -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 '{} <plugin> --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
+24 -15
View File
@@ -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
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
+58 -5
View File
@@ -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: