diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 50b5579afd..b4e993dacc 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -256,6 +256,14 @@ def run(self): default=[], action="append", ) + parser.add_argument( + "--hide-columns", + help="Case-insensitive space separated list of prefixes to determine which columns to hide in the output if provided", + default=None, + action="extend", + nargs="*", + type=str, + ) parser.set_defaults(**default_config) @@ -488,6 +496,7 @@ def run(self): grid = constructed.run() renderer = renderers[args.renderer]() renderer.filter = text_filter.CLIFilter(grid, args.filters) + renderer.column_hide_list = args.hide_columns renderer.render(grid) except exceptions.VolatilityException as excp: self.process_exceptions(excp) @@ -615,6 +624,10 @@ def process_exceptions(self, excp): caused_by = [ "A required python module is not installed (install the module and re-run)" ] + 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"] else: general = "Volatility encountered an unexpected situation." detail = "" diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index da0cdf62a7..96970d3cef 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -12,7 +12,7 @@ from typing import Any, Callable, Dict, List, Tuple from volatility3.cli import text_filter -from volatility3.framework import interfaces, renderers +from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.renderers import format_hints vollog = logging.getLogger(__name__) @@ -141,6 +141,30 @@ class CLIRenderer(interfaces.renderers.Renderer): name = "unnamed" structured_output = False filter: text_filter.CLIFilter = None + column_hide_list: list = None + + def ignored_columns( + self, + grid: interfaces.renderers.TreeGrid, + ) -> List[interfaces.renderers.Column]: + ignored_column_list = [] + if self.column_hide_list: + for column in grid.columns: + accept = True + for column_prefix in self.column_hide_list: + if column.name.lower().startswith(column_prefix.lower()): + accept = False + if not accept: + ignored_column_list.append(column) + elif self.column_hide_list is None: + return [] + + if len(ignored_column_list) == len(grid.columns): + raise exceptions.RenderException("No visible columns to render") + vollog.info( + f"Hiding columns: {[column.name for column in ignored_column_list]}" + ) + return ignored_column_list class QuickTextRenderer(CLIRenderer): @@ -173,9 +197,11 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: outfd = sys.stdout line = [] + ignore_columns = self.ignored_columns(grid) for column in grid.columns: # Ignore the type because namedtuples don't realize they have accessible attributes - line.append(f"{column.name}") + if column not in ignore_columns: + line.append(f"{column.name}") outfd.write("\n{}\n".format("\t".join(line))) def visitor(node: interfaces.renderers.TreeNode, accumulator): @@ -184,7 +210,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 ignore_columns: + line.append(renderer(node.values[column_index])) if self.filter and self.filter.filter(line): return accumulator @@ -245,11 +272,13 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: grid: The TreeGrid object to render """ outfd = sys.stdout + ignore_columns = self.ignored_columns(grid) header_list = ["TreeDepth"] for column in grid.columns: # Ignore the type because namedtuples don't realize they have accessible attributes - header_list.append(f"{column.name}") + if column not in ignore_columns: + header_list.append(f"{column.name}") writer = csv.DictWriter( outfd, header_list, lineterminator="\n", escapechar="\\" @@ -265,7 +294,10 @@ def visitor(node: interfaces.renderers.TreeNode, accumulator): column.type, self._type_renderers["default"] ) row[f"{column.name}"] = renderer(node.values[column_index]) - line.append(row[f"{column.name}"]) + if column not in ignore_columns: + line.append(row[f"{column.name}"]) + else: + del row[f"{column.name}"] if self.filter and self.filter.filter(line): return accumulator @@ -303,6 +335,7 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: sys.stderr.write("Formatting...\n") + ignore_columns = self.ignored_columns(grid) display_alignment = ">" column_separator = " | " @@ -335,7 +368,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 ignore_columns: + line[column] = data.split("\n") rendered_line.append(data) if self.filter and self.filter.filter(rendered_line): @@ -354,43 +388,49 @@ def visitor( format_string_list = [ "{0:<" + str(max_column_widths.get(tree_indent_column, 0)) + "s}" ] + column_offset = 0 for column_index, column in enumerate(grid.columns): - format_string_list.append( - "{" - + str(column_index + 1) - + ":" - + display_alignment - + str(max_column_widths[column.name]) - + "s}" - ) + if column not in ignore_columns: + format_string_list.append( + "{" + + str(column_index - column_offset + 1) + + ":" + + display_alignment + + str(max_column_widths[column.name]) + + "s}" + ) + else: + column_offset += 1 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 ignore_columns + ] + outfd.write(format_string.format(*column_titles)) for depth, line in final_output: nums_line = max([len(line[column]) for column in line]) for column in line: - line[column] = line[column] + ([""] * (nums_line - len(line[column]))) + if column in ignore_columns: + del line[column] + else: + line[column] = line[column] + ( + [""] * (nums_line - len(line[column])) + ) for index in range(nums_line): if index == 0: 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], ) ) @@ -436,6 +476,8 @@ def render(self, grid: interfaces.renderers.TreeGrid): List[interfaces.renderers.TreeNode], ] = ({}, []) + ignore_columns = self.ignored_columns(grid) + def visitor( node: interfaces.renderers.TreeNode, accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], @@ -445,6 +487,8 @@ def visitor( node_dict: Dict[str, Any] = {"__children": []} line = [] for column_index, column in enumerate(grid.columns): + if column in ignore_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..c44fb4f2e2 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -126,3 +126,7 @@ def __init__(self, url: str, *args) -> None: def __str__(self): return f"Volatility 3 is offline: unable to access {self._url}" + + +class RenderException(VolatilityException): + """Thrown if there is an error during rendering"""