Skip to content

Commit 7b0cb4f

Browse files
authored
Merge pull request #1304 from volatilityfoundation/feature/column-select-cli-only
Support hiding columns in the CLI
2 parents d1d45ca + e817e72 commit 7b0cb4f

File tree

3 files changed

+85
-24
lines changed

3 files changed

+85
-24
lines changed

volatility3/cli/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,14 @@ def run(self):
256256
default=[],
257257
action="append",
258258
)
259+
parser.add_argument(
260+
"--hide-columns",
261+
help="Case-insensitive space separated list of prefixes to determine which columns to hide in the output if provided",
262+
default=None,
263+
action="extend",
264+
nargs="*",
265+
type=str,
266+
)
259267

260268
parser.set_defaults(**default_config)
261269

@@ -488,6 +496,7 @@ def run(self):
488496
grid = constructed.run()
489497
renderer = renderers[args.renderer]()
490498
renderer.filter = text_filter.CLIFilter(grid, args.filters)
499+
renderer.column_hide_list = args.hide_columns
491500
renderer.render(grid)
492501
except exceptions.VolatilityException as excp:
493502
self.process_exceptions(excp)
@@ -615,6 +624,10 @@ def process_exceptions(self, excp):
615624
caused_by = [
616625
"A required python module is not installed (install the module and re-run)"
617626
]
627+
elif isinstance(excp, exceptions.RenderException):
628+
general = "Volatility experienced an issue when rendering the output:"
629+
detail = f"{excp}"
630+
caused_by = ["An invalid renderer option, such as no visible columns"]
618631
else:
619632
general = "Volatility encountered an unexpected situation."
620633
detail = ""

volatility3/cli/text_renderer.py

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from typing import Any, Callable, Dict, List, Tuple
1313
from volatility3.cli import text_filter
1414

15-
from volatility3.framework import interfaces, renderers
15+
from volatility3.framework import exceptions, interfaces, renderers
1616
from volatility3.framework.renderers import format_hints
1717

1818
vollog = logging.getLogger(__name__)
@@ -141,6 +141,30 @@ class CLIRenderer(interfaces.renderers.Renderer):
141141
name = "unnamed"
142142
structured_output = False
143143
filter: text_filter.CLIFilter = None
144+
column_hide_list: list = None
145+
146+
def ignored_columns(
147+
self,
148+
grid: interfaces.renderers.TreeGrid,
149+
) -> List[interfaces.renderers.Column]:
150+
ignored_column_list = []
151+
if self.column_hide_list:
152+
for column in grid.columns:
153+
accept = True
154+
for column_prefix in self.column_hide_list:
155+
if column.name.lower().startswith(column_prefix.lower()):
156+
accept = False
157+
if not accept:
158+
ignored_column_list.append(column)
159+
elif self.column_hide_list is None:
160+
return []
161+
162+
if len(ignored_column_list) == len(grid.columns):
163+
raise exceptions.RenderException("No visible columns to render")
164+
vollog.info(
165+
f"Hiding columns: {[column.name for column in ignored_column_list]}"
166+
)
167+
return ignored_column_list
144168

145169

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

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

181207
def visitor(node: interfaces.renderers.TreeNode, accumulator):
@@ -184,7 +210,8 @@ def visitor(node: interfaces.renderers.TreeNode, accumulator):
184210
renderer = self._type_renderers.get(
185211
column.type, self._type_renderers["default"]
186212
)
187-
line.append(renderer(node.values[column_index]))
213+
if column not in ignore_columns:
214+
line.append(renderer(node.values[column_index]))
188215

189216
if self.filter and self.filter.filter(line):
190217
return accumulator
@@ -245,11 +272,13 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None:
245272
grid: The TreeGrid object to render
246273
"""
247274
outfd = sys.stdout
275+
ignore_columns = self.ignored_columns(grid)
248276

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

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

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

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

338+
ignore_columns = self.ignored_columns(grid)
306339
display_alignment = ">"
307340
column_separator = " | "
308341

@@ -335,7 +368,8 @@ def visitor(
335368
max_column_widths[column.name] = max(
336369
max_column_widths.get(column.name, len(column.name)), field_width
337370
)
338-
line[column] = data.split("\n")
371+
if column not in ignore_columns:
372+
line[column] = data.split("\n")
339373
rendered_line.append(data)
340374

341375
if self.filter and self.filter.filter(rendered_line):
@@ -354,43 +388,49 @@ def visitor(
354388
format_string_list = [
355389
"{0:<" + str(max_column_widths.get(tree_indent_column, 0)) + "s}"
356390
]
391+
column_offset = 0
357392
for column_index, column in enumerate(grid.columns):
358-
format_string_list.append(
359-
"{"
360-
+ str(column_index + 1)
361-
+ ":"
362-
+ display_alignment
363-
+ str(max_column_widths[column.name])
364-
+ "s}"
365-
)
393+
if column not in ignore_columns:
394+
format_string_list.append(
395+
"{"
396+
+ str(column_index - column_offset + 1)
397+
+ ":"
398+
+ display_alignment
399+
+ str(max_column_widths[column.name])
400+
+ "s}"
401+
)
402+
else:
403+
column_offset += 1
366404

367405
format_string = column_separator.join(format_string_list) + "\n"
368406

369-
column_titles = [""] + [column.name for column in grid.columns]
407+
column_titles = [""] + [
408+
column.name for column in grid.columns if column not in ignore_columns
409+
]
410+
370411
outfd.write(format_string.format(*column_titles))
371412
for depth, line in final_output:
372413
nums_line = max([len(line[column]) for column in line])
373414
for column in line:
374-
line[column] = line[column] + ([""] * (nums_line - len(line[column])))
415+
if column in ignore_columns:
416+
del line[column]
417+
else:
418+
line[column] = line[column] + (
419+
[""] * (nums_line - len(line[column]))
420+
)
375421
for index in range(nums_line):
376422
if index == 0:
377423
outfd.write(
378424
format_string.format(
379425
"*" * depth,
380-
*[
381-
self.tab_stop(line[column][index])
382-
for column in grid.columns
383-
],
426+
*[self.tab_stop(line[column][index]) for column in line],
384427
)
385428
)
386429
else:
387430
outfd.write(
388431
format_string.format(
389432
" " * depth,
390-
*[
391-
self.tab_stop(line[column][index])
392-
for column in grid.columns
393-
],
433+
*[self.tab_stop(line[column][index]) for column in line],
394434
)
395435
)
396436

@@ -436,6 +476,8 @@ def render(self, grid: interfaces.renderers.TreeGrid):
436476
List[interfaces.renderers.TreeNode],
437477
] = ({}, [])
438478

479+
ignore_columns = self.ignored_columns(grid)
480+
439481
def visitor(
440482
node: interfaces.renderers.TreeNode,
441483
accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]],
@@ -445,6 +487,8 @@ def visitor(
445487
node_dict: Dict[str, Any] = {"__children": []}
446488
line = []
447489
for column_index, column in enumerate(grid.columns):
490+
if column in ignore_columns:
491+
continue
448492
renderer = self._type_renderers.get(
449493
column.type, self._type_renderers["default"]
450494
)

volatility3/framework/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,7 @@ def __init__(self, url: str, *args) -> None:
126126

127127
def __str__(self):
128128
return f"Volatility 3 is offline: unable to access {self._url}"
129+
130+
131+
class RenderException(VolatilityException):
132+
"""Thrown if there is an error during rendering"""

0 commit comments

Comments
 (0)