mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-09-25 11:04:52 +02:00
CLI: Add in concept of CellRenderer
This commit is contained in:
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user