Repository: volatilityfoundation/volatility3
Pull request #1661: Testing: enhance general design
Author: login: Abyss-W4tcher
Labels: name: parity-release
Created: 2025-03-05T09:29:24Z | Merged: 2025-03-07T21:31:18Z

Hi,

This PR is supported by multiple goals:

- Separate OS-specific tests into distinct files
- Group each plugin test methods under a common class
- Provide the ability to tie a test to a specific image, or to make it generic
- Abstract samples as Python classes

No significant updating was made to check themselves, as that is the purpose of upcoming PRs. Of course, this will come to evolve in the near future, with additional samples and plugin coverage once the `volatility3-data` infrastructure is ready.

I did not operate the macOS migration, as it isn't included in the pipeline right now, and is lower priority compared to Windows and Linux.

I am sorry if there are big additions and removals, but as I said I did not change the tests themselves.

Thanks :)

Needs #1660 before tests can be run.


Diff:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index ce27224575..6fbb93572d 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -43,12 +43,12 @@ jobs:
     - name: Testing...
       run: |
         # VolShell
-        pytest ./test/test_volatility.py --volatility=volshell.py --image-dir=./test_images -k test_windows_volshell -v
-        pytest ./test/test_volatility.py --volatility=volshell.py --image-dir=./test_images -k test_linux_volshell -v
+        pytest ./test/plugins/windows/windows.py --volatility=volshell.py --image-dir=./test_images -k test_windows_volshell -v
+        pytest ./test/plugins/linux/linux.py --volatility=volshell.py --image-dir=./test_images -k test_linux_volshell -v
 
         # Volatility
-        pytest ./test/test_volatility.py --volatility=vol.py --image-dir=./test_images -k "test_windows and not test_windows_volshell" -v
-        pytest ./test/test_volatility.py --volatility=vol.py --image-dir=./test_images -k "test_linux and not test_linux_volshell" -v
+        pytest ./test/plugins/windows/windows.py --volatility=vol.py --image-dir=./test_images -k "test_windows and not test_windows_volshell" -v
+        pytest ./test/plugins/linux/linux.py --volatility=vol.py --image-dir=./test_images -k "test_linux and not test_linux_volshell" -v
 
     - name: Clean up post-test
       run: |
diff --git a/test/__init__.py b/test/__init__.py
index e69de29bb2..2b59ef5ca6 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -0,0 +1,16 @@
+from enum import Enum
+
+
+class Sample:
+    def __init__(self, path: str):
+        self.path = path
+
+
+class WindowsSamples(Enum):
+    WINDOWSXP_GENERIC = Sample("./test_images/win-xp-laptop-2005-06-25.img")
+    """WindowsXP sample from early Volatility training."""
+
+
+class LinuxSamples(Enum):
+    LINUX_GENERIC = Sample("./test_images/linux-sample-1.bin")
+    """Linux Debian 3.2.0-4 sample from early Volatility training."""
diff --git a/test/plugins/linux/__init__.py b/test/plugins/linux/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py
new file mode 100644
index 0000000000..8ec485735b
--- /dev/null
+++ b/test/plugins/linux/linux.py
@@ -0,0 +1,617 @@
+import contextlib
+import tempfile
+import os
+import re
+from test import test_volatility, LinuxSamples
+
+
+class TestLinuxVolshell:
+    def test_linux_volshell(self, image, volatility, python):
+        out = test_volatility.basic_volshell_test(
+            image, volatility, python, globalargs=("-l",)
+        )
+        assert out.count(b"<task_struct") > 100
+
+
+class TestLinuxPslist:
+    def test_linux_generic_pslist(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.pslist.PsList", image, volatility, python
+        )
+
+        assert rc == 0
+        out = out.lower()
+        assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
+        assert out.find(b"watchdog") != -1
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxCheckIdt:
+    def test_linux_generic_check_idt(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.check_idt.Check_idt", image, volatility, python
+        )
+
+        assert rc == 0
+        out = out.lower()
+        assert out.count(b"__kernel__") >= 10
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxCheckSyscall:
+    def test_linux_generic_check_syscall(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.check_syscall.Check_syscall", image, volatility, python
+        )
+
+        assert rc == 0
+        out = out.lower()
+        assert out.find(b"sys_close") != -1
+        assert out.find(b"sys_open") != -1
+        assert out.count(b"\n") > 100
+
+
+class TestLinuxLsmod:
+    def test_linux_generic_lsmod(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.lsmod.Lsmod", image, volatility, python
+        )
+
+        assert rc == 0
+        out = out.lower()
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxLsof:
+    def test_linux_generic_lsof(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.lsof.Lsof", image, volatility, python
+        )
+
+        assert rc == 0
+        out = out.lower()
+        assert out.count(b"socket:") >= 10
+        assert out.count(b"\n") > 35
+
+
+class TestLinuxProcMaps:
+    def test_linux_generic_proc_maps(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.proc.Maps", image, volatility, python
+        )
+
+        assert rc == 0
+        out = out.lower()
+        assert out.count(b"anonymous mapping") >= 10
+        assert out.count(b"\n") > 100
+
+
+class TestLinuxTtyCheck:
+    def test_linux_generic_tty_check(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.tty_check.tty_check", image, volatility, python
+        )
+
+        assert rc == 0
+        out = out.lower()
+        assert out.find(b"__kernel__") != -1
+        assert out.count(b"\n") >= 5
+
+
+class TestLinuxSockstat:
+    def test_linux_generic_sockstat(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.sockstat.Sockstat", image, volatility, python
+        )
+
+        assert rc == 0
+        assert out.count(b"AF_UNIX") >= 354
+        assert out.count(b"AF_BLUETOOTH") >= 5
+        assert out.count(b"AF_INET") >= 32
+        assert out.count(b"AF_INET6") >= 20
+        assert out.count(b"AF_PACKET") >= 1
+        assert out.count(b"AF_NETLINK") >= 43
+
+
+class TestLinuxLibraryList:
+    def test_linux_specific_library_list(self, volatility, python):
+        image = LinuxSamples.LINUX_GENERIC.value.path
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.library_list.LibraryList",
+            image,
+            volatility,
+            python,
+            pluginargs=("--pids", "2363"),
+        )
+
+        assert rc == 0
+        assert re.search(
+            rb"NetworkManager\s2363\s0x7f52cdda0000\s/lib/x86_64-linux-gnu/libnss_files.so.2",
+            out,
+        )
+
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxPstree:
+    def test_linux_generic_pstree(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.pstree.PsTree", image, volatility, python
+        )
+
+        assert rc == 0
+        out = out.lower()
+        assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxPidhashtable:
+    def test_linux_generic_pidhashtable(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.pidhashtable.PIDHashTable", image, volatility, python
+        )
+
+        assert rc == 0
+        out = out.lower()
+        assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxBash:
+    def test_linux_bash(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.bash.Bash", image, volatility, python
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxBoottime:
+    def test_linux_generic_boottime(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.boottime.Boottime", image, volatility, python
+        )
+
+        assert rc == 0
+        out = out.lower()
+        assert out.count(b"utc") >= 1
+
+
+class TestLinuxCapabilities:
+    def test_linux_generic_capabilities(self, image, volatility, python):
+        rc, out, err = test_volatility.runvol_plugin(
+            "linux.capabilities.Capabilities",
+            image,
+            volatility,
+            python,
+            globalargs=("-vvv",),
+        )
+
+        if rc != 0 and err.count(b"Unsupported kernel capabilities implementation") > 0:
+            # The linux-sample-1.bin kernel implementation isn't supported.
+            # However, we can still check that the plugin requirements are met.
+            return None
+
+        assert rc == 0
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxCheckCreds:
+    def test_linux_generic_check_creds(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.check_creds.Check_creds", image, volatility, python
+        )
+
+        # linux-sample-1.bin has no processes sharing credentials.
+        # This validates that plugin requirements are met and exceptions are not raised.
+        assert rc == 0
+        assert out.count(b"\n") >= 4
+
+
+class TestLinuxElfs:
+    def test_linux_generic_elfs(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.elfs.Elfs", image, volatility, python
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxEnvars:
+    def test_linux_generic_envars(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.envars.Envars", image, volatility, python
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxKthreads:
+    def test_linux_generic_kthreads(self, image, volatility, python):
+        rc, out, err = test_volatility.runvol_plugin(
+            "linux.kthreads.Kthreads",
+            image,
+            volatility,
+            python,
+            globalargs=("-vvv",),
+        )
+
+        if rc != 0 and err.count(b"Unsupported kthread implementation") > 0:
+            # The linux-sample-1.bin kernel implementation isn't supported.
+            # However, we can still check that the plugin requirements are met.
+            return None
+
+        assert rc == 0
+        assert out.count(b"\n") >= 4
+
+
+class TestLinuxMalfind:
+    def test_linux_generic_malfind(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.malfind.Malfind", image, volatility, python
+        )
+
+        # linux-sample-1.bin has no process memory ranges with potential injected code.
+        # This validates that plugin requirements are met and exceptions are not raised.
+        assert rc == 0
+        assert out.count(b"\n") >= 4
+
+
+class TestLinuxMountinfo:
+    def test_linux_generic_mountinfo(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.mountinfo.MountInfo", image, volatility, python
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxPsaux:
+    def test_linux_generic_psaux(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.psaux.PsAux", image, volatility, python
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 50
+
+
+class TestLinuxPtrace:
+    def test_linux_generic_ptrace(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.ptrace.Ptrace", image, volatility, python
+        )
+
+        # linux-sample-1.bin has no processes being ptraced.
+        # This validates that plugin requirements are met and exceptions are not raised.
+        assert rc == 0
+        assert out.count(b"\n") >= 4
+
+
+class TestLinuxVmaregexscan:
+    def test_linux_generic_vmaregexscan(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.vmaregexscan.VmaRegExScan",
+            image,
+            volatility,
+            python,
+            pluginargs=("--pid", "1", "--pattern", "\\x7fELF"),
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxVmayarascanYaraRule:
+    def test_linux_specific_vmayarascan_yara_rule(self, volatility, python):
+        image = LinuxSamples.LINUX_GENERIC.value.path
+        yara_rule_01 = r"""
+            rule fullvmayarascan
+            {
+                strings:
+                    $s1 = "_nss_files_parse_grent"
+                    $s2 = "/lib64/ld-linux-x86-64.so.2"
+                    $s3 = "(bufferend - (char *) 0) % sizeof (char *) == 0"
+                condition:
+                    all of them
+            }
+        """
+
+        # FIXME: When the minimum Python version includes 3.12, replace the following with:
+        # with tempfile.NamedTemporaryFile(delete_on_close=False) as fd: ...
+        fd, filename = tempfile.mkstemp(suffix=".yar")
+        try:
+            with os.fdopen(fd, "w") as f:
+                f.write(yara_rule_01)
+
+            rc, out, _err = test_volatility.runvol_plugin(
+                "linux.vmayarascan.VmaYaraScan",
+                image,
+                volatility,
+                python,
+                pluginargs=("--pid", "8600", "--yara-file", filename),
+            )
+        finally:
+            with contextlib.suppress(FileNotFoundError):
+                os.remove(filename)
+
+        assert rc == 0
+        assert out.count(b"\n") > 4
+
+
+class TestLinuxVmayarascanYaraString:
+    def test_linux_generic_vmayarascan_yara_string(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.vmayarascan.VmaYaraScan",
+            image,
+            volatility,
+            python,
+            pluginargs=("--pid", "1", "--yara-string", "ELF"),
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 10
+
+
+class TestLinuxPageCacheFiles:
+    def test_linux_specific_page_cache_files(self, volatility, python):
+        image = LinuxSamples.LINUX_GENERIC.value.path
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.pagecache.Files",
+            image,
+            volatility,
+            python,
+            pluginargs=("--find", "/etc/passwd"),
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 4
+
+        # inode_num inode_addr ... file_path
+        assert re.search(
+            rb"146829\s0x88001ab5c270.*?/etc/passwd",
+            out,
+        )
+
+
+class TestLinuxPageCacheInodepages:
+    def test_linux_specific_page_cache_inodepages(self, volatility, python):
+        image = LinuxSamples.LINUX_GENERIC.value.path
+        inode_address = hex(0x88001AB5C270)
+        inode_dump_filename = f"inode_{inode_address}.dmp"
+
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.pagecache.InodePages",
+            image,
+            volatility,
+            python,
+            pluginargs=("--inode", inode_address),
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 4
+
+        # PageVAddr PagePAddr MappingAddr .. DumpSafe
+        assert re.search(
+            rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True",
+            out,
+        )
+
+        try:
+            rc, out, _err = test_volatility.runvol_plugin(
+                "linux.pagecache.InodePages",
+                image,
+                volatility,
+                python,
+                pluginargs=("--inode", inode_address, "--dump"),
+            )
+
+            assert rc == 0
+            assert out.count(b"\n") >= 4
+
+            assert os.path.exists(inode_dump_filename)
+            with open(inode_dump_filename, "rb") as fp:
+                inode_contents = fp.read()
+            assert inode_contents.count(b"\n") > 30
+            assert inode_contents.count(b"root:x:0:0:root:/root:/bin/bash") > 0
+        finally:
+            with contextlib.suppress(FileNotFoundError):
+                os.remove(inode_dump_filename)
+
+
+class TestLinuxCheckAfinfo:
+    def test_linux_generic_check_afinfo(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.check_afinfo.Check_afinfo", image, volatility, python
+        )
+
+        # linux-sample-1.bin has no suspicious results.
+        # This validates that plugin requirements are met and exceptions are not raised.
+        assert rc == 0
+        assert out.count(b"\n") >= 4
+
+
+class TestLinuxCheckModules:
+    def test_linux_generic_check_modules(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.check_modules.Check_modules", image, volatility, python
+        )
+
+        # linux-sample-1.bin has no suspicious results.
+        # This validates that plugin requirements are met and exceptions are not raised.
+        assert rc == 0
+        assert out.count(b"\n") >= 4
+
+
+class TestLinuxEbpf:
+    def test_linux_generic_ebpf_progs(self, image, volatility, python):
+        rc, out, err = test_volatility.runvol_plugin(
+            "linux.ebpf.EBPF",
+            image,
+            volatility,
+            python,
+            globalargs=("-vvv",),
+        )
+
+        if rc != 0 and err.count(b"Unsupported kernel") > 0:
+            # The linux-sample-1.bin kernel implementation isn't supported.
+            # However, we can still check that the plugin requirements are met.
+            return None
+
+        assert rc == 0
+        assert out.count(b"\n") > 4
+
+
+class TestLinuxIomem:
+    def test_linux_generic_iomem(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.iomem.IOMem", image, volatility, python
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 100
+
+
+class TestLinuxKeyboardNotifiers:
+    def test_linux_generic_keyboard_notifiers(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.keyboard_notifiers.Keyboard_notifiers", image, volatility, python
+        )
+
+        # linux-sample-1.bin has no suspicious results for this plugin.
+        # This validates that plugin requirements are met and exceptions are not raised.
+        assert rc == 0
+        assert out.count(b"\n") >= 4
+
+
+class TestLinuxKmesg:
+    def test_linux_generic_kmesg(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.kmsg.Kmsg", image, volatility, python
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 100
+
+
+class TestLinuxNetfilter:
+    def test_linux_generic_netfilter(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.netfilter.Netfilter", image, volatility, python
+        )
+
+        # linux-sample-1.bin has no suspicious results for this plugin.
+        # This validates that plugin requirements are met and exceptions are not raised.
+        assert rc == 0
+        assert out.count(b"\n") >= 4
+
+
+class TestLinuxPsscan:
+    def test_linux_generic__psscan(self, image, volatility, python):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.psscan.PsScan", image, volatility, python
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 100
+
+
+class TestLinuxHiddenModules:
+    def test_linux_specific_hidden_modules(self, volatility, python):
+        # TODO: this check should be specific, against a distinct infected sample
+        image = LinuxSamples.LINUX_GENERIC.value.path
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.hidden_modules.Hidden_modules", image, volatility, python
+        )
+
+        # linux-sample-1.bin has no hidden modules.
+        # This validates that plugin requirements are met and exceptions are not raised.
+        assert rc == 0
+        assert out.count(b"\n") >= 4
+
+
+class TestLinuxIpAddr:
+    def test_linux_specific_ip_addr(self, volatility, python):
+        image = LinuxSamples.LINUX_GENERIC.value.path
+        rc, out, err = test_volatility.runvol_plugin(
+            "linux.ip.Addr", image, volatility, python
+        )
+
+        assert re.search(
+            rb"2\s+eth0\s+00:0c:29:8f:ed:ca\s+False\s+192.168.201.161\s+24\s+global\s+UP",
+            out,
+        )
+        assert re.search(
+            rb"2\s+eth0\s+00:0c:29:8f:ed:ca\s+False\s+fe80::20c:29ff:fe8f:edca\s+64\s+link\s+UP",
+            out,
+        )
+        assert out.count(b"\n") >= 8
+        assert rc == 0
+
+
+class TestLinuxIpLink:
+    def test_linux_specific_ip_link(self, volatility, python):
+        image = LinuxSamples.LINUX_GENERIC.value.path
+        rc, out, err = test_volatility.runvol_plugin(
+            "linux.ip.Link", image, volatility, python
+        )
+
+        assert re.search(
+            rb"-\s+lo\s+00:00:00:00:00:00\s+UNKNOWN\s+16436\s+noqueue\s+0\s+LOOPBACK,LOWER_UP,UP",
+            out,
+        )
+        assert re.search(
+            rb"-\s+eth0\s+00:0c:29:8f:ed:ca\s+UP\s+1500\s+pfifo_fast\s+1000\s+BROADCAST,LOWER_UP,MULTICAST,UP",
+            out,
+        )
+        assert out.count(b"\n") >= 6
+        assert rc == 0
+
+
+class TestLinuxKallsyms:
+    def test_linux_specific_kallsyms(self, volatility, python):
+        image = LinuxSamples.LINUX_GENERIC.value.path
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.kallsyms.Kallsyms",
+            image,
+            volatility,
+            python,
+            pluginargs=("--modules",),
+        )
+        # linux-sample-1.bin has no hidden modules.
+        # This validates that plugin requirements are met and exceptions are not raised.
+        assert rc == 0
+        assert out.count(b"\n") > 1000
+
+        # Addr	Type	Size	Exported	SubSystem	ModuleName	SymbolName	Description
+        # 0xffffa009eba9	t	28	False	module	usbcore	usb_mon_register	Symbol is in the text (code) section
+        assert re.search(
+            rb"0xffffa009eba9\s+t\s+28\s+False\s+module\s+usbcore\s+usb_mon_register\s+Symbol is in the text \(code\) section",
+            out,
+        )
+
+
+class TestLinuxPscallstack:
+    def test_linux_specific_pscallstack(self, volatility, python):
+        image = LinuxSamples.LINUX_GENERIC.value.path
+        rc, out, _err = test_volatility.runvol_plugin(
+            "linux.pscallstack.PsCallStack",
+            image,
+            volatility,
+            python,
+            pluginargs=("--pid", "1"),
+        )
+
+        assert rc == 0
+        assert out.count(b"\n") > 30
+
+        # TID     Comm    Position        Address Value   Name    Type    Module
+        # 1       init    39      0x88001f999a40  0xffff81109039  do_select       T       kernel
+        assert re.search(
+            rb"1\s+init\s+39\s+0x88001f999a40.*?0xffff81109039\s+do_select\s+T\s+kernel",
+            out,
+        )
diff --git a/test/plugins/windows/windows.py b/test/plugins/windows/windows.py
new file mode 100644
index 0000000000..ce05af0cdf
--- /dev/null
+++ b/test/plugins/windows/windows.py
@@ -0,0 +1,326 @@
+import json
+import hashlib
+import shutil
+import contextlib
+import tempfile
+import os
+from test import test_volatility, WindowsSamples
+
+
+class TestWindowsVolshell:
+    def test_windows_volshell(self, image, volatility, python):
+        out = test_volatility.basic_volshell_test(
+            image, volatility, python, globalargs=("-w",)
+        )
+        assert out.count(b"<EPROCESS") > 40
+
+
+class TestWindowsPslist:
+    def test_windows_generic_pslist(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.pslist.PsList",
+            image,
+            volatility,
+            python,
+            # Notice that this is needed to hit lru_cache when "specific" will run
+            globalargs=("-r", "json"),
+        )
+        assert rc == 0
+        out = out.lower()
+        assert out.find(b"system") != -1
+        assert out.find(b"csrss.exe") != -1
+        assert out.find(b"svchost.exe") != -1
+        assert out.count(b"\n") > 10
+
+    def test_windows_specific_pslist(self, volatility, python):
+        image = WindowsSamples.WINDOWSXP_GENERIC.value.path
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.pslist.PsList",
+            image,
+            volatility,
+            python,
+            globalargs=("-r", "json"),
+        )
+        assert rc == 0
+        expected_row = {
+            "CreateTime": None,
+            "ExitTime": None,
+            "File output": "Disabled",
+            "Handles": 1140,
+            "ImageFileName": "System",
+            "Offset(V)": 2185004992,
+            "PID": 4,
+            "PPID": 0,
+            "SessionId": None,
+            "Threads": 61,
+            "Wow64": False,
+            "__children": [],
+        }
+        assert test_volatility.match_output_row(expected_row, json.loads(out))
+
+
+class TestWindowsPsscan:
+    def test_windows_generic_psscan(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.psscan.PsScan", image, volatility, python
+        )
+        assert rc == 0
+        out = out.lower()
+        assert out.find(b"system") != -1
+        assert out.find(b"csrss.exe") != -1
+        assert out.find(b"svchost.exe") != -1
+        assert out.count(b"\n") > 10
+
+
+class TestWindowsDlllist:
+    def test_windows_generic_dlllist(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.dlllist.DllList", image, volatility, python
+        )
+        assert rc == 0
+        out = out.lower()
+        assert out.count(b"\n") > 10
+
+
+class TestWindowsModules:
+    def test_windows_generic_modules(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.modules.Modules", image, volatility, python
+        )
+        assert rc == 0
+        out = out.lower()
+        assert out.count(b"\n") > 10
+
+
+class TestWindowsHivelist:
+    def test_windows_generic_hivelist(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.registry.hivelist.HiveList", image, volatility, python
+        )
+        assert rc == 0
+        out = out.lower()
+
+        not_xp = out.find(b"\\systemroot\\system32\\config\\software")
+        if not_xp == -1:
+            assert (
+                out.find(
+                    b"\\device\\harddiskvolume1\\windows\\system32\\config\\software"
+                )
+                != -1
+            )
+        assert out.count(b"\n") > 10
+
+
+class TestWindowsDumpfiles:
+    def test_windows_specific_dumpfiles(self, volatility, python):
+        image = WindowsSamples.WINDOWSXP_GENERIC.value.path
+        with open("./test/known_files.json") as json_file:
+            known_files = json.load(json_file)
+
+        failed_chksms = 0
+        file_name = os.path.basename(image)
+
+        try:
+            for addr in known_files["windows_dumpfiles"][file_name]:
+                path = tempfile.mkdtemp()
+
+                rc, _out, _err = test_volatility.runvol_plugin(
+                    "windows.dumpfiles.DumpFiles",
+                    image,
+                    volatility,
+                    python,
+                    globalargs=("-o", path),
+                    pluginargs=("--virtaddr", addr),
+                )
+
+                for file in os.listdir(path):
+                    with open(os.path.join(path, file), "rb") as fp:
+                        if (
+                            hashlib.md5(fp.read()).hexdigest()
+                            not in known_files["windows_dumpfiles"][file_name][addr]
+                        ):
+                            failed_chksms += 1
+
+                shutil.rmtree(path)
+            json_file.close()
+
+            assert failed_chksms == 0
+            assert rc == 0
+        except Exception as e:
+            json_file.close()
+            print("Key Error raised on " + str(e))
+            assert False
+
+
+class TestWindowsHandles:
+    def test_windows_generic_handles(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.handles.Handles",
+            image,
+            volatility,
+            python,
+            pluginargs=("--pid", "4"),
+        )
+        assert rc == 0
+        assert out.find(b"System Pid 4") != -1
+        assert (
+            out.find(
+                b"MACHINE\\SYSTEM\\CONTROLSET001\\CONTROL\\SESSION MANAGER\\MEMORY MANAGEMENT\\PREFETCHPARAMETERS"
+            )
+            != -1
+        )
+        assert out.find(b"MACHINE\\SYSTEM\\SETUP") != -1
+        assert out.count(b"\n") > 500
+
+
+class TestWindowsSvcscan:
+    def test_windows_generic_svcscan(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.svcscan.SvcScan", image, volatility, python
+        )
+        assert rc == 0
+        assert out.find(b"Microsoft ACPI Driver") != -1
+        assert out.count(b"\n") > 250
+
+
+class TestWindowsThrdscan:
+    def test_windows_generic_thrdscan(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.thrdscan.ThrdScan", image, volatility, python
+        )
+        assert rc == 0
+        assert out.find(b"\t4\t8") != -1
+        assert out.find(b"\t4\t12") != -1
+        assert out.find(b"\t4\t16") != -1
+
+
+class TestWindowsPrivileges:
+    def test_windows_generic_privileges(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.privileges.Privs",
+            image,
+            volatility,
+            python,
+            pluginargs=("--pid", "4"),
+        )
+        assert rc == 0
+        assert out.find(b"SeCreateTokenPrivilege") != -1
+        assert out.find(b"SeCreateGlobalPrivilege") != -1
+        assert out.find(b"SeAssignPrimaryTokenPrivilege") != -1
+        assert out.count(b"\n") > 20
+
+
+class TestWindowsGetsids:
+    def test_windows_generic_getsids(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.getsids.GetSIDs",
+            image,
+            volatility,
+            python,
+            pluginargs=("--pid", "4"),
+        )
+        assert rc == 0
+        assert out.find(b"Local System") != -1
+        assert out.find(b"Administrators") != -1
+        assert out.find(b"Everyone") != -1
+        assert out.find(b"Authenticated Users") != -1
+
+
+class TestWindowsEnvars:
+    def test_windows_generic_envars(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.envars.Envars", image, volatility, python
+        )
+        assert rc == 0
+        assert out.find(b"PATH") != -1
+        assert out.find(b"PROCESSOR_ARCHITECTURE") != -1
+        assert out.find(b"USERNAME") != -1
+        assert out.find(b"SystemRoot") != -1
+        assert out.find(b"CommonProgramFiles") != -1
+        assert out.count(b"\n") > 500
+
+
+class TestWindowsCallbacks:
+    def test_windows_generic_callbacks(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.callbacks.Callbacks", image, volatility, python
+        )
+        assert rc == 0
+        assert out.find(b"PspCreateProcessNotifyRoutine") != -1
+        assert out.find(b"KeBugCheckCallbackListHead") != -1
+        assert out.find(b"KeBugCheckReasonCallbackListHead") != -1
+        assert out.count(b"KeBugCheckReasonCallbackListHead	") > 5
+
+
+class TestWindowsVadwalk:
+    def test_windows_generic_vadwalk(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.vadwalk.VadWalk", image, volatility, python
+        )
+        assert rc == 0
+        assert out.find(b"Vad") != -1
+        assert out.find(b"VadS") != -1
+        assert out.find(b"Vadl") != -1
+        assert out.find(b"VadF") != -1
+        assert out.find(b"0x0") != -1
+
+
+class TestWindowsDevicetree:
+    def test_windows_generic_devicetree(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.devicetree.DeviceTree", image, volatility, python
+        )
+        assert rc == 0
+        assert out.find(b"DEV") != -1
+        assert out.find(b"DRV") != -1
+        assert out.find(b"ATT") != -1
+        assert out.find(b"FILE_DEVICE_CONTROLLER") != -1
+        assert out.find(b"FILE_DEVICE_DISK") != -1
+        assert out.find(b"FILE_DEVICE_DISK_FILE_SYSTEM") != -1
+
+
+class TestWindowsVadyarascan:
+    def test_windows_specific_vadyarascan_yara_rule(self, volatility, python):
+        image = WindowsSamples.WINDOWSXP_GENERIC.value.path
+        yara_rule_01 = r"""
+            rule fullvadyarascan
+            {
+                strings:
+                    $s1 = "!This program cannot be run in DOS mode."
+                    $s2 = "Qw))Pw"
+                    $s3 = "W_wD)Pw"
+                    $s4 = "1Xw+2Xw"
+                    $s5 = "xd`wh``w"
+                    $s6 = "0g`w0g`w8g`w8g`w@g`w@g`wHg`wHg`wPg`wPg`wXg`wXg`w`g`w`g`whg`whg`wpg`wpg`wxg`wxg`w"
+                condition:
+                    all of them
+            }
+        """
+        fd, filename = tempfile.mkstemp(suffix=".yar")
+        try:
+            with os.fdopen(fd, "w") as f:
+                f.write(yara_rule_01)
+            rc, out, _err = test_volatility.runvol_plugin(
+                "windows.vadyarascan.VadYaraScan",
+                image,
+                volatility,
+                python,
+                pluginargs=("--pid", "4012", "--yara-file", filename),
+            )
+        finally:
+            with contextlib.suppress(FileNotFoundError):
+                os.remove(filename)
+        assert rc == 0
+        assert out.count(b"\n") > 4
+
+    def test_windows_specific_vadyarascan_yara_string(self, volatility, python):
+        image = WindowsSamples.WINDOWSXP_GENERIC.value.path
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.vadyarascan.VadYaraScan",
+            image,
+            volatility,
+            python,
+            pluginargs=("--pid", "4012", "--yara-string", "MZ"),
+        )
+        assert rc == 0
+        assert out.count(b"\n") > 10
diff --git a/test/test_volatility.py b/test/test_volatility.py
index 07cab2c95b..632a6bcde9 100644
--- a/test/test_volatility.py
+++ b/test/test_volatility.py
@@ -6,25 +6,24 @@
 #
 
 import os
-import re
 import subprocess
 import sys
-import shutil
 import tempfile
-import hashlib
-import json
 import contextlib
+import functools
+from typing import List, Tuple
 
 #
 # HELPER FUNCTIONS
 #
 
 
+@functools.lru_cache
 def runvol(args, volatility, python):
     volpy = volatility
     python_cmd = python
 
-    cmd = [python_cmd, volpy] + args
+    cmd = (python_cmd, volpy) + args
     print(" ".join(cmd))
     p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
     stdout, stderr = p.communicate()
@@ -38,17 +37,18 @@ def runvol(args, volatility, python):
     return p.returncode, stdout, stderr
 
 
-def runvol_plugin(plugin, img, volatility, python, pluginargs=None, globalargs=None):
-    pluginargs = pluginargs or []
-    globalargs = globalargs or []
+@functools.lru_cache
+def runvol_plugin(
+    plugin, img, volatility, python, pluginargs: Tuple = (), globalargs: Tuple = ()
+):
     args = (
         globalargs
-        + [
+        + (
             "--single-location",
             img,
             "-q",
             plugin,
-        ]
+        )
         + pluginargs
     )
 
@@ -60,17 +60,43 @@ def runvolshell(img, volshell, python, volshellargs=None, globalargs=None):
     globalargs = globalargs or []
     args = (
         globalargs
-        + [
+        + (
             "--single-location",
             img,
             "-q",
-        ]
+        )
         + volshellargs
     )
 
     return runvol(args, volshell, python)
 
 
+def match_output_row(
+    expected_row: dict, plugin_json_out: List[dict], exact_match: bool = False
+):
+    """Search each row of a plugin's JSON output for an expected row. Each row is a dict.
+
+        Args:
+            expected_row: The expected row to be found in the output
+            plugin_json_out: The plugin's output in JSON format (typically obtained through -r json and json.loads)
+            exact_match: Whether to require exactly the expected row, no more no less, or to anticipate columns' addition by checking only
+    the expected row keys and values
+    """
+
+    if not exact_match:
+        for row in plugin_json_out:
+            if all(
+                expected_item in row.items() for expected_item in expected_row.items()
+            ):
+                return True
+    else:
+        for row in plugin_json_out:
+            if expected_row == row:
+                return True
+
+    return False
+
+
 #
 # TESTS
 #
@@ -96,7 +122,7 @@ def basic_volshell_test(image, volatility, python, globalargs):
             img=image,
             volshell=volatility,
             python=python,
-            volshellargs=["--script", filename],
+            volshellargs=("--script", filename),
             globalargs=globalargs,
         )
     finally:
@@ -109,806 +135,8 @@ def basic_volshell_test(image, volatility, python, globalargs):
     return out
 
 
-# WINDOWS
-
-
-def test_windows_volshell(image, volatility, python):
-    out = basic_volshell_test(image, volatility, python, globalargs=["-w"])
-    assert out.count(b"<EPROCESS") > 40
-
-
-def test_windows_pslist(image, volatility, python):
-    rc, out, _err = runvol_plugin("windows.pslist.PsList", image, volatility, python)
-    out = out.lower()
-    assert out.find(b"system") != -1
-    assert out.find(b"csrss.exe") != -1
-    assert out.find(b"svchost.exe") != -1
-    assert out.count(b"\n") > 10
-    assert rc == 0
-
-    rc, out, _err = runvol_plugin(
-        "windows.pslist.PsList", image, volatility, python, pluginargs=["--pid", "4"]
-    )
-    out = out.lower()
-    assert out.find(b"system") != -1
-    assert out.count(b"\n") < 10
-    assert rc == 0
-
-
-def test_windows_psscan(image, volatility, python):
-    rc, out, _err = runvol_plugin("windows.psscan.PsScan", image, volatility, python)
-    out = out.lower()
-    assert out.find(b"system") != -1
-    assert out.find(b"csrss.exe") != -1
-    assert out.find(b"svchost.exe") != -1
-    assert out.count(b"\n") > 10
-    assert rc == 0
-
-
-def test_windows_dlllist(image, volatility, python):
-    rc, out, _err = runvol_plugin("windows.dlllist.DllList", image, volatility, python)
-    out = out.lower()
-    assert out.count(b"\n") > 10
-    assert rc == 0
-
-
-def test_windows_modules(image, volatility, python):
-    rc, out, _err = runvol_plugin("windows.modules.Modules", image, volatility, python)
-    out = out.lower()
-    assert out.count(b"\n") > 10
-    assert rc == 0
-
-
-def test_windows_hivelist(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "windows.registry.hivelist.HiveList", image, volatility, python
-    )
-    out = out.lower()
-
-    not_xp = out.find(b"\\systemroot\\system32\\config\\software")
-    if not_xp == -1:
-        assert (
-            out.find(b"\\device\\harddiskvolume1\\windows\\system32\\config\\software")
-            != -1
-        )
-
-    assert out.count(b"\n") > 10
-    assert rc == 0
-
-
-def test_windows_dumpfiles(image, volatility, python):
-
-    with open("./test/known_files.json") as json_file:
-        known_files = json.load(json_file)
-
-    failed_chksms = 0
-    file_name = os.path.basename(image)
-
-    try:
-        for addr in known_files["windows_dumpfiles"][file_name]:
-
-            path = tempfile.mkdtemp()
-
-            rc, _out, _err = runvol_plugin(
-                "windows.dumpfiles.DumpFiles",
-                image,
-                volatility,
-                python,
-                globalargs=["-o", path],
-                pluginargs=["--virtaddr", addr],
-            )
-
-            for file in os.listdir(path):
-                with open(os.path.join(path, file), "rb") as fp:
-                    if (
-                        hashlib.md5(fp.read()).hexdigest()
-                        not in known_files["windows_dumpfiles"][file_name][addr]
-                    ):
-                        failed_chksms += 1
-
-            shutil.rmtree(path)
-
-        json_file.close()
-
-        assert failed_chksms == 0
-        assert rc == 0
-    except Exception as e:
-        json_file.close()
-        print("Key Error raised on " + str(e))
-        assert False
-
-
-def test_windows_handles(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "windows.handles.Handles", image, volatility, python, pluginargs=["--pid", "4"]
-    )
-
-    assert out.find(b"System Pid 4") != -1
-    assert (
-        out.find(
-            b"MACHINE\\SYSTEM\\CONTROLSET001\\CONTROL\\SESSION MANAGER\\MEMORY MANAGEMENT\\PREFETCHPARAMETERS"
-        )
-        != -1
-    )
-    assert out.find(b"MACHINE\\SYSTEM\\SETUP") != -1
-    assert out.count(b"\n") > 500
-    assert rc == 0
-
-
-def test_windows_svcscan(image, volatility, python):
-    rc, out, _err = runvol_plugin("windows.svcscan.SvcScan", image, volatility, python)
-
-    assert out.find(b"Microsoft ACPI Driver") != -1
-    assert out.count(b"\n") > 250
-    assert rc == 0
-
-
-def test_windows_thrdscan(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "windows.thrdscan.ThrdScan", image, volatility, python
-    )
-    # find pid 4 (of system process) which starts with lowest tids
-    assert out.find(b"\t4\t8") != -1
-    assert out.find(b"\t4\t12") != -1
-    assert out.find(b"\t4\t16") != -1
-    # assert out.find(b"this raieses AssertionError") != -1
-    assert rc == 0
-
-
-def test_windows_privileges(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "windows.privileges.Privs", image, volatility, python, pluginargs=["--pid", "4"]
-    )
-
-    assert out.find(b"SeCreateTokenPrivilege") != -1
-    assert out.find(b"SeCreateGlobalPrivilege") != -1
-    assert out.find(b"SeAssignPrimaryTokenPrivilege") != -1
-    assert out.count(b"\n") > 20
-    assert rc == 0
-
-
-def test_windows_getsids(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "windows.getsids.GetSIDs", image, volatility, python, pluginargs=["--pid", "4"]
-    )
-
-    assert out.find(b"Local System") != -1
-    assert out.find(b"Administrators") != -1
-    assert out.find(b"Everyone") != -1
-    assert out.find(b"Authenticated Users") != -1
-    assert rc == 0
-
-
-def test_windows_envars(image, volatility, python):
-    rc, out, _err = runvol_plugin("windows.envars.Envars", image, volatility, python)
-
-    assert out.find(b"PATH") != -1
-    assert out.find(b"PROCESSOR_ARCHITECTURE") != -1
-    assert out.find(b"USERNAME") != -1
-    assert out.find(b"SystemRoot") != -1
-    assert out.find(b"CommonProgramFiles") != -1
-    assert out.count(b"\n") > 500
-    assert rc == 0
-
-
-def test_windows_callbacks(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "windows.callbacks.Callbacks", image, volatility, python
-    )
-
-    assert out.find(b"PspCreateProcessNotifyRoutine") != -1
-    assert out.find(b"KeBugCheckCallbackListHead") != -1
-    assert out.find(b"KeBugCheckReasonCallbackListHead") != -1
-    assert out.count(b"KeBugCheckReasonCallbackListHead	") > 5
-    assert rc == 0
-
-
-def test_windows_vadwalk(image, volatility, python):
-    rc, out, _err = runvol_plugin("windows.vadwalk.VadWalk", image, volatility, python)
-
-    assert out.find(b"Vad") != -1
-    assert out.find(b"VadS") != -1
-    assert out.find(b"Vadl") != -1
-    assert out.find(b"VadF") != -1
-    assert out.find(b"0x0") != -1
-    assert rc == 0
-
-
-def test_windows_devicetree(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "windows.devicetree.DeviceTree", image, volatility, python
-    )
-
-    assert out.find(b"DEV") != -1
-    assert out.find(b"DRV") != -1
-    assert out.find(b"ATT") != -1
-    assert out.find(b"FILE_DEVICE_CONTROLLER") != -1
-    assert out.find(b"FILE_DEVICE_DISK") != -1
-    assert out.find(b"FILE_DEVICE_DISK_FILE_SYSTEM") != -1
-    assert rc == 0
-
-
-def test_windows_vadyarascan_yara_rule(image, volatility, python):
-    yara_rule_01 = r"""
-        rule fullvadyarascan
-        {
-            strings:
-                $s1 = "!This program cannot be run in DOS mode."
-                $s2 = "Qw))Pw"
-                $s3 = "W_wD)Pw"
-                $s4 = "1Xw+2Xw"
-                $s5 = "xd`wh``w"
-                $s6 = "0g`w0g`w8g`w8g`w@g`w@g`wHg`wHg`wPg`wPg`wXg`wXg`w`g`w`g`whg`whg`wpg`wpg`wxg`wxg`w"
-            condition:
-                all of them
-        }
-    """
-
-    # FIXME: When the minimum Python version includes 3.12, replace the following with:
-    # with tempfile.NamedTemporaryFile(delete_on_close=False) as fd: ...
-    fd, filename = tempfile.mkstemp(suffix=".yar")
-    try:
-        with os.fdopen(fd, "w") as f:
-            f.write(yara_rule_01)
-
-        rc, out, _err = runvol_plugin(
-            "windows.vadyarascan.VadYaraScan",
-            image,
-            volatility,
-            python,
-            pluginargs=["--pid", "4012", "--yara-file", filename],
-        )
-    finally:
-        with contextlib.suppress(FileNotFoundError):
-            os.remove(filename)
-
-    out = out.lower()
-    assert out.count(b"\n") > 4
-    assert rc == 0
-
-
-def test_windows_vadyarascan_yara_string(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "windows.vadyarascan.VadYaraScan",
-        image,
-        volatility,
-        python,
-        pluginargs=["--pid", "4012", "--yara-string", "MZ"],
-    )
-    out = out.lower()
-
-    assert out.count(b"\n") > 10
-    assert rc == 0
-
-
-# LINUX
-
-
-def test_linux_volshell(image, volatility, python):
-    out = basic_volshell_test(image, volatility, python, globalargs=["-l"])
-    assert out.count(b"<task_struct") > 100
-
-
-def test_linux_pslist(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.pslist.PsList", image, volatility, python)
-
-    assert rc == 0
-    out = out.lower()
-    assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
-    assert out.find(b"watchdog") != -1
-    assert out.count(b"\n") > 10
-
-
-def test_linux_check_idt(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.check_idt.Check_idt", image, volatility, python
-    )
-
-    assert rc == 0
-    out = out.lower()
-    assert out.count(b"__kernel__") >= 10
-    assert out.count(b"\n") > 10
-
-
-def test_linux_check_syscall(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.check_syscall.Check_syscall", image, volatility, python
-    )
-
-    assert rc == 0
-    out = out.lower()
-    assert out.find(b"sys_close") != -1
-    assert out.find(b"sys_open") != -1
-    assert out.count(b"\n") > 100
-
-
-def test_linux_lsmod(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.lsmod.Lsmod", image, volatility, python)
-
-    assert rc == 0
-    out = out.lower()
-    assert out.count(b"\n") > 10
-
-
-def test_linux_lsof(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.lsof.Lsof", image, volatility, python)
-
-    assert rc == 0
-    out = out.lower()
-    assert out.count(b"socket:") >= 10
-    assert out.count(b"\n") > 35
-
-
-def test_linux_proc_maps(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.proc.Maps", image, volatility, python)
-
-    assert rc == 0
-    out = out.lower()
-    assert out.count(b"anonymous mapping") >= 10
-    assert out.count(b"\n") > 100
-
-
-def test_linux_tty_check(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.tty_check.tty_check", image, volatility, python
-    )
-
-    assert rc == 0
-    out = out.lower()
-    assert out.find(b"__kernel__") != -1
-    assert out.count(b"\n") >= 5
-
-
-def test_linux_sockstat(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.sockstat.Sockstat", image, volatility, python)
-
-    assert rc == 0
-    assert out.count(b"AF_UNIX") >= 354
-    assert out.count(b"AF_BLUETOOTH") >= 5
-    assert out.count(b"AF_INET") >= 32
-    assert out.count(b"AF_INET6") >= 20
-    assert out.count(b"AF_PACKET") >= 1
-    assert out.count(b"AF_NETLINK") >= 43
-
-
-def test_linux_library_list(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.library_list.LibraryList",
-        image,
-        volatility,
-        python,
-        pluginargs=["--pids", "2363"],
-    )
-
-    assert rc == 0
-    assert re.search(
-        rb"NetworkManager\s2363\s0x7f52cdda0000\s/lib/x86_64-linux-gnu/libnss_files.so.2",
-        out,
-    )
-
-    assert out.count(b"\n") > 10
-
-
-def test_linux_pstree(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.pstree.PsTree", image, volatility, python)
-
-    assert rc == 0
-    out = out.lower()
-    assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
-    assert out.count(b"\n") > 10
-
-
-def test_linux_pidhashtable(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.pidhashtable.PIDHashTable", image, volatility, python
-    )
-
-    assert rc == 0
-    out = out.lower()
-    assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
-    assert out.count(b"\n") > 10
-
-
-def test_linux_bash(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.bash.Bash", image, volatility, python)
-
-    assert rc == 0
-    assert out.count(b"\n") > 10
-
-
-def test_linux_boottime(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.boottime.Boottime", image, volatility, python)
-
-    assert rc == 0
-    out = out.lower()
-    assert out.count(b"utc") >= 1
-
-
-def test_linux_capabilities(image, volatility, python):
-    rc, out, err = runvol_plugin(
-        "linux.capabilities.Capabilities",
-        image,
-        volatility,
-        python,
-        globalargs=["-vvv"],
-    )
-
-    if rc != 0 and err.count(b"Unsupported kernel capabilities implementation") > 0:
-        # The linux-sample-1.bin kernel implementation isn't supported.
-        # However, we can still check that the plugin requirements are met.
-        return None
-
-    assert rc == 0
-    assert out.count(b"\n") > 10
-
-
-def test_linux_check_creds(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.check_creds.Check_creds", image, volatility, python
-    )
-
-    # linux-sample-1.bin has no processes sharing credentials.
-    # This validates that plugin requirements are met and exceptions are not raised.
-    assert rc == 0
-    assert out.count(b"\n") >= 4
-
-
-def test_linux_elfs(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.elfs.Elfs", image, volatility, python)
-
-    assert rc == 0
-    assert out.count(b"\n") > 10
-
-
-def test_linux_envars(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.envars.Envars", image, volatility, python)
-
-    assert rc == 0
-    assert out.count(b"\n") > 10
-
-
-def test_linux_kthreads(image, volatility, python):
-    rc, out, err = runvol_plugin(
-        "linux.kthreads.Kthreads",
-        image,
-        volatility,
-        python,
-        globalargs=["-vvv"],
-    )
-
-    if rc != 0 and err.count(b"Unsupported kthread implementation") > 0:
-        # The linux-sample-1.bin kernel implementation isn't supported.
-        # However, we can still check that the plugin requirements are met.
-        return None
-
-    assert rc == 0
-    assert out.count(b"\n") >= 4
-
-
-def test_linux_malfind(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.malfind.Malfind", image, volatility, python)
-
-    # linux-sample-1.bin has no process memory ranges with potential injected code.
-    # This validates that plugin requirements are met and exceptions are not raised.
-    assert rc == 0
-    assert out.count(b"\n") >= 4
-
-
-def test_linux_mountinfo(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.mountinfo.MountInfo", image, volatility, python
-    )
-
-    assert rc == 0
-    assert out.count(b"\n") > 10
-
-
-def test_linux_psaux(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.psaux.PsAux", image, volatility, python)
-
-    assert rc == 0
-    assert out.count(b"\n") > 50
-
-
-def test_linux_ptrace(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.ptrace.Ptrace", image, volatility, python)
-
-    # linux-sample-1.bin has no processes being ptraced.
-    # This validates that plugin requirements are met and exceptions are not raised.
-    assert rc == 0
-    assert out.count(b"\n") >= 4
-
-
-def test_linux_vmaregexscan(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.vmaregexscan.VmaRegExScan",
-        image,
-        volatility,
-        python,
-        pluginargs=["--pid", "1", "--pattern", "\\x7fELF"],
-    )
-
-    assert rc == 0
-    assert out.count(b"\n") > 10
-
-
-def test_linux_vmayarascan_yara_rule(image, volatility, python):
-    yara_rule_01 = r"""
-        rule fullvmayarascan
-        {
-            strings:
-                $s1 = "_nss_files_parse_grent"
-                $s2 = "/lib64/ld-linux-x86-64.so.2"
-                $s3 = "(bufferend - (char *) 0) % sizeof (char *) == 0"
-            condition:
-                all of them
-        }
-    """
-
-    # FIXME: When the minimum Python version includes 3.12, replace the following with:
-    # with tempfile.NamedTemporaryFile(delete_on_close=False) as fd: ...
-    fd, filename = tempfile.mkstemp(suffix=".yar")
-    try:
-        with os.fdopen(fd, "w") as f:
-            f.write(yara_rule_01)
-
-        rc, out, _err = runvol_plugin(
-            "linux.vmayarascan.VmaYaraScan",
-            image,
-            volatility,
-            python,
-            pluginargs=["--pid", "8600", "--yara-file", filename],
-        )
-    finally:
-        with contextlib.suppress(FileNotFoundError):
-            os.remove(filename)
-
-    assert rc == 0
-    assert out.count(b"\n") > 4
-
-
-def test_linux_vmayarascan_yara_string(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.vmayarascan.VmaYaraScan",
-        image,
-        volatility,
-        python,
-        pluginargs=["--pid", "1", "--yara-string", "ELF"],
-    )
-
-    assert rc == 0
-    assert out.count(b"\n") > 10
-
-
-def test_linux_page_cache_files(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.pagecache.Files",
-        image,
-        volatility,
-        python,
-        pluginargs=["--find", "/etc/passwd"],
-    )
-
-    assert rc == 0
-    assert out.count(b"\n") > 4
-
-    # inode_num inode_addr ... file_path
-    assert re.search(
-        rb"146829\s0x88001ab5c270.*?/etc/passwd",
-        out,
-    )
-
-
-def test_linux_page_cache_inodepages(image, volatility, python):
-
-    inode_address = hex(0x88001AB5C270)
-    inode_dump_filename = f"inode_{inode_address}.dmp"
-
-    rc, out, _err = runvol_plugin(
-        "linux.pagecache.InodePages",
-        image,
-        volatility,
-        python,
-        pluginargs=["--inode", inode_address],
-    )
-
-    assert rc == 0
-    assert out.count(b"\n") > 4
-
-    # PageVAddr PagePAddr MappingAddr .. DumpSafe
-    assert re.search(
-        rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True",
-        out,
-    )
-
-    try:
-        rc, out, _err = runvol_plugin(
-            "linux.pagecache.InodePages",
-            image,
-            volatility,
-            python,
-            pluginargs=["--inode", inode_address, "--dump"],
-        )
-
-        assert rc == 0
-        assert out.count(b"\n") >= 4
-
-        assert os.path.exists(inode_dump_filename)
-        with open(inode_dump_filename, "rb") as fp:
-            inode_contents = fp.read()
-        assert inode_contents.count(b"\n") > 30
-        assert inode_contents.count(b"root:x:0:0:root:/root:/bin/bash") > 0
-    finally:
-        with contextlib.suppress(FileNotFoundError):
-            os.remove(inode_dump_filename)
-
-
-def test_linux_check_afinfo(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.check_afinfo.Check_afinfo", image, volatility, python
-    )
-
-    # linux-sample-1.bin has no suspicious results.
-    # This validates that plugin requirements are met and exceptions are not raised.
-    assert rc == 0
-    assert out.count(b"\n") >= 4
-
-
-def test_linux_check_modules(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.check_modules.Check_modules", image, volatility, python
-    )
-
-    # linux-sample-1.bin has no suspicious results.
-    # This validates that plugin requirements are met and exceptions are not raised.
-    assert rc == 0
-    assert out.count(b"\n") >= 4
-
-
-def test_linux_ebpf_progs(image, volatility, python):
-    rc, out, err = runvol_plugin(
-        "linux.ebpf.EBPF",
-        image,
-        volatility,
-        python,
-        globalargs=["-vvv"],
-    )
-
-    if rc != 0 and err.count(b"Unsupported kernel") > 0:
-        # The linux-sample-1.bin kernel implementation isn't supported.
-        # However, we can still check that the plugin requirements are met.
-        return None
-
-    assert rc == 0
-    assert out.count(b"\n") > 4
-
-
-def test_linux_iomem(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.iomem.IOMem", image, volatility, python)
-
-    assert rc == 0
-    assert out.count(b"\n") > 100
-
-
-def test_linux_keyboard_notifiers(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.keyboard_notifiers.Keyboard_notifiers", image, volatility, python
-    )
-
-    # linux-sample-1.bin has no suspicious results for this plugin.
-    # This validates that plugin requirements are met and exceptions are not raised.
-    assert rc == 0
-    assert out.count(b"\n") >= 4
-
-
-def test_linux_kmesg(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.kmsg.Kmsg", image, volatility, python)
-
-    assert rc == 0
-    assert out.count(b"\n") > 100
-
-
-def test_linux_netfilter(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.netfilter.Netfilter", image, volatility, python
-    )
-
-    # linux-sample-1.bin has no suspicious results for this plugin.
-    # This validates that plugin requirements are met and exceptions are not raised.
-    assert rc == 0
-    assert out.count(b"\n") >= 4
-
-
-def test_linux_psscan(image, volatility, python):
-    rc, out, _err = runvol_plugin("linux.psscan.PsScan", image, volatility, python)
-
-    assert rc == 0
-    assert out.count(b"\n") > 100
-
-
-def test_linux_hidden_modules(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.hidden_modules.Hidden_modules", image, volatility, python
-    )
-
-    # linux-sample-1.bin has no hidden modules.
-    # This validates that plugin requirements are met and exceptions are not raised.
-    assert rc == 0
-    assert out.count(b"\n") >= 4
-
-
-def test_linux_ip_addr(image, volatility, python):
-    rc, out, err = runvol_plugin("linux.ip.Addr", image, volatility, python)
-
-    assert re.search(
-        rb"2\s+eth0\s+00:0c:29:8f:ed:ca\s+False\s+192.168.201.161\s+24\s+global\s+UP",
-        out,
-    )
-    assert re.search(
-        rb"2\s+eth0\s+00:0c:29:8f:ed:ca\s+False\s+fe80::20c:29ff:fe8f:edca\s+64\s+link\s+UP",
-        out,
-    )
-    assert out.count(b"\n") >= 8
-    assert rc == 0
-
-
-def test_linux_ip_link(image, volatility, python):
-    rc, out, err = runvol_plugin("linux.ip.Link", image, volatility, python)
-
-    assert re.search(
-        rb"-\s+lo\s+00:00:00:00:00:00\s+UNKNOWN\s+16436\s+noqueue\s+0\s+LOOPBACK,LOWER_UP,UP",
-        out,
-    )
-    assert re.search(
-        rb"-\s+eth0\s+00:0c:29:8f:ed:ca\s+UP\s+1500\s+pfifo_fast\s+1000\s+BROADCAST,LOWER_UP,MULTICAST,UP",
-        out,
-    )
-    assert out.count(b"\n") >= 6
-    assert rc == 0
-
-
-def test_linux_kallsyms(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.kallsyms.Kallsyms",
-        image,
-        volatility,
-        python,
-        pluginargs=["--modules"],
-    )
-    # linux-sample-1.bin has no hidden modules.
-    # This validates that plugin requirements are met and exceptions are not raised.
-    assert rc == 0
-    assert out.count(b"\n") > 1000
-
-    # Addr	Type	Size	Exported	SubSystem	ModuleName	SymbolName	Description
-    # 0xffffa009eba9	t	28	False	module	usbcore	usb_mon_register	Symbol is in the text (code) section
-    assert re.search(
-        rb"0xffffa009eba9\s+t\s+28\s+False\s+module\s+usbcore\s+usb_mon_register\s+Symbol is in the text \(code\) section",
-        out,
-    )
-
-
-def test_linux_pscallstack(image, volatility, python):
-    rc, out, _err = runvol_plugin(
-        "linux.pscallstack.PsCallStack",
-        image,
-        volatility,
-        python,
-        pluginargs=["--pid", "1"],
-    )
-
-    assert rc == 0
-    assert out.count(b"\n") > 30
-
-    # TID     Comm    Position        Address Value   Name    Type    Module
-    # 1       init    39      0x88001f999a40  0xffff81109039  do_select       T       kernel
-    assert re.search(
-        rb"1\s+init\s+39\s+0x88001f999a40.*?0xffff81109039\s+do_select\s+T\s+kernel",
-        out,
-    )
-
-
 # MAC
+# TODO: Migrate and integrate in testing (once analysis is fixed ?)
 
 
 def test_mac_volshell(image, volatility, python):


Reviews:
user: login: Abyss-W4tcher
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T09:36:19Z
user: login: Abyss-W4tcher
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T09:37:51Z

Review comments:
diff_hunk: @@ -0,0 +1,16 @@
+from enum import Enum
+
+
+class Sample:
path: test/__init__.py
user: login: Abyss-W4tcher
body: This can be enhanced in the future (`config_path`, `sha256`, `details` etc.) as needed.
created_at: 2025-03-05T09:36:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,326 @@
+import json
+import hashlib
+import shutil
+import contextlib
+import tempfile
+import os
+from test import test_volatility, WindowsSamples
+
+
+class TestWindowsVolshell:
+    def test_windows_volshell(self, image, volatility, python):
+        out = test_volatility.basic_volshell_test(
+            image, volatility, python, globalargs=("-w",)
+        )
+        assert out.count(b"<EPROCESS") > 40
+
+
+class TestWindowsPslist:
+    def test_windows_generic_pslist(self, volatility, python, image):
+        rc, out, _err = test_volatility.runvol_plugin(
+            "windows.pslist.PsList",
+            image,
+            volatility,
+            python,
+            # Notice that this is needed to hit lru_cache when "specific" will run
path: test/plugins/windows/windows.py
user: login: Abyss-W4tcher
body: The `functools.lru_cache` wrapping of `runvol_plugin` allows for results to be cached automatically once the arguments match. This is handy when specific and generic tests run in fact against the same image.
created_at: 2025-03-05T09:37:51Z
author_association: MEMBER

PR comments:
user: login: ikelos
created_at: 2025-03-05T21:31:44Z
body: Ok, I've asked @atcuno, @dgmcdona  and @npetroni to have a look over it, as our major testing framework people.  Looks ok to me but... what do I know from tests...  5;P
author_association: MEMBER
user: login: atcuno
created_at: 2025-03-07T20:13:38Z
body: Being broken out like this looks much, much nicer and will be easier to maintain
author_association: CONTRIBUTOR
user: login: ikelos
created_at: 2025-03-07T21:31:14Z
body: Ok, looks good (be nice to get in the habbit of using the review functionality in future if possible @atcuno)  5;P
author_association: MEMBER