dissect.target 3.19.dev40__py3-none-any.whl → 3.19.dev42__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dissect/target/filesystems/extfs.py +4 -0
- dissect/target/helpers/shell_application_ids.py +732 -0
- dissect/target/helpers/utils.py +11 -0
- dissect/target/loaders/tar.py +8 -4
- dissect/target/loaders/velociraptor.py +6 -6
- dissect/target/plugin.py +50 -0
- dissect/target/plugins/os/unix/history.py +3 -7
- dissect/target/plugins/os/windows/catroot.py +1 -11
- dissect/target/plugins/os/windows/jumplist.py +292 -0
- dissect/target/plugins/os/windows/lnk.py +84 -89
- dissect/target/target.py +1 -1
- dissect/target/tools/fs.py +25 -65
- dissect/target/tools/fsutils.py +243 -0
- dissect/target/tools/info.py +5 -1
- dissect/target/tools/shell.py +473 -347
- dissect/target/tools/utils.py +9 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/METADATA +10 -6
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/RECORD +23 -20
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/LICENSE +0 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/WHEEL +0 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.19.dev40.dist-info → dissect.target-3.19.dev42.dist-info}/top_level.txt +0 -0
dissect/target/helpers/utils.py
CHANGED
@@ -13,6 +13,17 @@ from dissect.target.helpers import fsutil
|
|
13
13
|
log = logging.getLogger(__name__)
|
14
14
|
|
15
15
|
|
16
|
+
def findall(buf: bytes, needle: bytes) -> Iterator[int]:
|
17
|
+
offset = 0
|
18
|
+
while True:
|
19
|
+
offset = buf.find(needle, offset)
|
20
|
+
if offset == -1:
|
21
|
+
break
|
22
|
+
|
23
|
+
yield offset
|
24
|
+
offset += 1
|
25
|
+
|
26
|
+
|
16
27
|
class StrEnum(str, Enum):
|
17
28
|
"""Sortable and serializible string-based enum"""
|
18
29
|
|
dissect/target/loaders/tar.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import logging
|
2
4
|
import re
|
3
5
|
import tarfile
|
4
6
|
from pathlib import Path
|
5
|
-
from typing import Union
|
6
7
|
|
7
8
|
from dissect.target import filesystem, target
|
8
9
|
from dissect.target.filesystems.tar import (
|
@@ -21,22 +22,25 @@ ANON_FS_RE = re.compile(r"^fs[0-9]+$")
|
|
21
22
|
class TarLoader(Loader):
|
22
23
|
"""Load tar files."""
|
23
24
|
|
24
|
-
def __init__(self, path:
|
25
|
+
def __init__(self, path: Path | str, **kwargs):
|
25
26
|
super().__init__(path)
|
26
27
|
|
28
|
+
if isinstance(path, str):
|
29
|
+
path = Path(path)
|
30
|
+
|
27
31
|
if self.is_compressed(path):
|
28
32
|
log.warning(
|
29
33
|
f"Tar file {path!r} is compressed, which will affect performance. "
|
30
34
|
"Consider uncompressing the archive before passing the tar file to Dissect."
|
31
35
|
)
|
32
36
|
|
33
|
-
self.tar = tarfile.open(path)
|
37
|
+
self.tar = tarfile.open(fileobj=path.open("rb"))
|
34
38
|
|
35
39
|
@staticmethod
|
36
40
|
def detect(path: Path) -> bool:
|
37
41
|
return path.name.lower().endswith((".tar", ".tar.gz", ".tgz"))
|
38
42
|
|
39
|
-
def is_compressed(self, path:
|
43
|
+
def is_compressed(self, path: Path | str) -> bool:
|
40
44
|
return str(path).lower().endswith((".tar.gz", ".tgz"))
|
41
45
|
|
42
46
|
def map(self, target: target.Target) -> None:
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import logging
|
4
4
|
import zipfile
|
5
5
|
from pathlib import Path
|
6
|
-
from typing import TYPE_CHECKING
|
6
|
+
from typing import TYPE_CHECKING
|
7
7
|
|
8
8
|
from dissect.target.loaders.dir import DirLoader, find_dirs, map_dirs
|
9
9
|
from dissect.target.plugin import OperatingSystem
|
@@ -18,7 +18,7 @@ UNIX_ACCESSORS = ["file", "auto"]
|
|
18
18
|
WINDOWS_ACCESSORS = ["mft", "ntfs", "lazy_ntfs", "ntfs_vss", "auto"]
|
19
19
|
|
20
20
|
|
21
|
-
def find_fs_directories(path: Path) -> tuple[
|
21
|
+
def find_fs_directories(path: Path) -> tuple[OperatingSystem | None, list[Path] | None]:
|
22
22
|
fs_root = path.joinpath(FILESYSTEMS_ROOT)
|
23
23
|
|
24
24
|
# Unix
|
@@ -56,7 +56,7 @@ def find_fs_directories(path: Path) -> tuple[Optional[OperatingSystem], Optional
|
|
56
56
|
return None, None
|
57
57
|
|
58
58
|
|
59
|
-
def extract_drive_letter(name: str) ->
|
59
|
+
def extract_drive_letter(name: str) -> str | None:
|
60
60
|
# \\.\X: in URL encoding
|
61
61
|
if len(name) == 14 and name.startswith("%5C%5C.%5C") and name.endswith("%3A"):
|
62
62
|
return name[10].lower()
|
@@ -91,7 +91,7 @@ class VelociraptorLoader(DirLoader):
|
|
91
91
|
f"Velociraptor target {path!r} is compressed, which will slightly affect performance. "
|
92
92
|
"Consider uncompressing the archive and passing the uncompressed folder to Dissect."
|
93
93
|
)
|
94
|
-
self.root = zipfile.Path(path)
|
94
|
+
self.root = zipfile.Path(path.open("rb"))
|
95
95
|
else:
|
96
96
|
self.root = path
|
97
97
|
|
@@ -105,8 +105,8 @@ class VelociraptorLoader(DirLoader):
|
|
105
105
|
# results/
|
106
106
|
# uploads.json
|
107
107
|
# [...] other files related to the collection
|
108
|
-
if path.suffix == ".zip": # novermin
|
109
|
-
path = zipfile.Path(path)
|
108
|
+
if path.exists() and path.suffix == ".zip": # novermin
|
109
|
+
path = zipfile.Path(path.open("rb"))
|
110
110
|
|
111
111
|
if path.joinpath(FILESYSTEMS_ROOT).exists() and path.joinpath("uploads.json").exists():
|
112
112
|
_, dirs = find_fs_directories(path)
|
dissect/target/plugin.py
CHANGED
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
See dissect/target/plugins/general/example.py for an example plugin.
|
4
4
|
"""
|
5
|
+
|
5
6
|
from __future__ import annotations
|
6
7
|
|
7
8
|
import fnmatch
|
9
|
+
import functools
|
8
10
|
import importlib
|
9
11
|
import importlib.util
|
10
12
|
import inspect
|
@@ -196,6 +198,8 @@ class Plugin:
|
|
196
198
|
The :func:`internal` decorator and :class:`InternalPlugin` set the ``__internal__`` attribute.
|
197
199
|
Finally. :func:`args` decorator sets the ``__args__`` attribute.
|
198
200
|
|
201
|
+
The :func:`alias` decorator populates the ``__aliases__`` private attribute of :class:`Plugin` methods.
|
202
|
+
|
199
203
|
Args:
|
200
204
|
target: The :class:`~dissect.target.target.Target` object to load the plugin for.
|
201
205
|
"""
|
@@ -448,6 +452,11 @@ def register(plugincls: Type[Plugin]) -> None:
|
|
448
452
|
exports = []
|
449
453
|
functions = []
|
450
454
|
|
455
|
+
# First pass to resolve aliases
|
456
|
+
for attr in get_nonprivate_attributes(plugincls):
|
457
|
+
for alias in getattr(attr, "__aliases__", []):
|
458
|
+
clone_alias(plugincls, attr, alias)
|
459
|
+
|
451
460
|
for attr in get_nonprivate_attributes(plugincls):
|
452
461
|
if isinstance(attr, property):
|
453
462
|
attr = attr.fget
|
@@ -542,6 +551,47 @@ def arg(*args, **kwargs) -> Callable:
|
|
542
551
|
return decorator
|
543
552
|
|
544
553
|
|
554
|
+
def alias(*args, **kwargs: dict[str, Any]) -> Callable:
|
555
|
+
"""Decorator to be used on :class:`Plugin` functions to register an alias of that function."""
|
556
|
+
|
557
|
+
if not kwargs.get("name") and not args:
|
558
|
+
raise ValueError("Missing argument 'name'")
|
559
|
+
|
560
|
+
def decorator(obj: Callable) -> Callable:
|
561
|
+
if not hasattr(obj, "__aliases__"):
|
562
|
+
obj.__aliases__ = []
|
563
|
+
|
564
|
+
if name := (kwargs.get("name") or args[0]):
|
565
|
+
obj.__aliases__.append(name)
|
566
|
+
|
567
|
+
return obj
|
568
|
+
|
569
|
+
return decorator
|
570
|
+
|
571
|
+
|
572
|
+
def clone_alias(cls: type, attr: Callable, alias: str) -> None:
|
573
|
+
"""Clone the given attribute to an alias in the provided class."""
|
574
|
+
|
575
|
+
# Clone the function object
|
576
|
+
clone = type(attr)(attr.__code__, attr.__globals__, alias, attr.__defaults__, attr.__closure__)
|
577
|
+
clone.__kwdefaults__ = attr.__kwdefaults__
|
578
|
+
|
579
|
+
# Copy some attributes
|
580
|
+
functools.update_wrapper(clone, attr)
|
581
|
+
if wrapped := getattr(attr, "__wrapped__", None):
|
582
|
+
# update_wrapper sets a new wrapper, we want the original
|
583
|
+
clone.__wrapped__ = wrapped
|
584
|
+
|
585
|
+
# Update module path so we can fool inspect.getmodule with subclassed Plugin classes
|
586
|
+
clone.__module__ = cls.__module__
|
587
|
+
|
588
|
+
# Update the names
|
589
|
+
clone.__name__ = alias
|
590
|
+
clone.__qualname__ = f"{cls.__name__}.{alias}"
|
591
|
+
|
592
|
+
setattr(cls, alias, clone)
|
593
|
+
|
594
|
+
|
545
595
|
def plugins(
|
546
596
|
osfilter: Optional[type[OSPlugin]] = None,
|
547
597
|
special_keys: set[str] = set(),
|
@@ -8,7 +8,7 @@ from dissect.target.exceptions import UnsupportedPluginError
|
|
8
8
|
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
|
9
9
|
from dissect.target.helpers.fsutil import TargetPath
|
10
10
|
from dissect.target.helpers.record import UnixUserRecord, create_extended_descriptor
|
11
|
-
from dissect.target.plugin import Plugin, export, internal
|
11
|
+
from dissect.target.plugin import Plugin, alias, export, internal
|
12
12
|
|
13
13
|
CommandHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
|
14
14
|
"unix/history",
|
@@ -36,6 +36,7 @@ class CommandHistoryPlugin(Plugin):
|
|
36
36
|
("sqlite", ".sqlite_history"),
|
37
37
|
("zsh", ".zsh_history"),
|
38
38
|
("ash", ".ash_history"),
|
39
|
+
("dissect", ".dissect_history"), # wow so meta
|
39
40
|
)
|
40
41
|
|
41
42
|
def __init__(self, target: Target):
|
@@ -56,12 +57,7 @@ class CommandHistoryPlugin(Plugin):
|
|
56
57
|
history_files.append((shell, history_path, user_details.user))
|
57
58
|
return history_files
|
58
59
|
|
59
|
-
@
|
60
|
-
def bashhistory(self):
|
61
|
-
"""Deprecated, use commandhistory function."""
|
62
|
-
self.target.log.warn("Function 'bashhistory' is deprecated, use the 'commandhistory' function instead.")
|
63
|
-
return self.commandhistory()
|
64
|
-
|
60
|
+
@alias("bashhistory")
|
65
61
|
@export(record=CommandHistoryRecord)
|
66
62
|
def commandhistory(self):
|
67
63
|
"""Return shell history for all users.
|
@@ -5,6 +5,7 @@ from flow.record.fieldtypes import digest
|
|
5
5
|
|
6
6
|
from dissect.target.exceptions import UnsupportedPluginError
|
7
7
|
from dissect.target.helpers.record import TargetRecordDescriptor
|
8
|
+
from dissect.target.helpers.utils import findall
|
8
9
|
from dissect.target.plugin import Plugin, export
|
9
10
|
|
10
11
|
try:
|
@@ -36,17 +37,6 @@ CatrootRecord = TargetRecordDescriptor(
|
|
36
37
|
)
|
37
38
|
|
38
39
|
|
39
|
-
def findall(buf: bytes, needle: bytes) -> Iterator[int]:
|
40
|
-
offset = 0
|
41
|
-
while True:
|
42
|
-
offset = buf.find(needle, offset)
|
43
|
-
if offset == -1:
|
44
|
-
break
|
45
|
-
|
46
|
-
yield offset
|
47
|
-
offset += 1
|
48
|
-
|
49
|
-
|
50
40
|
def _get_package_name(sequence: Sequence) -> str:
|
51
41
|
"""Parse sequences within a sequence and return the 'PackageName' value if it exists."""
|
52
42
|
for value in sequence.native.values():
|
@@ -0,0 +1,292 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import io
|
4
|
+
import logging
|
5
|
+
from struct import error as StructError
|
6
|
+
from typing import BinaryIO, Iterator
|
7
|
+
|
8
|
+
from dissect.cstruct import cstruct
|
9
|
+
from dissect.ole import OLE
|
10
|
+
from dissect.ole.exceptions import Error as OleError
|
11
|
+
from dissect.shellitem.lnk import Lnk
|
12
|
+
|
13
|
+
from dissect.target import Target
|
14
|
+
from dissect.target.exceptions import UnsupportedPluginError
|
15
|
+
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
|
16
|
+
from dissect.target.helpers.record import create_extended_descriptor
|
17
|
+
from dissect.target.helpers.shell_application_ids import APPLICATION_IDENTIFIERS
|
18
|
+
from dissect.target.helpers.utils import findall
|
19
|
+
from dissect.target.plugin import Plugin, export
|
20
|
+
from dissect.target.plugins.os.windows.lnk import LnkRecord, parse_lnk_file
|
21
|
+
|
22
|
+
log = logging.getLogger(__name__)
|
23
|
+
|
24
|
+
LNK_GUID = b"\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x46"
|
25
|
+
|
26
|
+
JumpListRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
|
27
|
+
"windows/jumplist",
|
28
|
+
[
|
29
|
+
("string", "type"),
|
30
|
+
("string", "application_id"),
|
31
|
+
("string", "application_name"),
|
32
|
+
*LnkRecord.target_fields,
|
33
|
+
],
|
34
|
+
)
|
35
|
+
|
36
|
+
|
37
|
+
custom_destination_def = """
|
38
|
+
struct header {
|
39
|
+
int version;
|
40
|
+
int unknown1;
|
41
|
+
int unknown2;
|
42
|
+
int value_type;
|
43
|
+
}
|
44
|
+
|
45
|
+
struct header_end {
|
46
|
+
int number_of_entries;
|
47
|
+
}
|
48
|
+
|
49
|
+
struct header_end_0 {
|
50
|
+
uint16 name_length;
|
51
|
+
wchar name[name_length];
|
52
|
+
int number_of_entries;
|
53
|
+
}
|
54
|
+
|
55
|
+
struct footer {
|
56
|
+
char magic[4];
|
57
|
+
}
|
58
|
+
"""
|
59
|
+
|
60
|
+
c_custom_destination = cstruct()
|
61
|
+
c_custom_destination.load(custom_destination_def)
|
62
|
+
|
63
|
+
|
64
|
+
class JumpListFile:
|
65
|
+
def __init__(self, fh: BinaryIO, file_name: str):
|
66
|
+
self.fh = fh
|
67
|
+
self.file_name = file_name
|
68
|
+
|
69
|
+
self.application_id, self.application_type = file_name.split(".")
|
70
|
+
self.application_type = self.application_type.split("-")[0]
|
71
|
+
self.application_name = APPLICATION_IDENTIFIERS.get(self.application_id)
|
72
|
+
|
73
|
+
def __iter__(self) -> Iterator[Lnk]:
|
74
|
+
raise NotImplementedError
|
75
|
+
|
76
|
+
@property
|
77
|
+
def name(self) -> str:
|
78
|
+
"""Return the name of the application."""
|
79
|
+
return self.application_name
|
80
|
+
|
81
|
+
@property
|
82
|
+
def id(self) -> str:
|
83
|
+
"""Return the application identifier."""
|
84
|
+
return self.application_id
|
85
|
+
|
86
|
+
@property
|
87
|
+
def type(self) -> str:
|
88
|
+
"""Return the type of the Jump List file."""
|
89
|
+
return self.application_type
|
90
|
+
|
91
|
+
|
92
|
+
class AutomaticDestinationFile(JumpListFile):
|
93
|
+
"""Parse Jump List AutomaticDestination file."""
|
94
|
+
|
95
|
+
def __init__(self, fh: BinaryIO, file_name: str):
|
96
|
+
super().__init__(fh, file_name)
|
97
|
+
self.ole = OLE(self.fh)
|
98
|
+
|
99
|
+
def __iter__(self) -> Iterator[Lnk]:
|
100
|
+
for dir_name in self.ole.root.listdir():
|
101
|
+
if dir_name == "DestList":
|
102
|
+
continue
|
103
|
+
|
104
|
+
dir = self.ole.get(dir_name)
|
105
|
+
|
106
|
+
for item in dir.open():
|
107
|
+
try:
|
108
|
+
yield Lnk(io.BytesIO(item))
|
109
|
+
except StructError:
|
110
|
+
continue
|
111
|
+
except Exception as e:
|
112
|
+
log.warning("Failed to parse LNK file from directory %s", dir_name)
|
113
|
+
log.debug("", exc_info=e)
|
114
|
+
continue
|
115
|
+
|
116
|
+
|
117
|
+
class CustomDestinationFile(JumpListFile):
|
118
|
+
"""Parse Jump List CustomDestination file."""
|
119
|
+
|
120
|
+
MAGIC_FOOTER = 0xBABFFBAB
|
121
|
+
VERSIONS = [2]
|
122
|
+
|
123
|
+
def __init__(self, fh: BinaryIO, file_name: str):
|
124
|
+
super().__init__(fh, file_name)
|
125
|
+
|
126
|
+
self.fh.seek(-4, io.SEEK_END)
|
127
|
+
self.footer = c_custom_destination.footer(self.fh.read(4))
|
128
|
+
self.magic = int.from_bytes(self.footer.magic, "little")
|
129
|
+
|
130
|
+
self.fh.seek(0, io.SEEK_SET)
|
131
|
+
self.header = c_custom_destination.header(self.fh)
|
132
|
+
self.version = self.header.version
|
133
|
+
|
134
|
+
if self.header.value_type == 0:
|
135
|
+
self.header_end = c_custom_destination.header_end_0(self.fh)
|
136
|
+
elif self.header.value_type in [1, 2]:
|
137
|
+
self.header_end = c_custom_destination.header_end(self.fh)
|
138
|
+
else:
|
139
|
+
raise NotImplementedError(
|
140
|
+
f"The value_type ({self.header.value_type}) of the CustomDestination file is not implemented"
|
141
|
+
)
|
142
|
+
|
143
|
+
if self.version not in self.VERSIONS:
|
144
|
+
raise NotImplementedError(f"The CustomDestination file has an unsupported version: {self.version}")
|
145
|
+
|
146
|
+
if not self.MAGIC_FOOTER == self.magic:
|
147
|
+
raise ValueError(f"The CustomDestination file has an invalid magic footer: {self.magic}")
|
148
|
+
|
149
|
+
def __iter__(self) -> Iterator[Lnk]:
|
150
|
+
# Searches for all LNK GUID's because the number of entries in the header is not always correct.
|
151
|
+
buf = self.fh.read()
|
152
|
+
|
153
|
+
for offset in findall(buf, LNK_GUID):
|
154
|
+
try:
|
155
|
+
lnk = Lnk(io.BytesIO(buf[offset + len(LNK_GUID) :]))
|
156
|
+
yield lnk
|
157
|
+
except EOFError:
|
158
|
+
break
|
159
|
+
except Exception as e:
|
160
|
+
log.warning("Failed to parse LNK file from a CustomDestination file")
|
161
|
+
log.debug("", exc_info=e)
|
162
|
+
continue
|
163
|
+
|
164
|
+
|
165
|
+
class JumpListPlugin(Plugin):
|
166
|
+
"""Jump List is a Windows feature introduced in Windows 7.
|
167
|
+
|
168
|
+
It stores information about recently accessed applications and files.
|
169
|
+
|
170
|
+
References:
|
171
|
+
- https://forensics.wiki/jump_lists
|
172
|
+
- https://github.com/libyal/dtformats/blob/main/documentation/Jump%20lists%20format.asciidoc
|
173
|
+
"""
|
174
|
+
|
175
|
+
__namespace__ = "jumplist"
|
176
|
+
|
177
|
+
def __init__(self, target: Target):
|
178
|
+
super().__init__(target)
|
179
|
+
self.automatic_destinations = []
|
180
|
+
self.custom_destinations = []
|
181
|
+
|
182
|
+
for user_details in self.target.user_details.all_with_home():
|
183
|
+
for destination in user_details.home_path.glob(
|
184
|
+
"AppData/Roaming/Microsoft/Windows/Recent/CustomDestinations/*.customDestinations-ms"
|
185
|
+
):
|
186
|
+
self.custom_destinations.append([destination, user_details.user])
|
187
|
+
|
188
|
+
for user_details in self.target.user_details.all_with_home():
|
189
|
+
for destination in user_details.home_path.glob(
|
190
|
+
"AppData/Roaming/Microsoft/Windows/Recent/AutomaticDestinations/*.automaticDestinations-ms"
|
191
|
+
):
|
192
|
+
self.automatic_destinations.append([destination, user_details.user])
|
193
|
+
|
194
|
+
def check_compatible(self) -> None:
|
195
|
+
if not any([self.automatic_destinations, self.custom_destinations]):
|
196
|
+
raise UnsupportedPluginError("No Jump List files found")
|
197
|
+
|
198
|
+
@export(record=JumpListRecord)
|
199
|
+
def custom_destination(self) -> Iterator[JumpListRecord]:
|
200
|
+
"""Return the content of CustomDestination Windows Jump Lists.
|
201
|
+
|
202
|
+
These are created when a user pins an application or a file in a Jump List.
|
203
|
+
|
204
|
+
Yields JumpListRecord with fields:
|
205
|
+
|
206
|
+
.. code-block:: text
|
207
|
+
|
208
|
+
type (string): Type of Jump List.
|
209
|
+
application_id (string): ID of the application.
|
210
|
+
application_name (string): Name of the application.
|
211
|
+
lnk_path (path): Path of the link (.lnk) file.
|
212
|
+
lnk_name (string): Name of the link (.lnk) file.
|
213
|
+
lnk_mtime (datetime): Modification time of the link (.lnk) file.
|
214
|
+
lnk_atime (datetime): Access time of the link (.lnk) file.
|
215
|
+
lnk_ctime (datetime): Creation time of the link (.lnk) file.
|
216
|
+
lnk_relativepath (path): Relative path of target file to the link (.lnk) file.
|
217
|
+
lnk_workdir (path): Path of the working directory the link (.lnk) file will execute from.
|
218
|
+
lnk_iconlocation (path): Path of the display icon used for the link (.lnk) file.
|
219
|
+
lnk_arguments (string): Command-line arguments passed to the target (linked) file.
|
220
|
+
local_base_path (string): Absolute path of the target (linked) file.
|
221
|
+
common_path_suffix (string): Suffix of the local_base_path.
|
222
|
+
lnk_full_path (string): Full path of the linked file. Made from local_base_path and common_path_suffix.
|
223
|
+
lnk_net_name (string): Specifies a server share path; for example, "\\\\server\\share".
|
224
|
+
lnk_device_name (string): Specifies a device; for example, the drive letter "D:"
|
225
|
+
machine_id (string): The NetBIOS name of the machine where the linked file was last known to reside.
|
226
|
+
target_mtime (datetime): Modification time of the target (linked) file.
|
227
|
+
target_atime (datetime): Access time of the target (linked) file.
|
228
|
+
target_ctime (datetime): Creation time of the target (linked) file.
|
229
|
+
"""
|
230
|
+
yield from self._generate_records(self.custom_destinations, CustomDestinationFile)
|
231
|
+
|
232
|
+
@export(record=JumpListRecord)
|
233
|
+
def automatic_destination(self) -> Iterator[JumpListRecord]:
|
234
|
+
"""Return the content of AutomaticDestination Windows Jump Lists.
|
235
|
+
|
236
|
+
These are created automatically when a user opens an application or file.
|
237
|
+
|
238
|
+
Yields JumpListRecord with fields:
|
239
|
+
|
240
|
+
.. code-block:: text
|
241
|
+
|
242
|
+
type (string): Type of Jump List.
|
243
|
+
application_id (string): ID of the application.
|
244
|
+
application_name (string): Name of the application.
|
245
|
+
lnk_path (path): Path of the link (.lnk) file.
|
246
|
+
lnk_name (string): Name of the link (.lnk) file.
|
247
|
+
lnk_mtime (datetime): Modification time of the link (.lnk) file.
|
248
|
+
lnk_atime (datetime): Access time of the link (.lnk) file.
|
249
|
+
lnk_ctime (datetime): Creation time of the link (.lnk) file.
|
250
|
+
lnk_relativepath (path): Relative path of target file to the link (.lnk) file.
|
251
|
+
lnk_workdir (path): Path of the working directory the link (.lnk) file will execute from.
|
252
|
+
lnk_iconlocation (path): Path of the display icon used for the link (.lnk) file.
|
253
|
+
lnk_arguments (string): Command-line arguments passed to the target (linked) file.
|
254
|
+
local_base_path (string): Absolute path of the target (linked) file.
|
255
|
+
common_path_suffix (string): Suffix of the local_base_path.
|
256
|
+
lnk_full_path (string): Full path of the linked file. Made from local_base_path and common_path_suffix.
|
257
|
+
lnk_net_name (string): Specifies a server share path; for example, "\\\\server\\share".
|
258
|
+
lnk_device_name (string): Specifies a device; for example, the drive letter "D:"
|
259
|
+
machine_id (string): The NetBIOS name of the machine where the linked file was last known to reside.
|
260
|
+
target_mtime (datetime): Modification time of the target (linked) file.
|
261
|
+
target_atime (datetime): Access time of the target (linked) file.
|
262
|
+
target_ctime (datetime): Creation time of the target (linked) file.
|
263
|
+
"""
|
264
|
+
yield from self._generate_records(self.automatic_destinations, AutomaticDestinationFile)
|
265
|
+
|
266
|
+
def _generate_records(
|
267
|
+
self,
|
268
|
+
destinations: list,
|
269
|
+
destination_file: AutomaticDestinationFile | CustomDestinationFile,
|
270
|
+
) -> Iterator[JumpListRecord]:
|
271
|
+
for destination, user in destinations:
|
272
|
+
fh = destination.open("rb")
|
273
|
+
|
274
|
+
try:
|
275
|
+
jumplist = destination_file(fh, destination.name)
|
276
|
+
except OleError:
|
277
|
+
continue
|
278
|
+
except Exception as e:
|
279
|
+
self.target.log.warning("Failed to parse Jump List file: %s", destination)
|
280
|
+
self.target.log.debug("", exc_info=e)
|
281
|
+
continue
|
282
|
+
|
283
|
+
for lnk in jumplist:
|
284
|
+
if lnk := parse_lnk_file(self.target, lnk, destination):
|
285
|
+
yield JumpListRecord(
|
286
|
+
type=jumplist.type,
|
287
|
+
application_name=jumplist.name,
|
288
|
+
application_id=jumplist.id,
|
289
|
+
**lnk._asdict(),
|
290
|
+
_user=user,
|
291
|
+
_target=self.target,
|
292
|
+
)
|