From 8b6d8fb4e0e6e9b4cea53f80761247935ac523ad Mon Sep 17 00:00:00 2001 From: Zawadi Done Date: Tue, 15 Aug 2023 11:26:42 +0200 Subject: [PATCH] Add cPanel lastlogin parser (#317) --- .../plugins/apps/webhosting/__init__.py | 0 .../target/plugins/apps/webhosting/cpanel.py | 72 +++++++++++++++++++ .../plugins/apps/webhosting/cpanel/lastlogin | 5 ++ tests/test_plugins_apps_webhosting_cpanel.py | 25 +++++++ 4 files changed, 102 insertions(+) create mode 100644 dissect/target/plugins/apps/webhosting/__init__.py create mode 100644 dissect/target/plugins/apps/webhosting/cpanel.py create mode 100644 tests/data/plugins/apps/webhosting/cpanel/lastlogin create mode 100644 tests/test_plugins_apps_webhosting_cpanel.py diff --git a/dissect/target/plugins/apps/webhosting/__init__.py b/dissect/target/plugins/apps/webhosting/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dissect/target/plugins/apps/webhosting/cpanel.py b/dissect/target/plugins/apps/webhosting/cpanel.py new file mode 100644 index 000000000..67ab56c7e --- /dev/null +++ b/dissect/target/plugins/apps/webhosting/cpanel.py @@ -0,0 +1,72 @@ +import re +from datetime import datetime +from typing import Iterator + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +CPanelLastloginRecord = TargetRecordDescriptor( + "application/log/cpanel/lastlogin", + [ + ("datetime", "ts"), + ("string", "user"), + ("net.ipaddress", "remote_ip"), + ], +) + +CPANEL_LASTLOGIN = ".lastlogin" +CPANEL_LOGS_PATH = "/usr/local/cpanel/logs" +CPANEL_LASTLOGIN_PATTERN = re.compile( + r"([^\s]+) # ([0-9]{4}-[0-9]{2}-[0-9]{2}) ([0-9]{2}:[0-9]{2}:[0-9]{2}) ([+-][0-9]{4})" +) + + +class CPanelPlugin(Plugin): + # TODO: Parse other log files https://support.cartika.com/portal/en/kb/articles/whm-cpanel-log-files-and-locations + __namespace__ = "cpanel" + + def check_compatible(self) -> None: + if not self.target.fs.path(CPANEL_LOGS_PATH).exists(): + raise UnsupportedPluginError("No cPanel log path found") + + @export(record=CPanelLastloginRecord) + def lastlogin(self) -> Iterator[CPanelLastloginRecord]: + """Return the content of the cPanel lastlogin file. + + The lastlogin files tracks successful cPanel interface logons. New logon events are only tracked + if the IP-address of the logon changes. + + References: + - https://forums.cpanel.net/threads/cpanel-control-panel-last-login-clarification.579221/ + - https://forums.cpanel.net/threads/lastlogin.707557/ + """ + for user_details in self.target.user_details.all_with_home(): + if (lastlogin := user_details.home_path.joinpath(CPANEL_LASTLOGIN)).exists(): + try: + for index, line in enumerate(lastlogin.open("rt")): + line = line.strip() + if not line: + continue + + if events := CPANEL_LASTLOGIN_PATTERN.findall(line): + for event in events: + remote_ip, date, time, utc_offset = event + + timestamp = datetime.strptime(f"{date} {time} {utc_offset}", "%Y-%m-%d %H:%M:%S %z") + + yield CPanelLastloginRecord( + ts=timestamp, + user=user_details.user.name, + remote_ip=remote_ip, + _target=self.target, + ) + else: + self.target.log.warning( + "The cPanel lastlogin line number %s is malformed: %s", index + 1, lastlogin + ) + + except Exception: + self.target.log.warning( + "An error occurred parsing cPanel lastlogin line number %i in file: %s", index + 1, lastlogin + ) diff --git a/tests/data/plugins/apps/webhosting/cpanel/lastlogin b/tests/data/plugins/apps/webhosting/cpanel/lastlogin new file mode 100644 index 000000000..052494726 --- /dev/null +++ b/tests/data/plugins/apps/webhosting/cpanel/lastlogin @@ -0,0 +1,5 @@ +8.8.8.8 # 2023-06-27 14:22:13 +0100 +8.8.8.8 # 2023-06-28 14:13:37 +0200 +8.8.8.8 # 2023-06-28 14:13:37 +02008.8.8.8 # 2023-06-28 14:13:37 +0200 +8.8.8.8 # 2016-10-04 16:40:39 -0500 +8.8.8.8 # 2016-10-27 14:33:05 -0500 diff --git a/tests/test_plugins_apps_webhosting_cpanel.py b/tests/test_plugins_apps_webhosting_cpanel.py new file mode 100644 index 000000000..ae07faab2 --- /dev/null +++ b/tests/test_plugins_apps_webhosting_cpanel.py @@ -0,0 +1,25 @@ +from datetime import datetime, timezone + +from dissect.target import Target +from dissect.target.filesystem import VirtualFilesystem +from dissect.target.plugins.apps.webhosting.cpanel import CPanelPlugin + +from ._utils import absolute_path + + +def test_cpanel_plugin(target_unix_users: Target, fs_unix: VirtualFilesystem) -> None: + data_file = absolute_path("data/plugins/apps/webhosting/cpanel/lastlogin") + fs_unix.map_file("/home/user/.lastlogin", data_file) + + fs_unix.makedirs("/usr/local/cpanel/logs") + + target_unix_users.add_plugin(CPanelPlugin) + + results = list(target_unix_users.cpanel.lastlogin()) + + record = results[0] + + assert len(results) == 6 + assert record.ts == datetime(2023, 6, 27, 13, 22, 13, tzinfo=timezone.utc) + assert record.user == "user" + assert record.remote_ip == "8.8.8.8"