diff --git a/dissect/target/plugins/apps/vpn/wireguard.py b/dissect/target/plugins/apps/vpn/wireguard.py index a68a03403..2b5942d80 100644 --- a/dissect/target/plugins/apps/vpn/wireguard.py +++ b/dissect/target/plugins/apps/vpn/wireguard.py @@ -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"), diff --git a/dissect/target/plugins/os/unix/linux/sockets.py b/dissect/target/plugins/os/unix/linux/sockets.py index b79136e58..970b2b3c5 100644 --- a/dissect/target/plugins/os/unix/linux/sockets.py +++ b/dissect/target/plugins/os/unix/linux/sockets.py @@ -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"), diff --git a/dissect/target/plugins/os/unix/shadow.py b/dissect/target/plugins/os/unix/shadow.py index c58957d81..b1c813bcf 100644 --- a/dissect/target/plugins/os/unix/shadow.py +++ b/dissect/target/plugins/os/unix/shadow.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone from typing import Iterator from dissect.target.exceptions import UnsupportedPluginError @@ -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"), ], ) @@ -39,6 +42,7 @@ def passwords(self) -> Iterator[UnixShadowRecord]: Resources: - https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html + - https://linux.die.net/man/5/shadow """ seen_hashes = set() @@ -64,6 +68,40 @@ def passwords(self) -> Iterator[UnixShadowRecord]: 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( + "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( + "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( + "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), @@ -71,12 +109,12 @@ def passwords(self) -> Iterator[UnixShadowRecord]: 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, ) @@ -128,3 +166,11 @@ def extract_crypt_details(shent: dict) -> dict: 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") + + return datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc) + timedelta(days) diff --git a/dissect/target/plugins/os/windows/generic.py b/dissect/target/plugins/os/windows/generic.py index 5c3153cf2..218630e12 100644 --- a/dissect/target/plugins/os/windows/generic.py +++ b/dissect/target/plugins/os/windows/generic.py @@ -60,7 +60,7 @@ "filesystem/registry/ndis", [ ("datetime", "ts"), - ("string", "network"), + ("string", "network_name"), ("string", "name"), ("string", "pnpinstanceid"), ], @@ -113,7 +113,7 @@ ("path", "librarypath"), ("string", "displaystring"), ("bytes", "providerid"), - ("string", "enabled"), + ("boolean", "enabled"), ("string", "version"), ], ) @@ -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, diff --git a/dissect/target/plugins/os/windows/log/amcache.py b/dissect/target/plugins/os/windows/log/amcache.py index ba0b150e0..f264c9140 100644 --- a/dissect/target/plugins/os/windows/log/amcache.py +++ b/dissect/target/plugins/os/windows/log/amcache.py @@ -42,7 +42,7 @@ ("string", "pe_image"), ("string", "pe_subsystem"), ("string", "crc_checksum"), - ("string", "filesize"), + ("filesize", "filesize"), ("wstring", "longname"), ("string", "msi"), ] @@ -91,7 +91,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"), diff --git a/dissect/target/plugins/os/windows/sru.py b/dissect/target/plugins/os/windows/sru.py index 0eee8bedf..35fb512f1 100644 --- a/dissect/target/plugins/os/windows/sru.py +++ b/dissect/target/plugins/os/windows/sru.py @@ -176,8 +176,8 @@ ("path", "app"), ("string", "user"), ("varint", "flags"), - ("varint", "start_time"), - ("varint", "end_time"), + ("datetime", "start_time"), + ("datetime", "end_time"), ("bytes", "usage"), ], ) diff --git a/tests/plugins/apps/vpn/test_wireguard.py b/tests/plugins/apps/vpn/test_wireguard.py index a5639e47f..d9d96ab5b 100644 --- a/tests/plugins/apps/vpn/test_wireguard.py +++ b/tests/plugins/apps/vpn/test_wireguard.py @@ -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 diff --git a/tests/plugins/os/unix/test_shadow.py b/tests/plugins/os/unix/test_shadow.py index 1568e625b..55bc817f8 100644 --- a/tests/plugins/os/unix/test_shadow.py +++ b/tests/plugins/os/unix/test_shadow.py @@ -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 @@ -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 == "" @@ -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')" # noqa:E501 + 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) diff --git a/tests/plugins/os/windows/test_amcache.py b/tests/plugins/os/windows/log/test_amcache.py similarity index 99% rename from tests/plugins/os/windows/test_amcache.py rename to tests/plugins/os/windows/log/test_amcache.py index 8964fbbfb..f8aff21cb 100644 --- a/tests/plugins/os/windows/test_amcache.py +++ b/tests/plugins/os/windows/log/test_amcache.py @@ -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