Skip to content

Commit 6192bde

Browse files
authored
Merge branch 'main' into py313-support
2 parents 7841482 + 385512c commit 6192bde

File tree

23 files changed

+247
-67
lines changed

23 files changed

+247
-67
lines changed

dissect/target/helpers/fsutil.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -516,10 +516,12 @@ def open_decompress(
516516
An binary or text IO stream, depending on the mode with which the file was opened.
517517
518518
Example:
519-
bytes_buf = open_decompress(Path("/dir/file.gz")).read()
519+
.. code-block:: python
520520
521-
for line in open_decompress(Path("/dir/file.gz"), "rt"):
522-
print(line)
521+
bytes_buf = open_decompress(Path("/dir/file.gz")).read()
522+
523+
for line in open_decompress(Path("/dir/file.gz"), "rt"):
524+
print(line)
523525
"""
524526
if path and fileobj:
525527
raise ValueError("path and fileobj are mutually exclusive")
@@ -531,6 +533,7 @@ def open_decompress(
531533
file = path.open("rb")
532534
else:
533535
file = fileobj
536+
file.seek(0)
534537

535538
magic = file.read(5)
536539
file.seek(0)

dissect/target/helpers/localeutil.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,5 @@ def _get_resource_path(path: str) -> Path:
4848
)
4949
if child.attrib["territory"] == "001"
5050
}
51+
52+
WINDOWS_ZONE_MAP["UTC"] = "UTC" # Change 'Etc/UTC' to 'UTC' to be consistent across operating systems.

dissect/target/plugins/apps/vpn/wireguard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
("string", "name"), # basename of .conf file if unset
1616
("net.ipaddress", "address"),
1717
("string", "private_key"),
18-
("string", "listen_port"),
18+
("varint", "listen_port"),
1919
("string", "fw_mark"),
2020
("string", "dns"),
2121
("varint", "table"),

dissect/target/plugins/os/unix/_os.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def users(self, sessions: bool = False) -> Iterator[UnixUserRecord]:
7575
# Yield users found in passwd files.
7676
for passwd_file in PASSWD_FILES:
7777
if (path := self.target.fs.path(passwd_file)).exists():
78-
for line in path.open("rt"):
78+
for line in path.open("rt", errors="surrogateescape"):
7979
line = line.strip()
8080
if not line or line.startswith("#"):
8181
continue
@@ -85,13 +85,12 @@ def users(self, sessions: bool = False) -> Iterator[UnixUserRecord]:
8585
current_user = (pwent.get(0), pwent.get(5), pwent.get(6))
8686
if current_user in seen_users:
8787
continue
88-
8988
seen_users.add(current_user)
9089
yield UnixUserRecord(
9190
name=pwent.get(0),
9291
passwd=pwent.get(1),
93-
uid=pwent.get(2),
94-
gid=pwent.get(3),
92+
uid=pwent.get(2) or None,
93+
gid=pwent.get(3) or None,
9594
gecos=pwent.get(4),
9695
home=posix_path(pwent.get(5)),
9796
shell=pwent.get(6),

dissect/target/plugins/os/unix/linux/sockets.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
("string", "protocol"),
1717
("uint32", "rx_queue"),
1818
("uint32", "tx_queue"),
19-
("string", "local_ip"),
19+
("net.ipaddress", "local_ip"),
2020
("uint16", "local_port"),
21-
("string", "remote_ip"),
21+
("net.ipaddress", "remote_ip"),
2222
("uint16", "remote_port"),
2323
("string", "state"),
2424
("string", "owner"),

dissect/target/plugins/os/unix/locale.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020

2121

2222
def timezone_from_path(path: Path) -> str:
23-
"""Return timezone name for given zoneinfo path.
23+
"""Return timezone name for the given zoneinfo path.
2424
25-
/usr/share/zoneinfo/Europe/Amsterdam -> Europe/Amsterdam
25+
.. code-block::
26+
27+
/usr/share/zoneinfo/Europe/Amsterdam -> Europe/Amsterdam
28+
/usr/share/zoneinfo/UTC -> UTC
29+
Etc/UTC -> UTC
2630
"""
27-
zoneinfo_path = str(path).split("/")
28-
return "/".join(zoneinfo_path[-2:])
31+
return "/".join([p for p in path.parts[-2:] if p.lower() not in ["zoneinfo", "etc"]])
2932

3033

3134
class LocalePlugin(Plugin):
@@ -42,7 +45,7 @@ def timezone(self) -> str | None:
4245
# on most unix systems
4346
if (path := self.target.fs.path("/etc/timezone")).exists():
4447
for line in path.open("rt"):
45-
return line.strip()
48+
return timezone_from_path(Path(line.strip()))
4649

4750
# /etc/localtime can be a symlink, hardlink or a copy of:
4851
# eg. /usr/share/zoneinfo/America/New_York

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

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime, timedelta, timezone
14
from typing import Iterator
25

36
from dissect.target.exceptions import UnsupportedPluginError
@@ -13,12 +16,12 @@
1316
("string", "hash"),
1417
("string", "algorithm"),
1518
("string", "crypt_param"),
16-
("string", "last_change"),
17-
("varint", "min_age"),
18-
("varint", "max_age"),
19+
("datetime", "last_change"),
20+
("datetime", "min_age"),
21+
("datetime", "max_age"),
1922
("varint", "warning_period"),
20-
("string", "inactivity_period"),
21-
("string", "expiration_date"),
23+
("varint", "inactivity_period"),
24+
("datetime", "expiration_date"),
2225
("string", "unused_field"),
2326
],
2427
)
@@ -39,6 +42,7 @@ def passwords(self) -> Iterator[UnixShadowRecord]:
3942
4043
Resources:
4144
- https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
45+
- https://linux.die.net/man/5/shadow
4246
"""
4347

4448
seen_hashes = set()
@@ -64,19 +68,53 @@ def passwords(self) -> Iterator[UnixShadowRecord]:
6468

6569
seen_hashes.add(current_hash)
6670

71+
# improve readability
72+
last_change = None
73+
min_age = None
74+
max_age = None
75+
expiration_date = None
76+
77+
try:
78+
last_change = int(shent.get(2)) if shent.get(2) else None
79+
except ValueError as e:
80+
self.target.log.warning(
81+
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(2)
82+
)
83+
84+
try:
85+
min_age = int(shent.get(3)) if shent.get(3) else None
86+
except ValueError as e:
87+
self.target.log.warning(
88+
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(3)
89+
)
90+
91+
try:
92+
max_age = int(shent.get(4)) if shent.get(4) else None
93+
except ValueError as e:
94+
self.target.log.warning(
95+
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(4)
96+
)
97+
98+
try:
99+
expiration_date = int(shent.get(7)) if shent.get(7) else None
100+
except ValueError as e:
101+
self.target.log.warning(
102+
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(7)
103+
)
104+
67105
yield UnixShadowRecord(
68106
name=shent.get(0),
69107
crypt=shent.get(1),
70108
algorithm=crypt.get("algo"),
71109
crypt_param=crypt.get("param"),
72110
salt=crypt.get("salt"),
73111
hash=crypt.get("hash"),
74-
last_change=shent.get(2),
75-
min_age=shent.get(3),
76-
max_age=shent.get(4),
77-
warning_period=shent.get(5),
78-
inactivity_period=shent.get(6),
79-
expiration_date=shent.get(7),
112+
last_change=epoch_days_to_datetime(last_change) if last_change else None,
113+
min_age=epoch_days_to_datetime(last_change + min_age) if last_change and min_age else None,
114+
max_age=epoch_days_to_datetime(last_change + max_age) if last_change and max_age else None,
115+
warning_period=shent.get(5) if shent.get(5) else None,
116+
inactivity_period=shent.get(6) if shent.get(6) else None,
117+
expiration_date=epoch_days_to_datetime(expiration_date) if expiration_date else None,
80118
unused_field=shent.get(8),
81119
_target=self.target,
82120
)
@@ -128,3 +166,11 @@ def extract_crypt_details(shent: dict) -> dict:
128166
crypt["algo"] = algos[crypt["algo"]]
129167

130168
return crypt
169+
170+
171+
def epoch_days_to_datetime(days: int) -> datetime:
172+
"""Convert a number representing the days since 1 January 1970 to a datetime object."""
173+
if not isinstance(days, int):
174+
raise ValueError("days argument should be an integer")
175+
176+
return datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc) + timedelta(days)

dissect/target/plugins/os/windows/generic.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"filesystem/registry/ndis",
6161
[
6262
("datetime", "ts"),
63-
("string", "network"),
63+
("string", "network_name"),
6464
("string", "name"),
6565
("string", "pnpinstanceid"),
6666
],
@@ -113,7 +113,7 @@
113113
("path", "librarypath"),
114114
("string", "displaystring"),
115115
("bytes", "providerid"),
116-
("string", "enabled"),
116+
("boolean", "enabled"),
117117
("string", "version"),
118118
],
119119
)
@@ -408,7 +408,7 @@ def ndis(self) -> Iterator[NdisRecord]:
408408

409409
yield NdisRecord(
410410
ts=network.ts,
411-
network=sub.name,
411+
network_name=sub.name,
412412
name=name,
413413
pnpinstanceid=pnpinstanceid,
414414
_target=self.target,

dissect/target/plugins/os/windows/log/amcache.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
("string", "pe_image"),
4343
("string", "pe_subsystem"),
4444
("string", "crc_checksum"),
45-
("string", "filesize"),
45+
("filesize", "filesize"),
4646
("wstring", "longname"),
4747
("string", "msi"),
4848
]
@@ -91,7 +91,7 @@ def create_record(
9191
binary_type=install_properties.get("binarytype"),
9292
bin_product_version=install_properties.get("binproductversion"),
9393
bin_file_version=install_properties.get("binfileversion"),
94-
filesize=install_properties.get("filesize"),
94+
filesize=int(install_properties.get("filesize", "0"), 16),
9595
pe_image=install_properties.get("peimagetype"),
9696
product_version=install_properties.get("productversion"),
9797
crc_checksum=install_properties.get("crcchecksum"),

dissect/target/plugins/os/windows/prefetch.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from io import BytesIO
4-
from typing import Iterator
4+
from typing import TYPE_CHECKING, BinaryIO, Iterator
55

66
from dissect.cstruct import cstruct
77
from dissect.util import lzxpress_huffman
@@ -11,6 +11,11 @@
1111
from dissect.target.helpers.record import TargetRecordDescriptor
1212
from dissect.target.plugin import Plugin, arg, export
1313

14+
if TYPE_CHECKING:
15+
from datetime import datetime
16+
17+
from dissect.target.target import Target
18+
1419
PrefetchRecord = TargetRecordDescriptor(
1520
"filesystem/ntfs/prefetch",
1621
[
@@ -167,11 +172,12 @@
167172
17: (c_prefetch.FILE_INFORMATION_17, c_prefetch.FILE_METRICS_ARRAY_ENTRY_17),
168173
23: (c_prefetch.FILE_INFORMATION_23, c_prefetch.FILE_METRICS_ARRAY_ENTRY_23),
169174
30: (c_prefetch.FILE_INFORMATION_26, c_prefetch.FILE_METRICS_ARRAY_ENTRY_23),
175+
31: (c_prefetch.FILE_INFORMATION_26, c_prefetch.FILE_METRICS_ARRAY_ENTRY_23),
170176
}
171177

172178

173179
class Prefetch:
174-
def __init__(self, fh):
180+
def __init__(self, fh: BinaryIO):
175181
header_detect = c_prefetch.PREFETCH_HEADER_DETECT(fh.read(8))
176182
if header_detect.signature == b"MAM\x04":
177183
fh = BytesIO(lzxpress_huffman.decompress(fh))
@@ -186,25 +192,27 @@ def __init__(self, fh):
186192

187193
self.parse()
188194

189-
def identify(self):
195+
def identify(self) -> int:
190196
self.fh.seek(0)
191197
version = self.header.version
192198

193199
if version in prefetch_version_structs.keys():
194200
return version
195201

196-
raise NotImplementedError()
202+
raise NotImplementedError
197203

198-
def parse(self):
204+
def parse(self) -> None:
199205
try:
200206
file_info_header, file_metrics_header = prefetch_version_structs.get(self.version)
201207
self.fh.seek(84)
202208
self.fn = file_info_header(self.fh)
203209
self.metrics = self.parse_metrics(metric_array_struct=file_metrics_header)
204210
except KeyError:
205-
raise NotImplementedError()
211+
raise NotImplementedError
206212

207-
def parse_metrics(self, metric_array_struct):
213+
def parse_metrics(
214+
self, metric_array_struct: c_prefetch.FILE_METRICS_ARRAY_ENTRY_17 | c_prefetch.FILE_METRICS_ARRAY_ENTRY_23
215+
) -> list[str | None]:
208216
metrics = []
209217
self.fh.seek(self.fn.metrics_array_offset)
210218
for _ in range(self.fn.number_of_file_metrics_entries):
@@ -216,20 +224,20 @@ def parse_metrics(self, metric_array_struct):
216224
metrics.append(filename.decode("utf-16-le"))
217225
return metrics
218226

219-
def read_filename(self, off, size):
227+
def read_filename(self, off: int, size: int) -> bytes:
220228
offset = self.fh.tell()
221229
self.fh.seek(off)
222230
data = self.fh.read(size * 2)
223231
self.fh.seek(offset) # reset pointer
224232
return data
225233

226234
@property
227-
def latest_timestamp(self):
235+
def latest_timestamp(self) -> datetime:
228236
"""Get the latest execution timestamp inside the prefetch file."""
229237
return wintimestamp(self.fn.last_run_time)
230238

231239
@property
232-
def previous_timestamps(self):
240+
def previous_timestamps(self) -> list[datetime | None]:
233241
"""Get the previous timestamps from the prefetch file."""
234242
try:
235243
# We check if timestamp actually has a value
@@ -242,7 +250,7 @@ def previous_timestamps(self):
242250
class PrefetchPlugin(Plugin):
243251
"""Windows prefetch plugin."""
244252

245-
def __init__(self, target):
253+
def __init__(self, target: Target):
246254
super().__init__(target)
247255
self.prefetchdir = self.target.fs.path("sysvol/windows/prefetch")
248256

0 commit comments

Comments
 (0)