mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-09-21 17:17:39 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user