Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cPanel lastlogin parser #317

Merged
merged 16 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")

Check warning on line 31 in dissect/target/plugins/apps/webhosting/cpanel.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/webhosting/cpanel.py#L31

Added line #L31 was not covered by tests

@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

Check warning on line 50 in dissect/target/plugins/apps/webhosting/cpanel.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/webhosting/cpanel.py#L50

Added line #L50 was not covered by tests

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(

Check warning on line 65 in dissect/target/plugins/apps/webhosting/cpanel.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/webhosting/cpanel.py#L65

Added line #L65 was not covered by tests
"The cPanel lastlogin line number %s is malformed: %s", index + 1, lastlogin
)

except Exception:
self.target.log.warning(

Check warning on line 70 in dissect/target/plugins/apps/webhosting/cpanel.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/webhosting/cpanel.py#L69-L70

Added lines #L69 - L70 were not covered by tests
"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"