Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Add in support for selective column output #1085

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions API_CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ API Changes
When an addition to the existing API is made, the minor version is bumped.
When an API feature or function is removed or changed, the major version is bumped.

2.6.0
=====
Plugins defining treegrid columns can use three-tuples, including a new third "extra" parameter to determine if a column
should be hidden by default.

2.5.0
=====
Add in support for specifying a type override for object_from_symbol
Expand Down
16 changes: 15 additions & 1 deletion volatility3/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,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,
)

parser.set_defaults(**default_config)

Expand Down Expand Up @@ -454,7 +462,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 @@ -569,6 +579,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
84 changes: 61 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,24 @@ 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)
elif self.column_output_list is not None:
return []
return ignored_columns


class QuickTextRenderer(CLIRenderer):
_type_renderers = {
Expand Down Expand Up @@ -166,9 +184,15 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None:
outfd = sys.stdout

line = []
ignored_columns = [column for column in grid.columns if column.extra == True]
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 +208,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 +262,12 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None:
outfd = sys.stdout

header_list = ["TreeDepth"]
ignored_columns = [column for column in grid.columns if column.extra == True]
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 +282,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 +327,10 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> None:
[(column.name, len(column.name)) for column in grid.columns]
)

ignored_columns = [column for column in grid.columns if column.extra == True]
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 +339,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 +353,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 +370,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 +395,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 @@ -416,6 +448,10 @@ def render(self, grid: interfaces.renderers.TreeGrid):
List[interfaces.renderers.TreeNode],
] = ({}, [])

ignored_columns = [column for column in grid.columns if column.extra == True]
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 @@ -425,6 +461,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: 2 additions & 2 deletions volatility3/framework/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@

# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 5 # Number of changes that only add to the interface
VERSION_PATCH = 2 # Number of changes that do not change the interface
VERSION_MINOR = 6 # Number of changes that only add to the interface
VERSION_PATCH = 0 # Number of changes that do not change the interface
VERSION_SUFFIX = ""

# TODO: At version 2.0.0, remove the symbol_shift feature
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
11 changes: 9 additions & 2 deletions volatility3/framework/interfaces/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
suitable output.
"""

import dataclasses
import datetime
from abc import abstractmethod, ABCMeta
from collections import abc
Expand All @@ -26,7 +27,13 @@
Union,
)

Column = NamedTuple("Column", [("name", str), ("type", Any)])

@dataclasses.dataclass
class Column:
name: str
type: Any
extra: bool = True


RenderOption = Any

Expand Down Expand Up @@ -133,7 +140,7 @@ def __init__(
Type[BaseAbsentValue],
Type[Disassembly],
]
ColumnsType = List[Tuple[str, BaseTypes]]
ColumnsType = List[Tuple[str, BaseTypes, bool]]
VisitorSignature = Callable[[TreeNode, _Type], _Type]


Expand Down
13 changes: 10 additions & 3 deletions volatility3/framework/renderers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ class TreeGrid(interfaces.renderers.TreeGrid):

def __init__(
self,
columns: List[Tuple[str, interfaces.renderers.BaseTypes]],
columns: interfaces.renderers.ColumnsType,
generator: Optional[Iterable[Tuple[int, Tuple]]],
) -> None:
"""Constructs a TreeGrid object using a specific set of columns.
Expand All @@ -185,15 +185,22 @@ def __init__(
converted_columns: List[interfaces.renderers.Column] = []
if len(columns) < 1:
raise ValueError("Columns must be a list containing at least one column")
for name, column_type in columns:
for column_info in columns:
if len(column_info) < 3:
name, column_type = column_info
extra = False
else:
name, column_type, extra = column_info
is_simple_type = issubclass(column_type, self.base_types)
if not is_simple_type:
raise TypeError(
"Column {}'s type is not a simple type: {}".format(
name, column_type.__class__.__name__
)
)
converted_columns.append(interfaces.renderers.Column(name, column_type))
converted_columns.append(
interfaces.renderers.Column(name, column_type, extra)
)
self.RowStructure = RowStructureConstructor(
[column.name for column in converted_columns]
)
Expand Down
Loading