Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
50767ff
add arrow and parquet renderers
kyrre Jun 27, 2025
a0b0076
formatting with ruff
kyrre Jun 27, 2025
1c78562
linting
kyrre Jun 27, 2025
0389596
Fix: Avoid import-time errors if pyarrow is missing
machinelearningdesignpatterns Jun 28, 2025
78d5653
fix: handle layerdata to pyarrow conversion
machinelearningdesignpatterns Jun 28, 2025
d3a6b03
moved arrow/parquet parsers to it's own file and reverted changes
machinelearningdesignpatterns Jul 14, 2025
8479fa1
moved renderer parsing to after the plugins are imported
machinelearningdesignpatterns Jul 14, 2025
066076b
black formatting
machinelearningdesignpatterns Jul 14, 2025
0f32bec
restore orginal text_renderer
machinelearningdesignpatterns Jul 14, 2025
72422d0
fix bug: hadn't properly renamed the write_data method name
machinelearningdesignpatterns Jul 21, 2025
e7c1126
moved the arrow/parquet renderers to a subdir for renderers
machinelearningdesignpatterns Jul 21, 2025
6437148
remove old parquet renderer
machinelearningdesignpatterns Jul 21, 2025
11b3123
fix: update data type mappings for format_hints.Bin and format_hints.…
machinelearningdesignpatterns Jul 22, 2025
533b305
Added tests for the Parquet and Arrow renderers
machinelearningdesignpatterns Aug 2, 2025
eb7daa9
flatten tree output
machinelearningdesignpatterns Aug 6, 2025
3c830e6
black & ruff
machinelearningdesignpatterns Aug 6, 2025
d21a81d
fix: update type hints to support Python 3.8
machinelearningdesignpatterns Sep 5, 2025
86fe448
Update volatility3/framework/plugins/renderers/parquet_renderer.py
kyrre Sep 15, 2025
4c7a9b5
Update volatility3/framework/plugins/renderers/parquet_renderer.py
kyrre Sep 15, 2025
4af48ae
Update volatility3/framework/plugins/renderers/parquet_renderer.py
kyrre Sep 15, 2025
8090d72
Update volatility3/framework/plugins/renderers/parquet_renderer.py
kyrre Sep 15, 2025
81f2e73
Update test/renderers/test_parquet_renderers.py
ikelos Sep 15, 2025
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
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ dev = [
"types-jsonschema>=4.23.0,<5",
]

arrow = ["pyarrow>=17.0.0"]

test = [
"volatility3[dev]",
"pytest>=8.3.3,<9",
Expand Down
Empty file added test/renderers/__init__.py
Empty file.
109 changes: 109 additions & 0 deletions test/renderers/test_parquet_renderers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import io
import pytest
from abc import ABC, abstractmethod
from test import test_volatility

HAS_PYARROW = False
try:
import pyarrow as pa
import pyarrow.parquet as pq
import pyarrow.compute as pc
HAS_PYARROW = True
except ImportError:
# The user doesn't have pyarrow installed, but HAS_PYARROW will be false so just continue
pass


@pytest.mark.skipif(not HAS_PYARROW, reason="pyarrow not installed")
class TestArrowRendererBase(ABC):
"""Base class for testing Arrow-based renderers.

Re-implements Windows and Linux plugin tests using PyArrow operations
instead of text-based assertions.
"""

renderer_format = None # Override in subclasses

@abstractmethod
def _get_table_from_output(self, output_bytes) -> "pa.Table":
"""Parse output bytes into Arrow table. Override in subclasses."""

def test_windows_generic_pslist(self, volatility, python, image):
rc, out, _err = test_volatility.runvol_plugin(
"windows.pslist.PsList",
image,
volatility,
python,
globalargs=("-r", self.renderer_format),
)
assert rc == 0

table = self._get_table_from_output(out)
assert table.num_rows > 10

assert table.filter(pc.match_substring(pc.utf8_lower(table.column('ImageFileName')), "system")).num_rows > 0
assert table.filter(pc.match_substring(pc.utf8_lower(table.column('ImageFileName')), "csrss.exe")).num_rows > 0
assert table.filter(pc.match_substring(pc.utf8_lower(table.column('ImageFileName')), "svchost.exe")).num_rows > 0
assert table.filter(pc.greater(table.column('PID'), 0)).num_rows == table.num_rows

def test_linux_generic_pslist(self, volatility, python, image):
rc, out, _err = test_volatility.runvol_plugin(
"linux.pslist.PsList",
image,
volatility,
python,
globalargs=("-r", self.renderer_format),
)
assert rc == 0

table = self._get_table_from_output(out)
assert table.num_rows > 10

init_rows = table.filter(pc.match_substring(pc.utf8_lower(table.column('COMM')), "init"))
systemd_rows = table.filter(pc.match_substring(pc.utf8_lower(table.column('COMM')), "systemd"))
assert (init_rows.num_rows > 0) or (systemd_rows.num_rows > 0)

assert table.filter(pc.match_substring(pc.utf8_lower(table.column('COMM')), "watchdog")).num_rows > 0
assert table.filter(pc.greater(table.column('PID'), 0)).num_rows == table.num_rows

def test_windows_generic_handles(self, volatility, python, image):
rc, out, _err = test_volatility.runvol_plugin(
"windows.handles.Handles",
image,
volatility,
python,
globalargs=("-r", self.renderer_format),
pluginargs=("--pid", "4"),
)
assert rc == 0

table = self._get_table_from_output(out)
assert table.num_rows > 500
assert table.filter(pc.match_substring(pc.utf8_lower(table.column('Name')), "machine\\system")).num_rows > 0

def test_linux_generic_lsof(self, volatility, python, image):
rc, out, _err = test_volatility.runvol_plugin(
"linux.lsof.Lsof",
image,
volatility,
python,
globalargs=("-r", self.renderer_format),
)
assert rc == 0

table = self._get_table_from_output(out)
assert table.num_rows > 35

class TestParquetRenderer(TestArrowRendererBase):
renderer_format = "parquet"

def _get_table_from_output(self, output_bytes):
return pq.read_table(io.BytesIO(output_bytes))


class TestArrowRenderer(TestArrowRendererBase):
renderer_format = "arrow"

def _get_table_from_output(self, output_bytes):
return pa.ipc.open_stream(io.BytesIO(output_bytes)).read_all()

45 changes: 25 additions & 20 deletions volatility3/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,6 @@ def run(self):

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

renderers = dict(
[
(x.name.lower(), x)
for x in framework.class_subclasses(text_renderer.CLIRenderer)
]
)

# Load up system defaults
delayed_logs, default_config = self.load_system_defaults("vol.json")

Expand Down Expand Up @@ -193,14 +186,6 @@ def run(self):
default=False,
action="store_true",
)
parser.add_argument(
"-r",
"--renderer",
metavar="RENDERER",
help=f"Determines how to render the output ({', '.join(list(renderers))})",
default="quick",
choices=list(renderers),
)
parser.add_argument(
"-f",
"--file",
Expand Down Expand Up @@ -270,11 +255,6 @@ def run(self):
known_args = [arg for arg in sys.argv if arg != "--help" and arg != "-h"]
partial_args, _ = parser.parse_known_args(known_args)

banner_output = sys.stdout
if renderers[partial_args.renderer].structured_output:
banner_output = sys.stderr
banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n")

### Start up logging
if partial_args.log:
file_logger = logging.FileHandler(partial_args.log)
Expand Down Expand Up @@ -346,6 +326,24 @@ def run(self):

plugin_list = framework.list_plugins()

# Discover renderers after plugin directories are loaded
# This allows custom renderers to be found in plugin directories
renderers = dict(
[
(x.name.lower(), x)
for x in framework.class_subclasses(text_renderer.CLIRenderer)
]
)

parser.add_argument(
"-r",
"--renderer",
metavar="RENDERER",
help=f"Determines how to render the output ({', '.join(list(renderers))})",
default="quick",
choices=list(renderers),
)

seen_automagics = set()
chosen_configurables_list = {}
for amagic in automagics:
Expand Down Expand Up @@ -392,6 +390,13 @@ def run(self):
# before all the plugins have been added
argcomplete.autocomplete(parser)
args = parser.parse_args()

# Display banner - redirect to stderr if using structured output
banner_output = sys.stdout
if renderers[args.renderer].structured_output:
banner_output = sys.stderr
banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n")

if args.plugin is None:
parser.error(
f"Please select a plugin to run (see '{self.CLI_NAME} --help' for options"
Expand Down
Loading
Loading