Skip to content

Commit

Permalink
Merge pull request #1304 from volatilityfoundation/feature/column-sel…
Browse files Browse the repository at this point in the history
…ect-cli-only

Support hiding columns in the CLI
  • Loading branch information
ikelos authored Oct 13, 2024
2 parents d1d45ca + e817e72 commit 7b0cb4f
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 24 deletions.
13 changes: 13 additions & 0 deletions volatility3/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,14 @@ def run(self):
default=[],
action="append",
)
parser.add_argument(
"--hide-columns",
help="Case-insensitive space separated list of prefixes to determine which columns to hide in the output if provided",
default=None,
action="extend",
nargs="*",
type=str,
)

parser.set_defaults(**default_config)

Expand Down Expand Up @@ -488,6 +496,7 @@ def run(self):
grid = constructed.run()
renderer = renderers[args.renderer]()
renderer.filter = text_filter.CLIFilter(grid, args.filters)
renderer.column_hide_list = args.hide_columns
renderer.render(grid)
except exceptions.VolatilityException as excp:
self.process_exceptions(excp)
Expand Down Expand Up @@ -615,6 +624,10 @@ def process_exceptions(self, excp):
caused_by = [
"A required python module is not installed (install the module and re-run)"
]
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"]
else:
general = "Volatility encountered an unexpected situation."
detail = ""
Expand Down
92 changes: 68 additions & 24 deletions volatility3/cli/text_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from typing import Any, Callable, Dict, List, Tuple
from volatility3.cli import text_filter

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 @@ -141,6 +141,30 @@ class CLIRenderer(interfaces.renderers.Renderer):
name = "unnamed"
structured_output = False
filter: text_filter.CLIFilter = None
column_hide_list: list = None

def ignored_columns(
self,
grid: interfaces.renderers.TreeGrid,
) -> List[interfaces.renderers.Column]:
ignored_column_list = []
if self.column_hide_list:
for column in grid.columns:
accept = True
for column_prefix in self.column_hide_list:
if column.name.lower().startswith(column_prefix.lower()):
accept = False
if not accept:
ignored_column_list.append(column)
elif self.column_hide_list is None:
return []

if len(ignored_column_list) == len(grid.columns):
raise exceptions.RenderException("No visible columns to render")
vollog.info(
f"Hiding columns: {[column.name for column in ignored_column_list]}"
)
return ignored_column_list


class QuickTextRenderer(CLIRenderer):
Expand Down Expand Up @@ -173,9 +197,11 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None:
outfd = sys.stdout

line = []
ignore_columns = self.ignored_columns(grid)
for column in grid.columns:
# Ignore the type because namedtuples don't realize they have accessible attributes
line.append(f"{column.name}")
if column not in ignore_columns:
line.append(f"{column.name}")
outfd.write("\n{}\n".format("\t".join(line)))

def visitor(node: interfaces.renderers.TreeNode, accumulator):
Expand All @@ -184,7 +210,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 ignore_columns:
line.append(renderer(node.values[column_index]))

if self.filter and self.filter.filter(line):
return accumulator
Expand Down Expand Up @@ -245,11 +272,13 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None:
grid: The TreeGrid object to render
"""
outfd = sys.stdout
ignore_columns = self.ignored_columns(grid)

header_list = ["TreeDepth"]
for column in grid.columns:
# Ignore the type because namedtuples don't realize they have accessible attributes
header_list.append(f"{column.name}")
if column not in ignore_columns:
header_list.append(f"{column.name}")

writer = csv.DictWriter(
outfd, header_list, lineterminator="\n", escapechar="\\"
Expand All @@ -265,7 +294,10 @@ def visitor(node: interfaces.renderers.TreeNode, accumulator):
column.type, self._type_renderers["default"]
)
row[f"{column.name}"] = renderer(node.values[column_index])
line.append(row[f"{column.name}"])
if column not in ignore_columns:
line.append(row[f"{column.name}"])
else:
del row[f"{column.name}"]

if self.filter and self.filter.filter(line):
return accumulator
Expand Down Expand Up @@ -303,6 +335,7 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None:

sys.stderr.write("Formatting...\n")

ignore_columns = self.ignored_columns(grid)
display_alignment = ">"
column_separator = " | "

Expand Down Expand Up @@ -335,7 +368,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 ignore_columns:
line[column] = data.split("\n")
rendered_line.append(data)

if self.filter and self.filter.filter(rendered_line):
Expand All @@ -354,43 +388,49 @@ def visitor(
format_string_list = [
"{0:<" + str(max_column_widths.get(tree_indent_column, 0)) + "s}"
]
column_offset = 0
for column_index, column in enumerate(grid.columns):
format_string_list.append(
"{"
+ str(column_index + 1)
+ ":"
+ display_alignment
+ str(max_column_widths[column.name])
+ "s}"
)
if column not in ignore_columns:
format_string_list.append(
"{"
+ str(column_index - column_offset + 1)
+ ":"
+ display_alignment
+ str(max_column_widths[column.name])
+ "s}"
)
else:
column_offset += 1

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 ignore_columns
]

outfd.write(format_string.format(*column_titles))
for depth, line in final_output:
nums_line = max([len(line[column]) for column in line])
for column in line:
line[column] = line[column] + ([""] * (nums_line - len(line[column])))
if column in ignore_columns:
del line[column]
else:
line[column] = line[column] + (
[""] * (nums_line - len(line[column]))
)
for index in range(nums_line):
if index == 0:
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 @@ -436,6 +476,8 @@ def render(self, grid: interfaces.renderers.TreeGrid):
List[interfaces.renderers.TreeNode],
] = ({}, [])

ignore_columns = self.ignored_columns(grid)

def visitor(
node: interfaces.renderers.TreeNode,
accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]],
Expand All @@ -445,6 +487,8 @@ def visitor(
node_dict: Dict[str, Any] = {"__children": []}
line = []
for column_index, column in enumerate(grid.columns):
if column in ignore_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 @@ -126,3 +126,7 @@ def __init__(self, url: str, *args) -> None:

def __str__(self):
return f"Volatility 3 is offline: unable to access {self._url}"


class RenderException(VolatilityException):
"""Thrown if there is an error during rendering"""

0 comments on commit 7b0cb4f

Please sign in to comment.