Skip to content

Commit 78e05f9

Browse files
authored
Merge pull request #1861 from kyrre/feature/parquet-arrow-renderer
Feature/parquet arrow renderer
2 parents 50b3c22 + 81f2e73 commit 78e05f9

File tree

5 files changed

+354
-20
lines changed

5 files changed

+354
-20
lines changed

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ dev = [
4242
"types-jsonschema>=4.23.0,<5",
4343
]
4444

45+
arrow = ["pyarrow>=17.0.0"]
46+
4547
test = [
4648
"volatility3[dev]",
4749
"pytest>=8.3.3,<9",

test/renderers/__init__.py

Whitespace-only changes.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import io
2+
import pytest
3+
from abc import ABC, abstractmethod
4+
from test import test_volatility
5+
6+
HAS_PYARROW = False
7+
try:
8+
import pyarrow as pa
9+
import pyarrow.parquet as pq
10+
import pyarrow.compute as pc
11+
HAS_PYARROW = True
12+
except ImportError:
13+
# The user doesn't have pyarrow installed, but HAS_PYARROW will be false so just continue
14+
pass
15+
16+
17+
@pytest.mark.skipif(not HAS_PYARROW, reason="pyarrow not installed")
18+
class TestArrowRendererBase(ABC):
19+
"""Base class for testing Arrow-based renderers.
20+
21+
Re-implements Windows and Linux plugin tests using PyArrow operations
22+
instead of text-based assertions.
23+
"""
24+
25+
renderer_format = None # Override in subclasses
26+
27+
@abstractmethod
28+
def _get_table_from_output(self, output_bytes) -> "pa.Table":
29+
"""Parse output bytes into Arrow table. Override in subclasses."""
30+
31+
def test_windows_generic_pslist(self, volatility, python, image):
32+
rc, out, _err = test_volatility.runvol_plugin(
33+
"windows.pslist.PsList",
34+
image,
35+
volatility,
36+
python,
37+
globalargs=("-r", self.renderer_format),
38+
)
39+
assert rc == 0
40+
41+
table = self._get_table_from_output(out)
42+
assert table.num_rows > 10
43+
44+
assert table.filter(pc.match_substring(pc.utf8_lower(table.column('ImageFileName')), "system")).num_rows > 0
45+
assert table.filter(pc.match_substring(pc.utf8_lower(table.column('ImageFileName')), "csrss.exe")).num_rows > 0
46+
assert table.filter(pc.match_substring(pc.utf8_lower(table.column('ImageFileName')), "svchost.exe")).num_rows > 0
47+
assert table.filter(pc.greater(table.column('PID'), 0)).num_rows == table.num_rows
48+
49+
def test_linux_generic_pslist(self, volatility, python, image):
50+
rc, out, _err = test_volatility.runvol_plugin(
51+
"linux.pslist.PsList",
52+
image,
53+
volatility,
54+
python,
55+
globalargs=("-r", self.renderer_format),
56+
)
57+
assert rc == 0
58+
59+
table = self._get_table_from_output(out)
60+
assert table.num_rows > 10
61+
62+
init_rows = table.filter(pc.match_substring(pc.utf8_lower(table.column('COMM')), "init"))
63+
systemd_rows = table.filter(pc.match_substring(pc.utf8_lower(table.column('COMM')), "systemd"))
64+
assert (init_rows.num_rows > 0) or (systemd_rows.num_rows > 0)
65+
66+
assert table.filter(pc.match_substring(pc.utf8_lower(table.column('COMM')), "watchdog")).num_rows > 0
67+
assert table.filter(pc.greater(table.column('PID'), 0)).num_rows == table.num_rows
68+
69+
def test_windows_generic_handles(self, volatility, python, image):
70+
rc, out, _err = test_volatility.runvol_plugin(
71+
"windows.handles.Handles",
72+
image,
73+
volatility,
74+
python,
75+
globalargs=("-r", self.renderer_format),
76+
pluginargs=("--pid", "4"),
77+
)
78+
assert rc == 0
79+
80+
table = self._get_table_from_output(out)
81+
assert table.num_rows > 500
82+
assert table.filter(pc.match_substring(pc.utf8_lower(table.column('Name')), "machine\\system")).num_rows > 0
83+
84+
def test_linux_generic_lsof(self, volatility, python, image):
85+
rc, out, _err = test_volatility.runvol_plugin(
86+
"linux.lsof.Lsof",
87+
image,
88+
volatility,
89+
python,
90+
globalargs=("-r", self.renderer_format),
91+
)
92+
assert rc == 0
93+
94+
table = self._get_table_from_output(out)
95+
assert table.num_rows > 35
96+
97+
class TestParquetRenderer(TestArrowRendererBase):
98+
renderer_format = "parquet"
99+
100+
def _get_table_from_output(self, output_bytes):
101+
return pq.read_table(io.BytesIO(output_bytes))
102+
103+
104+
class TestArrowRenderer(TestArrowRendererBase):
105+
renderer_format = "arrow"
106+
107+
def _get_table_from_output(self, output_bytes):
108+
return pa.ipc.open_stream(io.BytesIO(output_bytes)).read_all()
109+

volatility3/cli/__init__.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,6 @@ def run(self):
106106

107107
volatility3.framework.require_interface_version(2, 0, 0)
108108

109-
renderers = dict(
110-
[
111-
(x.name.lower(), x)
112-
for x in framework.class_subclasses(text_renderer.CLIRenderer)
113-
]
114-
)
115-
116109
# Load up system defaults
117110
delayed_logs, default_config = self.load_system_defaults("vol.json")
118111

@@ -193,14 +186,6 @@ def run(self):
193186
default=False,
194187
action="store_true",
195188
)
196-
parser.add_argument(
197-
"-r",
198-
"--renderer",
199-
metavar="RENDERER",
200-
help=f"Determines how to render the output ({', '.join(list(renderers))})",
201-
default="quick",
202-
choices=list(renderers),
203-
)
204189
parser.add_argument(
205190
"-f",
206191
"--file",
@@ -270,11 +255,6 @@ def run(self):
270255
known_args = [arg for arg in sys.argv if arg != "--help" and arg != "-h"]
271256
partial_args, _ = parser.parse_known_args(known_args)
272257

273-
banner_output = sys.stdout
274-
if renderers[partial_args.renderer].structured_output:
275-
banner_output = sys.stderr
276-
banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n")
277-
278258
### Start up logging
279259
if partial_args.log:
280260
file_logger = logging.FileHandler(partial_args.log)
@@ -346,6 +326,24 @@ def run(self):
346326

347327
plugin_list = framework.list_plugins()
348328

329+
# Discover renderers after plugin directories are loaded
330+
# This allows custom renderers to be found in plugin directories
331+
renderers = dict(
332+
[
333+
(x.name.lower(), x)
334+
for x in framework.class_subclasses(text_renderer.CLIRenderer)
335+
]
336+
)
337+
338+
parser.add_argument(
339+
"-r",
340+
"--renderer",
341+
metavar="RENDERER",
342+
help=f"Determines how to render the output ({', '.join(list(renderers))})",
343+
default="quick",
344+
choices=list(renderers),
345+
)
346+
349347
seen_automagics = set()
350348
chosen_configurables_list = {}
351349
for amagic in automagics:
@@ -392,6 +390,13 @@ def run(self):
392390
# before all the plugins have been added
393391
argcomplete.autocomplete(parser)
394392
args = parser.parse_args()
393+
394+
# Display banner - redirect to stderr if using structured output
395+
banner_output = sys.stdout
396+
if renderers[args.renderer].structured_output:
397+
banner_output = sys.stderr
398+
banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n")
399+
395400
if args.plugin is None:
396401
parser.error(
397402
f"Please select a plugin to run (see '{self.CLI_NAME} --help' for options"

0 commit comments

Comments
 (0)