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, volshellargs=("-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.malware.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.malware.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.malware.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.malware.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.malware.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.malware.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.malware.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.malware.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.malware.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.malware.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,
        )


class TestLinuxSockscan:
    def test_linux_sockscan(self, volatility, python):
        # designed for linux-sample-1.dmp SHA1:1C3A4627EDCA94A7ADE3414592BEF0E62D7D3BB6
        image = LinuxSamples.LINUX_GENERIC.value.path
        rc, out, err = test_volatility.runvol_plugin(
            "linux.sockscan.Sockscan", image, volatility, python
        )

        # ensure that multiple unix paths for sockets have been found
        assert (
            len(
                re.findall(
                    rb"(/[ -~]+?){1,8}",
                    out,
                )
            )
            >= 10
        )

        # ensure that multiple IPv4 addresses have been found
        assert (
            len(
                re.findall(
                    rb"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}",
                    out,
                )
            )
            >= 10
        )

        assert out.count(b"\n") >= 50
        assert rc == 0
