dissect.target 3.19.dev40__py3-none-any.whl → 3.19.dev42__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
)
|