From 453f52ba3e192643b8729a45e0d0bb6b4dd5d7d0 Mon Sep 17 00:00:00 2001 From: Mike Auty Date: Tue, 31 Dec 2024 23:04:58 +0000 Subject: [PATCH] CLI: Add in concept of CellRenderer --- volatility3/cli/text_renderer.py | 74 +++++++++++-------- volatility3/framework/interfaces/renderers.py | 58 ++++++++++++--- 2 files changed, 92 insertions(+), 40 deletions(-) diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index b1944ae5a..6cfd10ced 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -9,10 +9,11 @@ import random import string import sys from functools import wraps -from typing import Any, Callable, Dict, List, Tuple +from typing import Any, Callable, Dict, List, Tuple, TypeVar from volatility3.cli import text_filter from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.interfaces.renderers import BaseAbsentValue from volatility3.framework.renderers import format_hints vollog = logging.getLogger(__name__) @@ -79,8 +80,9 @@ def multitypedata_as_text(value: format_hints.MultiTypeData) -> str: return string_representation.split("\x00")[0] return hex_bytes_as_text(value) +T = TypeVar("T") -def optional(func: Callable) -> Callable: +def optional(func: Callable[[BaseAbsentValue| T], str]) -> Callable[[T], str]: @wraps(func) def wrapped(x: Any) -> str: if isinstance(x, interfaces.renderers.BaseAbsentValue): @@ -137,9 +139,41 @@ def display_disassembly(disasm: interfaces.renderers.Disassembly) -> str: return QuickTextRenderer._type_renderers[bytes](disasm.data) +class CLITypeRenderer(interfaces.renderers.TypeRendererInterface): + def __init__(self, func): + super().__init__(func = optional(func)) + + +class LayerDataRenderer(CLITypeRenderer): + """Renders a LayerData object into data/bytes""" + def __init__(self): + def render(data: interfaces.renderers.LayerData| BaseAbsentValue): + if isinstance(data, BaseAbsentValue): + # FIXME: Do something cleverer here + return "" + data = data.context.layers[data.layer_name].read(data.offset, data.length) + return " ".join(f"{b:02x}" for b in data) + + render_func = render + return super().__init__(render_func) + + class CLIRenderer(interfaces.renderers.Renderer): """Class to add specific requirements for CLI renderers.""" + _type_renderers = { + format_hints.Bin: CLITypeRenderer(lambda x: f"0b{x:b}"), + format_hints.Hex: CLITypeRenderer(lambda x: f"0x{x:x}"), + format_hints.HexBytes: CLITypeRenderer(hex_bytes_as_text), + format_hints.MultiTypeData: CLITypeRenderer(multitypedata_as_text), + interfaces.renderers.Disassembly: CLITypeRenderer(display_disassembly), + bytes: CLITypeRenderer(lambda x: " ".join(f"{b:02x}" for b in x)), + interfaces.renderers.LayerData: LayerDataRenderer(), + datetime.datetime: CLITypeRenderer(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")), + "default": CLITypeRenderer(lambda x: f"{x}"), + } + + name = "unnamed" structured_output = False filter: text_filter.CLIFilter = None @@ -170,21 +204,11 @@ class CLIRenderer(interfaces.renderers.Renderer): class QuickTextRenderer(CLIRenderer): - _type_renderers = { - format_hints.Bin: optional(lambda x: f"0b{x:b}"), - format_hints.Hex: optional(lambda x: f"0x{x:x}"), - format_hints.HexBytes: optional(hex_bytes_as_text), - format_hints.MultiTypeData: quoted_optional(multitypedata_as_text), - interfaces.renderers.Disassembly: optional(display_disassembly), - bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)), - datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")), - "default": optional(lambda x: f"{x}"), - } name = "quick" def get_render_options(self): - pass + return [] def render(self, grid: interfaces.renderers.TreeGrid) -> None: """Renders each column immediately to stdout. @@ -242,7 +266,7 @@ class NoneRenderer(CLIRenderer): name = "none" def get_render_options(self): - pass + return [] def render(self, grid: interfaces.renderers.TreeGrid) -> None: if not grid.populated: @@ -250,22 +274,12 @@ class NoneRenderer(CLIRenderer): class CSVRenderer(CLIRenderer): - _type_renderers = { - format_hints.Bin: optional(lambda x: f"0b{x:b}"), - format_hints.Hex: optional(lambda x: f"0x{x:x}"), - format_hints.HexBytes: optional(hex_bytes_as_text), - format_hints.MultiTypeData: optional(multitypedata_as_text), - interfaces.renderers.Disassembly: optional(display_disassembly), - bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)), - datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")), - "default": optional(lambda x: f"{x}"), - } name = "csv" structured_output = True def get_render_options(self): - pass + return [] def render(self, grid: interfaces.renderers.TreeGrid) -> None: """Renders each row immediately to stdout. @@ -316,12 +330,10 @@ class CSVRenderer(CLIRenderer): class PrettyTextRenderer(CLIRenderer): - _type_renderers = QuickTextRenderer._type_renderers - name = "pretty" def get_render_options(self): - pass + return [] def render(self, grid: interfaces.renderers.TreeGrid) -> None: """Renders each column immediately to stdout. @@ -380,7 +392,7 @@ class PrettyTextRenderer(CLIRenderer): accumulator.append((node.path_depth, line)) return accumulator - final_output: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]] = [] + final_output: List[Tuple[int, Dict[interfaces.renderers.Column, str]]] = [] if not grid.populated: grid.populate(visitor, final_output) else: @@ -418,7 +430,7 @@ class PrettyTextRenderer(CLIRenderer): del line[column] else: line[column] = line[column] + ( - [""] * (nums_line - len(line[column])) + "" * (nums_line - len(line[column])) ) for index in range(nums_line): if index == 0: @@ -463,7 +475,7 @@ class JsonRenderer(CLIRenderer): structured_output = True def get_render_options(self) -> List[interfaces.renderers.RenderOption]: - pass + return [] def output_result(self, outfd, result): """Outputs the JSON data to a file in a particular format""" diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index 477d743d1..6aa0ad7aa 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -9,9 +9,8 @@ renderer interface which can interact with a TreeGrid to produce suitable output. """ -from dataclasses import dataclass +import dataclasses import datetime -from volatility3.framework import interfaces from abc import ABCMeta, abstractmethod from collections import abc from typing import ( @@ -27,6 +26,14 @@ from typing import ( TypeVar, Union, ) +from typing import Dict +import functools + +from volatility3.framework import interfaces + + +class BaseAbsentValue: + """Class that represents values which are not present for some reason.""" class Column(NamedTuple): @@ -36,11 +43,34 @@ class Column(NamedTuple): RenderOption = Any +T = TypeVar("T") + +class TypeRendererInterface: + type = T + + def __init__(self, func: Optional[Callable] = None, options: Optional[Dict[str, Any]] = None): + self._options = options or {} + setattr(self, "render", func) + + @property + def options(self): + return self._options + + def render(self, data: T|BaseAbsentValue) -> Any: + """Renders a specific datatype""" + return "" + + def __call__(self, data: T|BaseAbsentValue) -> Any: + """Shortcut for render""" + return self.render(data) + class Renderer(metaclass=ABCMeta): """Class that defines the interface that all output renderers must support.""" + _type_renderers: Dict[Union[Type, str], Callable] + 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 @@ -104,10 +134,6 @@ class TreeNode(abc.Sequence, metaclass=ABCMeta): """ -class BaseAbsentValue: - """Class that represents values which are not present for some reason.""" - - class Disassembly: """A class to indicate that the bytes provided should be disassembled (based on the architecture)""" @@ -126,12 +152,24 @@ class Disassembly: self.offset = offset -@dataclass +@dataclasses.dataclass class LayerData(object): + """Layer data + + This requires the contex to be passed in, in case plugins want to use multiple contexts + and to ensure the TreeGrid interface doesn't change, since this would break all existing plugins""" + context: 'interfaces.context.ContextInterface' layer_name: str offset: int length: int + @staticmethod + def from_object(object: 'interfaces.objects.ObjectInterface', size: Optional[int] = None): + return LayerData(context = object._context, + layer_name = object.vol.layer_name, + offset = object.vol.offset, + length = size or object.vol.size) + # We don't class these off a shared base, because the BaseTypes must only # contain the types that the validator will accept (which would not include the base) @@ -164,6 +202,7 @@ class TreeGrid(metaclass=ABCMeta): and to create cycles. """ + # TODO: Figure out why this isn't just BaseTypes (which includes AbsentValues' base_types: ClassVar[Tuple] = ( int, str, @@ -171,13 +210,14 @@ class TreeGrid(metaclass=ABCMeta): bytes, datetime.datetime, Disassembly, + LayerData ) def __init__( self, columns: ColumnsType, generator: Generator, - context: Optional[interfaces.context.ContextInterface] = None, + context: Optional['interfaces.context.ContextInterface'] = None, ) -> None: """Constructs a TreeGrid object using a specific set of columns. @@ -192,7 +232,7 @@ class TreeGrid(metaclass=ABCMeta): self._context = context @property - def context(self) -> Optional[interfaces.context.ContextInterface]: + def context(self) -> Optional['interfaces.context.ContextInterface']: """Returns the context value for the tree grid (to retrieve data items) This is a property to ensure the renderers don't try changing the context for any reason