Skip to content

Commit

Permalink
CLI: Add in support for selective column output
Browse files Browse the repository at this point in the history
  • Loading branch information
ikelos committed Jan 15, 2024
1 parent 66a4fc9 commit b90a637
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 24 deletions.
16 changes: 15 additions & 1 deletion volatility3/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,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,
)

# We have to filter out help, otherwise parse_known_args will trigger the help message before having
# processed the plugin choice or had the plugin subparser added.
Expand Down Expand Up @@ -444,7 +452,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)

Expand Down Expand Up @@ -515,6 +525,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}"
Expand Down
82 changes: 59 additions & 23 deletions volatility3/cli/text_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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="\\"
Expand All @@ -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

Expand Down Expand Up @@ -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]]],
Expand All @@ -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]
Expand All @@ -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

Expand All @@ -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])
Expand All @@ -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],
)
)

Expand Down Expand Up @@ -414,6 +444,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]]],
Expand All @@ -423,6 +457,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"]
)
Expand Down
4 changes: 4 additions & 0 deletions volatility3/framework/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down

0 comments on commit b90a637

Please sign in to comment.