From 4c7528fcae16d39c752f236d54e1e0357b0efed7 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Mon, 15 Jan 2024 00:55:57 +0000 Subject: [PATCH 1/4] CLI: Add in support for selective column output --- volatility3/cli/__init__.py | 16 +++++- volatility3/cli/text_renderer.py | 82 +++++++++++++++++++++-------- volatility3/framework/exceptions.py | 4 ++ 3 files changed, 78 insertions(+), 24 deletions(-) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 9393c0805d..0c03aa56d8 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -233,6 +233,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, + ) parser.set_defaults(**default_config) @@ -454,7 +462,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) @@ -569,6 +579,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 6e58ee68d7..090a82d4a4 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], ) ) @@ -416,6 +446,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]]], @@ -425,6 +459,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""" From e381a0078037210f8650b80eade94224acd8421b Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 18 Feb 2024 21:58:26 +0000 Subject: [PATCH 2/4] Core: Support hidden by default columns in plugins This adds the ability to provide a third argument in the column definition ("name", "type", "extra"). Extra columns will not be shown by the CLI by default, but can be seen by passing an empty list to the CLI (ie, provide no parameters to the `--columns` flag). Otherwise any columns passed to `--columns` will be displayed and no other columns will. If no valid column name are passed to columns, then the normal list of columns (not including extra) will be displayed. All other UIs will need to implement support for the extra column, otherwise they will silently ignore it. --- volatility3/cli/text_renderer.py | 12 +++++++----- volatility3/framework/interfaces/renderers.py | 4 ++-- volatility3/framework/renderers/__init__.py | 13 ++++++++++--- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 090a82d4a4..f63e97c189 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -149,6 +149,8 @@ def ignore_columns( accept = True if not accept: ignored_columns.append(column) + elif self.column_output_list is not None: + return [] return ignored_columns @@ -182,12 +184,12 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: outfd = sys.stdout line = [] - ignored_columns = [] + ignored_columns = [column for column in grid.columns if column.extra == True] for column in grid.columns: # Ignore the type because namedtuples don't realize they have accessible attributes ignored_columns = self.ignore_columns(column, ignored_columns) if column not in ignored_columns: - line.append(f"column.name") + line.append(f"{column.name}") if not line: raise exceptions.RenderException("No visible columns") @@ -260,7 +262,7 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: outfd = sys.stdout header_list = ["TreeDepth"] - ignored_columns = [] + ignored_columns = [column for column in grid.columns if column.extra == True] for column in grid.columns: # Ignore the type because namedtuples don't realize they have accessible attributes ignored_columns = self.ignore_columns(column, ignored_columns) @@ -325,7 +327,7 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: [(column.name, len(column.name)) for column in grid.columns] ) - ignored_columns = [] + ignored_columns = [column for column in grid.columns if column.extra == True] for column in grid.columns: ignored_columns = self.ignore_columns(column, ignored_columns) @@ -446,7 +448,7 @@ def render(self, grid: interfaces.renderers.TreeGrid): List[interfaces.renderers.TreeNode], ] = ({}, []) - ignored_columns = [] + ignored_columns = [column for column in grid.columns if column.extra == True] for column in grid.columns: ignored_columns = self.ignore_columns(column, ignored_columns) diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index b13de18349..4692a112cb 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -26,7 +26,7 @@ Union, ) -Column = NamedTuple("Column", [("name", str), ("type", Any)]) +Column = NamedTuple("Column", [("name", str), ("type", Any), ("extra", bool)]) RenderOption = Any @@ -133,7 +133,7 @@ def __init__( Type[BaseAbsentValue], Type[Disassembly], ] -ColumnsType = List[Tuple[str, BaseTypes]] +ColumnsType = List[Tuple[str, BaseTypes, bool]] VisitorSignature = Callable[[TreeNode, _Type], _Type] diff --git a/volatility3/framework/renderers/__init__.py b/volatility3/framework/renderers/__init__.py index 43bb59a210..1d6e39d3fd 100644 --- a/volatility3/framework/renderers/__init__.py +++ b/volatility3/framework/renderers/__init__.py @@ -166,7 +166,7 @@ class TreeGrid(interfaces.renderers.TreeGrid): def __init__( self, - columns: List[Tuple[str, interfaces.renderers.BaseTypes]], + columns: interfaces.renderers.ColumnsType, generator: Optional[Iterable[Tuple[int, Tuple]]], ) -> None: """Constructs a TreeGrid object using a specific set of columns. @@ -185,7 +185,12 @@ def __init__( converted_columns: List[interfaces.renderers.Column] = [] if len(columns) < 1: raise ValueError("Columns must be a list containing at least one column") - for name, column_type in columns: + for column_info in columns: + if len(column_info) < 3: + name, column_type = column_info + extra = False + else: + name, column_type, extra = column_info is_simple_type = issubclass(column_type, self.base_types) if not is_simple_type: raise TypeError( @@ -193,7 +198,9 @@ def __init__( name, column_type.__class__.__name__ ) ) - converted_columns.append(interfaces.renderers.Column(name, column_type)) + converted_columns.append( + interfaces.renderers.Column(name, column_type, extra) + ) self.RowStructure = RowStructureConstructor( [column.name for column in converted_columns] ) From 262642ee37c80930715da4cfaf17fb4101ba2ce4 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 18 Feb 2024 22:14:25 +0000 Subject: [PATCH 3/4] Core: Bump to allow a third value when creating columns --- API_CHANGES.md | 5 +++++ volatility3/framework/constants/__init__.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/API_CHANGES.md b/API_CHANGES.md index 61d8781fba..4adeaaf4da 100644 --- a/API_CHANGES.md +++ b/API_CHANGES.md @@ -4,6 +4,11 @@ API Changes When an addition to the existing API is made, the minor version is bumped. When an API feature or function is removed or changed, the major version is bumped. +2.6.0 +===== +Plugins defining treegrid columns can use three-tuples, including a new third "extra" parameter to determine if a column +should be hidden by default. + 2.5.0 ===== Add in support for specifying a type override for object_from_symbol diff --git a/volatility3/framework/constants/__init__.py b/volatility3/framework/constants/__init__.py index 09dded076e..9aaafb9335 100644 --- a/volatility3/framework/constants/__init__.py +++ b/volatility3/framework/constants/__init__.py @@ -44,8 +44,8 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 5 # Number of changes that only add to the interface -VERSION_PATCH = 2 # Number of changes that do not change the interface +VERSION_MINOR = 6 # Number of changes that only add to the interface +VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" # TODO: At version 2.0.0, remove the symbol_shift feature From ac0c8ee5d93ec88491e5ed5c14b71415c0f94fbc Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Sun, 18 Feb 2024 23:59:08 +0000 Subject: [PATCH 4/4] Renderers: Convert column to dataclass Slightly heavier weight classes rather than tuples, but allows greater flexibility and future assignment of new attribtues. --- volatility3/framework/interfaces/renderers.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index 4692a112cb..5835277740 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -9,6 +9,7 @@ suitable output. """ +import dataclasses import datetime from abc import abstractmethod, ABCMeta from collections import abc @@ -26,7 +27,13 @@ Union, ) -Column = NamedTuple("Column", [("name", str), ("type", Any), ("extra", bool)]) + +@dataclasses.dataclass +class Column: + name: str + type: Any + extra: bool = True + RenderOption = Any