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/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..f63e97c189 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,24 @@ 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) + elif self.column_output_list is not None: + return [] + return ignored_columns + class QuickTextRenderer(CLIRenderer): _type_renderers = { @@ -166,9 +184,15 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: outfd = sys.stdout line = [] + 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 - 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 +208,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 +262,12 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: outfd = sys.stdout header_list = ["TreeDepth"] + 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 - 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 +282,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 +327,10 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None: [(column.name, len(column.name)) for column in grid.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) + def visitor( node: interfaces.renderers.TreeNode, accumulator: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]], @@ -306,6 +339,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 +353,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 +370,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 +395,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 +448,10 @@ def render(self, grid: interfaces.renderers.TreeGrid): List[interfaces.renderers.TreeNode], ] = ({}, []) + 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) + def visitor( node: interfaces.renderers.TreeNode, accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], @@ -425,6 +461,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/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 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""" diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index b13de18349..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)]) + +@dataclasses.dataclass +class Column: + name: str + type: Any + extra: bool = True + RenderOption = Any @@ -133,7 +140,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] )