CLI: Add in concept of CellRenderer

This commit is contained in:
Mike Auty
2025-03-23 00:41:43 +00:00
parent 55151f546d
commit 453f52ba3e
2 changed files with 92 additions and 40 deletions
+43 -31
View File
@@ -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"""
+49 -9
View File
@@ -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