diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 91bda7c665..207c7a75c8 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -230,6 +230,14 @@ def run(self): default=False, action="store_true", ) + parser.add_argument( + "--columns", + help="Case-insensitive space separated list of prefixes to determine which columns to output if provided (otherwise all columns)", + default=None, + # action="extend", Can be enabled when python 3.8 is made default + nargs="*", + type=str, + ) # We have to filter out help, otherwise parse_known_args will trigger the help message before having # processed the plugin choice or had the plugin subparser added. @@ -444,7 +452,9 @@ def run(self): try: # Construct and run the plugin if constructed: - renderers[args.renderer]().render(constructed.run()) + renderer = renderers[args.renderer]() + renderer.column_output_list = args.columns + renderer.render(constructed.run()) except exceptions.VolatilityException as excp: self.process_exceptions(excp) @@ -515,6 +525,10 @@ def process_exceptions(self, excp): "A plugin requesting a bad symbol", "A plugin requesting a symbol from the wrong table", ] + elif isinstance(excp, exceptions.RenderException): + general = "Volatility experienced an issue when rendering the output:" + detail = f"{excp}" + caused_by = ["An invalid renderer option, such as no visible columns"] elif isinstance(excp, exceptions.LayerException): general = f"Volatility experienced a layer-related issue: {excp.layer_name}" detail = f"{excp}" diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index ffb8d516bc..b97994f288 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -11,7 +11,7 @@ from functools import wraps from typing import Any, Callable, Dict, List, Tuple -from volatility3.framework import interfaces, renderers +from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.renderers import format_hints vollog = logging.getLogger(__name__) @@ -135,6 +135,22 @@ class CLIRenderer(interfaces.renderers.Renderer): name = "unnamed" structured_output = False + column_output_list: list = None + + def ignore_columns( + self, + column: interfaces.renderers.Column, + ignored_columns: List[interfaces.renderers.Column], + ) -> Tuple[List[interfaces.renderers.Column], Any]: + if self.column_output_list: + accept = False + for column_prefix in self.column_output_list: + if column.name.lower().startswith(column_prefix.lower()): + accept = True + if not accept: + ignored_columns.append(column) + return ignored_columns + class QuickTextRenderer(CLIRenderer): _type_renderers = { @@ -166,9 +182,15 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: outfd = sys.stdout line = [] + ignored_columns = [] for column in grid.columns: # Ignore the type because namedtuples don't realize they have accessible attributes - line.append(f"{column.name}") + ignored_columns = self.ignore_columns(column, ignored_columns) + if column not in ignored_columns: + line.append(f"column.name") + + if not line: + raise exceptions.RenderException("No visible columns") outfd.write("\n{}\n".format("\t".join(line))) def visitor(node: interfaces.renderers.TreeNode, accumulator): @@ -184,7 +206,8 @@ def visitor(node: interfaces.renderers.TreeNode, accumulator): renderer = self._type_renderers.get( column.type, self._type_renderers["default"] ) - line.append(renderer(node.values[column_index])) + if column not in ignored_columns: + line.append(renderer(node.values[column_index])) accumulator.write("{}".format("\t".join(line))) accumulator.flush() return accumulator @@ -237,9 +260,12 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: outfd = sys.stdout header_list = ["TreeDepth"] + ignored_columns = [] for column in grid.columns: # Ignore the type because namedtuples don't realize they have accessible attributes - header_list.append(f"{column.name}") + ignored_columns = self.ignore_columns(column, ignored_columns) + if column not in ignored_columns: + header_list.append(f"{column.name}") writer = csv.DictWriter( outfd, header_list, lineterminator="\n", escapechar="\\" @@ -254,7 +280,8 @@ def visitor(node: interfaces.renderers.TreeNode, accumulator): renderer = self._type_renderers.get( column.type, self._type_renderers["default"] ) - row[f"{column.name}"] = renderer(node.values[column_index]) + if column not in ignored_columns: + row[f"{column.name}"] = renderer(node.values[column_index]) accumulator.writerow(row) return accumulator @@ -298,6 +325,10 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: [(column.name, len(column.name)) for column in grid.columns] ) + ignored_columns = [] + for column in grid.columns: + ignored_columns = self.ignore_columns(column, ignored_columns) + def visitor( node: interfaces.renderers.TreeNode, accumulator: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]], @@ -306,6 +337,7 @@ def visitor( max_column_widths[tree_indent_column] = max( max_column_widths.get(tree_indent_column, 0), node.path_depth ) + line = {} for column_index in range(len(grid.columns)): column = grid.columns[column_index] @@ -319,7 +351,8 @@ def visitor( max_column_widths[column.name] = max( max_column_widths.get(column.name, len(column.name)), field_width ) - line[column] = data.split("\n") + if column not in ignored_columns: + line[column] = data.split("\n") accumulator.append((node.path_depth, line)) return accumulator @@ -335,18 +368,21 @@ def visitor( ] for column_index in range(len(grid.columns)): column = grid.columns[column_index] - format_string_list.append( - "{" - + str(column_index + 1) - + ":" - + display_alignment - + str(max_column_widths[column.name]) - + "s}" - ) + if column not in ignored_columns: + format_string_list.append( + "{" + + str(column_index + 1) + + ":" + + display_alignment + + str(max_column_widths[column.name]) + + "s}" + ) format_string = column_separator.join(format_string_list) + "\n" - column_titles = [""] + [column.name for column in grid.columns] + column_titles = [""] + [ + column.name for column in grid.columns if column not in ignored_columns + ] outfd.write(format_string.format(*column_titles)) for depth, line in final_output: nums_line = max([len(line[column]) for column in line]) @@ -357,20 +393,14 @@ def visitor( outfd.write( format_string.format( "*" * depth, - *[ - self.tab_stop(line[column][index]) - for column in grid.columns - ], + *[self.tab_stop(line[column][index]) for column in line], ) ) else: outfd.write( format_string.format( " " * depth, - *[ - self.tab_stop(line[column][index]) - for column in grid.columns - ], + *[self.tab_stop(line[column][index]) for column in line], ) ) @@ -414,6 +444,10 @@ def render(self, grid: interfaces.renderers.TreeGrid): List[interfaces.renderers.TreeNode], ] = ({}, []) + ignored_columns = [] + for column in grid.columns: + ignored_columns = self.ignore_columns(column, ignored_columns) + def visitor( node: interfaces.renderers.TreeNode, accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], @@ -423,6 +457,8 @@ def visitor( node_dict: Dict[str, Any] = {"__children": []} for column_index in range(len(grid.columns)): column = grid.columns[column_index] + if column in ignored_columns: + continue renderer = self._type_renderers.get( column.type, self._type_renderers["default"] ) diff --git a/volatility3/framework/exceptions.py b/volatility3/framework/exceptions.py index f8701683b0..cbe5409d12 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -117,6 +117,10 @@ def __init__(self, module: str, *args) -> None: self.module = module +class RenderException(VolatilityException): + """Thrown if there is an error during rendering""" + + class OfflineException(VolatilityException): """Throw when a remote resource is requested but Volatility is in offline mode"""