Skip to content

Commit

Permalink
Add cPanel lastlogin parser (#317)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zawadidone committed Aug 15, 2023
1 parent 8820f69 commit 8b6d8fb
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 0 deletions.
Empty file.
72 changes: 72 additions & 0 deletions dissect/target/plugins/apps/webhosting/cpanel.py
Original file line number Diff line number Diff line change
@@ -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
)
5 changes: 5 additions & 0 deletions tests/data/plugins/apps/webhosting/cpanel/lastlogin
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions tests/test_plugins_apps_webhosting_cpanel.py
Original file line number Diff line number Diff line change
@@ -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"

0 comments on commit 8b6d8fb

Please sign in to comment.