Skip to content

Commit 4c7528f

Browse files
committed
CLI: Add in support for selective column output
1 parent 6d34e43 commit 4c7528f

File tree

3 files changed

+78
-24
lines changed

3 files changed

+78
-24
lines changed

volatility3/cli/__init__.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,14 @@ def run(self):
233233
default=False,
234234
action="store_true",
235235
)
236+
parser.add_argument(
237+
"--columns",
238+
help="Case-insensitive space separated list of prefixes to determine which columns to output if provided (otherwise all columns)",
239+
default=None,
240+
# action="extend", Can be enabled when python 3.8 is made default
241+
nargs="*",
242+
type=str,
243+
)
236244

237245
parser.set_defaults(**default_config)
238246

@@ -454,7 +462,9 @@ def run(self):
454462
try:
455463
# Construct and run the plugin
456464
if constructed:
457-
renderers[args.renderer]().render(constructed.run())
465+
renderer = renderers[args.renderer]()
466+
renderer.column_output_list = args.columns
467+
renderer.render(constructed.run())
458468
except exceptions.VolatilityException as excp:
459469
self.process_exceptions(excp)
460470

@@ -569,6 +579,10 @@ def process_exceptions(self, excp):
569579
"A plugin requesting a bad symbol",
570580
"A plugin requesting a symbol from the wrong table",
571581
]
582+
elif isinstance(excp, exceptions.RenderException):
583+
general = "Volatility experienced an issue when rendering the output:"
584+
detail = f"{excp}"
585+
caused_by = ["An invalid renderer option, such as no visible columns"]
572586
elif isinstance(excp, exceptions.LayerException):
573587
general = f"Volatility experienced a layer-related issue: {excp.layer_name}"
574588
detail = f"{excp}"

volatility3/cli/text_renderer.py

+59-23
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from functools import wraps
1212
from typing import Any, Callable, Dict, List, Tuple
1313

14-
from volatility3.framework import interfaces, renderers
14+
from volatility3.framework import exceptions, interfaces, renderers
1515
from volatility3.framework.renderers import format_hints
1616

1717
vollog = logging.getLogger(__name__)
@@ -135,6 +135,22 @@ class CLIRenderer(interfaces.renderers.Renderer):
135135
name = "unnamed"
136136
structured_output = False
137137

138+
column_output_list: list = None
139+
140+
def ignore_columns(
141+
self,
142+
column: interfaces.renderers.Column,
143+
ignored_columns: List[interfaces.renderers.Column],
144+
) -> Tuple[List[interfaces.renderers.Column], Any]:
145+
if self.column_output_list:
146+
accept = False
147+
for column_prefix in self.column_output_list:
148+
if column.name.lower().startswith(column_prefix.lower()):
149+
accept = True
150+
if not accept:
151+
ignored_columns.append(column)
152+
return ignored_columns
153+
138154

139155
class QuickTextRenderer(CLIRenderer):
140156
_type_renderers = {
@@ -166,9 +182,15 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None:
166182
outfd = sys.stdout
167183

168184
line = []
185+
ignored_columns = []
169186
for column in grid.columns:
170187
# Ignore the type because namedtuples don't realize they have accessible attributes
171-
line.append(f"{column.name}")
188+
ignored_columns = self.ignore_columns(column, ignored_columns)
189+
if column not in ignored_columns:
190+
line.append(f"column.name")
191+
192+
if not line:
193+
raise exceptions.RenderException("No visible columns")
172194
outfd.write("\n{}\n".format("\t".join(line)))
173195

174196
def visitor(node: interfaces.renderers.TreeNode, accumulator):
@@ -184,7 +206,8 @@ def visitor(node: interfaces.renderers.TreeNode, accumulator):
184206
renderer = self._type_renderers.get(
185207
column.type, self._type_renderers["default"]
186208
)
187-
line.append(renderer(node.values[column_index]))
209+
if column not in ignored_columns:
210+
line.append(renderer(node.values[column_index]))
188211
accumulator.write("{}".format("\t".join(line)))
189212
accumulator.flush()
190213
return accumulator
@@ -237,9 +260,12 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None:
237260
outfd = sys.stdout
238261

239262
header_list = ["TreeDepth"]
263+
ignored_columns = []
240264
for column in grid.columns:
241265
# Ignore the type because namedtuples don't realize they have accessible attributes
242-
header_list.append(f"{column.name}")
266+
ignored_columns = self.ignore_columns(column, ignored_columns)
267+
if column not in ignored_columns:
268+
header_list.append(f"{column.name}")
243269

244270
writer = csv.DictWriter(
245271
outfd, header_list, lineterminator="\n", escapechar="\\"
@@ -254,7 +280,8 @@ def visitor(node: interfaces.renderers.TreeNode, accumulator):
254280
renderer = self._type_renderers.get(
255281
column.type, self._type_renderers["default"]
256282
)
257-
row[f"{column.name}"] = renderer(node.values[column_index])
283+
if column not in ignored_columns:
284+
row[f"{column.name}"] = renderer(node.values[column_index])
258285
accumulator.writerow(row)
259286
return accumulator
260287

@@ -298,6 +325,10 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None:
298325
[(column.name, len(column.name)) for column in grid.columns]
299326
)
300327

328+
ignored_columns = []
329+
for column in grid.columns:
330+
ignored_columns = self.ignore_columns(column, ignored_columns)
331+
301332
def visitor(
302333
node: interfaces.renderers.TreeNode,
303334
accumulator: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]],
@@ -306,6 +337,7 @@ def visitor(
306337
max_column_widths[tree_indent_column] = max(
307338
max_column_widths.get(tree_indent_column, 0), node.path_depth
308339
)
340+
309341
line = {}
310342
for column_index in range(len(grid.columns)):
311343
column = grid.columns[column_index]
@@ -319,7 +351,8 @@ def visitor(
319351
max_column_widths[column.name] = max(
320352
max_column_widths.get(column.name, len(column.name)), field_width
321353
)
322-
line[column] = data.split("\n")
354+
if column not in ignored_columns:
355+
line[column] = data.split("\n")
323356
accumulator.append((node.path_depth, line))
324357
return accumulator
325358

@@ -335,18 +368,21 @@ def visitor(
335368
]
336369
for column_index in range(len(grid.columns)):
337370
column = grid.columns[column_index]
338-
format_string_list.append(
339-
"{"
340-
+ str(column_index + 1)
341-
+ ":"
342-
+ display_alignment
343-
+ str(max_column_widths[column.name])
344-
+ "s}"
345-
)
371+
if column not in ignored_columns:
372+
format_string_list.append(
373+
"{"
374+
+ str(column_index + 1)
375+
+ ":"
376+
+ display_alignment
377+
+ str(max_column_widths[column.name])
378+
+ "s}"
379+
)
346380

347381
format_string = column_separator.join(format_string_list) + "\n"
348382

349-
column_titles = [""] + [column.name for column in grid.columns]
383+
column_titles = [""] + [
384+
column.name for column in grid.columns if column not in ignored_columns
385+
]
350386
outfd.write(format_string.format(*column_titles))
351387
for depth, line in final_output:
352388
nums_line = max([len(line[column]) for column in line])
@@ -357,20 +393,14 @@ def visitor(
357393
outfd.write(
358394
format_string.format(
359395
"*" * depth,
360-
*[
361-
self.tab_stop(line[column][index])
362-
for column in grid.columns
363-
],
396+
*[self.tab_stop(line[column][index]) for column in line],
364397
)
365398
)
366399
else:
367400
outfd.write(
368401
format_string.format(
369402
" " * depth,
370-
*[
371-
self.tab_stop(line[column][index])
372-
for column in grid.columns
373-
],
403+
*[self.tab_stop(line[column][index]) for column in line],
374404
)
375405
)
376406

@@ -416,6 +446,10 @@ def render(self, grid: interfaces.renderers.TreeGrid):
416446
List[interfaces.renderers.TreeNode],
417447
] = ({}, [])
418448

449+
ignored_columns = []
450+
for column in grid.columns:
451+
ignored_columns = self.ignore_columns(column, ignored_columns)
452+
419453
def visitor(
420454
node: interfaces.renderers.TreeNode,
421455
accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]],
@@ -425,6 +459,8 @@ def visitor(
425459
node_dict: Dict[str, Any] = {"__children": []}
426460
for column_index in range(len(grid.columns)):
427461
column = grid.columns[column_index]
462+
if column in ignored_columns:
463+
continue
428464
renderer = self._type_renderers.get(
429465
column.type, self._type_renderers["default"]
430466
)

volatility3/framework/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ def __init__(self, module: str, *args) -> None:
117117
self.module = module
118118

119119

120+
class RenderException(VolatilityException):
121+
"""Thrown if there is an error during rendering"""
122+
123+
120124
class OfflineException(VolatilityException):
121125
"""Throw when a remote resource is requested but Volatility is in offline mode"""
122126

0 commit comments

Comments
 (0)