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

Fix inconsistent duplicate field mappings in various plugins #990

Merged
merged 4 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion dissect/target/plugins/apps/vpn/wireguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
("string", "name"), # basename of .conf file if unset
("net.ipaddress", "address"),
("string", "private_key"),
("string", "listen_port"),
("varint", "listen_port"),
("string", "fw_mark"),
("string", "dns"),
("varint", "table"),
Expand Down
4 changes: 2 additions & 2 deletions dissect/target/plugins/os/unix/linux/sockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
("string", "protocol"),
("uint32", "rx_queue"),
("uint32", "tx_queue"),
("string", "local_ip"),
("net.ipaddress", "local_ip"),
("uint16", "local_port"),
("string", "remote_ip"),
("net.ipaddress", "remote_ip"),
("uint16", "remote_port"),
("string", "state"),
("string", "owner"),
Expand Down
68 changes: 57 additions & 11 deletions dissect/target/plugins/os/unix/shadow.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import annotations

from datetime import datetime, timedelta, timezone
from typing import Iterator

from dissect.target.exceptions import UnsupportedPluginError
Expand All @@ -13,12 +16,12 @@
("string", "hash"),
("string", "algorithm"),
("string", "crypt_param"),
("string", "last_change"),
("varint", "min_age"),
("varint", "max_age"),
("datetime", "last_change"),
("datetime", "min_age"),
("datetime", "max_age"),
("varint", "warning_period"),
("string", "inactivity_period"),
("string", "expiration_date"),
("varint", "inactivity_period"),
("datetime", "expiration_date"),
("string", "unused_field"),
],
)
Expand All @@ -39,6 +42,7 @@

Resources:
- https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
- https://linux.die.net/man/5/shadow
"""

seen_hashes = set()
Expand All @@ -64,19 +68,53 @@

seen_hashes.add(current_hash)

# improve readability
last_change = None
min_age = None
max_age = None
expiration_date = None

try:
last_change = int(shent.get(2)) if shent.get(2) else None
except ValueError as e:
self.target.log.warning(
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(2)
)

try:
min_age = int(shent.get(3)) if shent.get(3) else None
except ValueError as e:
self.target.log.warning(

Check warning on line 87 in dissect/target/plugins/os/unix/shadow.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/unix/shadow.py#L86-L87

Added lines #L86 - L87 were not covered by tests
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(3)
)

try:
max_age = int(shent.get(4)) if shent.get(4) else None
except ValueError as e:
self.target.log.warning(

Check warning on line 94 in dissect/target/plugins/os/unix/shadow.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/unix/shadow.py#L93-L94

Added lines #L93 - L94 were not covered by tests
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(4)
)

try:
expiration_date = int(shent.get(7)) if shent.get(7) else None
except ValueError as e:
self.target.log.warning(

Check warning on line 101 in dissect/target/plugins/os/unix/shadow.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/unix/shadow.py#L100-L101

Added lines #L100 - L101 were not covered by tests
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(7)
)

yield UnixShadowRecord(
name=shent.get(0),
crypt=shent.get(1),
algorithm=crypt.get("algo"),
crypt_param=crypt.get("param"),
salt=crypt.get("salt"),
hash=crypt.get("hash"),
last_change=shent.get(2),
min_age=shent.get(3),
max_age=shent.get(4),
warning_period=shent.get(5),
inactivity_period=shent.get(6),
expiration_date=shent.get(7),
last_change=epoch_days_to_datetime(last_change) if last_change else None,
min_age=epoch_days_to_datetime(last_change + min_age) if last_change and min_age else None,
max_age=epoch_days_to_datetime(last_change + max_age) if last_change and max_age else None,
warning_period=shent.get(5) if shent.get(5) else None,
inactivity_period=shent.get(6) if shent.get(6) else None,
expiration_date=epoch_days_to_datetime(expiration_date) if expiration_date else None,
unused_field=shent.get(8),
_target=self.target,
)
Expand Down Expand Up @@ -128,3 +166,11 @@
crypt["algo"] = algos[crypt["algo"]]

return crypt


def epoch_days_to_datetime(days: int) -> datetime:
"""Convert a number representing the days since 1 January 1970 to a datetime object."""
if not isinstance(days, int):
raise ValueError("days argument should be an integer")

Check warning on line 174 in dissect/target/plugins/os/unix/shadow.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/unix/shadow.py#L174

Added line #L174 was not covered by tests
Horofic marked this conversation as resolved.
Show resolved Hide resolved

return datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc) + timedelta(days)
6 changes: 3 additions & 3 deletions dissect/target/plugins/os/windows/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"filesystem/registry/ndis",
[
("datetime", "ts"),
("string", "network"),
("string", "network_name"),
("string", "name"),
("string", "pnpinstanceid"),
],
Expand Down Expand Up @@ -113,7 +113,7 @@
("path", "librarypath"),
("string", "displaystring"),
("bytes", "providerid"),
("string", "enabled"),
("boolean", "enabled"),
("string", "version"),
],
)
Expand Down Expand Up @@ -408,7 +408,7 @@ def ndis(self) -> Iterator[NdisRecord]:

yield NdisRecord(
ts=network.ts,
network=sub.name,
network_name=sub.name,
name=name,
pnpinstanceid=pnpinstanceid,
_target=self.target,
Expand Down
5 changes: 3 additions & 2 deletions dissect/target/plugins/os/windows/log/amcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
("string", "pe_image"),
("string", "pe_subsystem"),
("string", "crc_checksum"),
("string", "filesize"),
("filesize", "filesize"),
("wstring", "longname"),
("string", "msi"),
]
Expand All @@ -69,6 +69,7 @@ def create_record(
create: str,
target: Target,
) -> TargetRecordDescriptor:

JSCU-CNI marked this conversation as resolved.
Show resolved Hide resolved
return description(
start_time=_to_log_timestamp(install_properties.get("starttime")),
stop_time=_to_log_timestamp(install_properties.get("stoptime")),
Expand All @@ -91,7 +92,7 @@ def create_record(
binary_type=install_properties.get("binarytype"),
bin_product_version=install_properties.get("binproductversion"),
bin_file_version=install_properties.get("binfileversion"),
filesize=install_properties.get("filesize"),
filesize=int(install_properties.get("filesize", "0"), 16),
pe_image=install_properties.get("peimagetype"),
product_version=install_properties.get("productversion"),
crc_checksum=install_properties.get("crcchecksum"),
Expand Down
4 changes: 2 additions & 2 deletions dissect/target/plugins/os/windows/sru.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@
("path", "app"),
("string", "user"),
("varint", "flags"),
("varint", "start_time"),
("varint", "end_time"),
("datetime", "start_time"),
("datetime", "end_time"),
("bytes", "usage"),
],
)
Expand Down
2 changes: 1 addition & 1 deletion tests/plugins/apps/vpn/test_wireguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_wireguard_plugin_global_log(target_unix_users, fs_unix):
assert record.name == "wg0"
assert str(record.address) == "10.13.37.1"
assert record.private_key == "UHJpdmF0ZUtleQ=="
assert record.listen_port == "12345"
assert record.listen_port == 12345
assert record.source == "etc/wireguard/wg0.conf"
assert record.dns is None

Expand Down
77 changes: 72 additions & 5 deletions tests/plugins/os/unix/test_shadow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from datetime import datetime, timezone
from io import BytesIO
from pathlib import Path
from textwrap import dedent

import pytest

from dissect.target.filesystem import VirtualFilesystem
from dissect.target.plugins.os.unix.shadow import ShadowPlugin
Expand All @@ -25,12 +29,12 @@ def test_unix_shadow(target_unix_users: Target, fs_unix: VirtualFilesystem) -> N
) # noqa E501
assert results[0].algorithm == "sha512"
assert results[0].crypt_param is None
assert results[0].last_change == "18963"
assert results[0].min_age == 0
assert results[0].max_age == 99999
assert results[0].last_change == datetime(2021, 12, 2, 0, 0, 0, tzinfo=timezone.utc) # 18963
assert results[0].min_age is None
assert results[0].max_age == datetime(2295, 9, 16, 0, 0, 0, tzinfo=timezone.utc) # 99999
assert results[0].warning_period == 7
assert results[0].inactivity_period == ""
assert results[0].expiration_date == ""
assert results[0].inactivity_period is None
assert results[0].expiration_date is None
assert results[0].unused_field == ""


Expand All @@ -49,3 +53,66 @@ def test_unix_shadow_backup_file(target_unix_users: Target, fs_unix: VirtualFile
assert results[0].name == "test"
assert results[1].name == "other-user"
assert results[0].hash == results[1].hash


def test_unix_shadow_invalid_shent(
caplog: pytest.LogCaptureFixture, target_unix_users: Target, fs_unix: VirtualFilesystem
) -> None:
"""test if we can parse invalid day values in shents."""

shadow_invalid = """
no_last_change:$6$salt$hash1::0:99999:7::123456:
no_max_age:$6$salt$hash2:18963:0::7:::
only_last_change:$6$salt$hash3:18963::::::
no_int_fields:$6$salt$hash4:string::::::
daemon:*:18474:0:99999:7:::
bin:*:18474:0:99999:7:::
nobody:*:18474:0:99999:7:::
regular:$6$salt$hash5:1337:0:99999:7::123456:
"""
fs_unix.map_file_fh("/etc/shadow", BytesIO(dedent(shadow_invalid).encode()))

results = list(target_unix_users.passwords())
assert len(results) == 5

assert [r.name for r in results] == [
"no_last_change",
"no_max_age",
"only_last_change",
"no_int_fields",
"regular",
]

assert results[0].name == "no_last_change"
assert results[0].last_change is None
assert results[0].min_age is None
assert results[0].max_age is None
assert results[0].warning_period == 7
assert results[0].inactivity_period is None
assert results[0].expiration_date == datetime(2308, 1, 6, tzinfo=timezone.utc)

assert results[1].name == "no_max_age"
assert results[1].last_change == datetime(2021, 12, 2, tzinfo=timezone.utc)
assert results[1].max_age is None

assert results[2].name == "only_last_change"
assert results[2].last_change == datetime(2021, 12, 2, tzinfo=timezone.utc)

assert results[3].name == "no_int_fields"
assert results[3].last_change is None
assert (
"Unable to parse last_change shadow value in /etc/shadow: invalid literal for int() with base 10: 'string' ('string')"
JSCU-CNI marked this conversation as resolved.
Show resolved Hide resolved
in caplog.text
)

# make sure we parsed the last entry even though the other entries are 'broken'
assert results[-1].name == "regular"
assert results[-1].salt == "salt"
assert results[-1].hash == "hash5"
assert results[-1].algorithm == "sha512"
assert results[-1].last_change == datetime(1973, 8, 30, tzinfo=timezone.utc)
assert results[-1].min_age is None
assert results[-1].max_age == datetime(2247, 6, 14, tzinfo=timezone.utc)
assert results[-1].warning_period == 7
assert results[-1].inactivity_period is None
assert results[-1].expiration_date == datetime(2308, 1, 6, tzinfo=timezone.utc)
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,4 @@ def test_amcache_install_entry(target_win: Target):
assert str(entry.create) == create
assert str(entry.path) == r"C:\Users\JohnCena"
assert str(entry.longname) == r"7z2201-x64.exe"
assert entry.filesize == 1575742
Loading