Skip to content

Commit a9b32eb

Browse files
committed
Merge branch 'feature/windows_notepad_tabs' of github.com:joost-j/dissect.target into feature/windows_notepad_tabs
2 parents 27fca92 + 9bb13c7 commit a9b32eb

File tree

27 files changed

+3260
-617
lines changed

27 files changed

+3260
-617
lines changed

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,19 @@ Opening a shell on a target is straight-forward. You can do so by specifying a p
5858

5959
```bash
6060
target-shell targets/EXAMPLE.vmx
61-
EXAMPLE /> help
61+
WIN-EXAMPLE:/$ help
6262

6363
Documented commands (type help <topic>):
6464
========================================
65-
cat disks filesystems help less python save
66-
cd exit find hexdump ls readlink stat
67-
clear file hash info pwd registry volumes
65+
attr cls enter find info man registry volumes
66+
cat cyber exit hash less pwd save zcat
67+
cd debug file help ll python stat zless
68+
clear disks filesystems hexdump ls readlink tree
6869

69-
EXAMPLE /> ls
70+
WIN-EXAMPLE:/$ ls
71+
$fs$
7072
c:
73+
efi
7174
sysvol
7275
```
7376

dissect/target/filesystems/extfs.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ def lstat(self) -> fsutil.stat_result:
134134
st_info.st_mtime_ns = self.entry.mtime_ns
135135
st_info.st_ctime_ns = self.entry.ctime_ns
136136

137+
# Set blocks
138+
st_info.st_blocks = self.entry.inode.i_blocks_lo
139+
st_info.st_blksize = self.entry.extfs.block_size
140+
137141
return st_info
138142

139143
def attr(self) -> Any:

dissect/target/helpers/shell_application_ids.py

Lines changed: 732 additions & 0 deletions
Large diffs are not rendered by default.

dissect/target/helpers/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@
1313
log = logging.getLogger(__name__)
1414

1515

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+
1627
class StrEnum(str, Enum):
1728
"""Sortable and serializible string-based enum"""
1829

dissect/target/loaders/tar.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
from __future__ import annotations
2+
13
import logging
24
import re
35
import tarfile
46
from pathlib import Path
5-
from typing import Union
67

78
from dissect.target import filesystem, target
89
from dissect.target.filesystems.tar import (
@@ -21,22 +22,25 @@
2122
class TarLoader(Loader):
2223
"""Load tar files."""
2324

24-
def __init__(self, path: Union[Path, str], **kwargs):
25+
def __init__(self, path: Path | str, **kwargs):
2526
super().__init__(path)
2627

28+
if isinstance(path, str):
29+
path = Path(path)
30+
2731
if self.is_compressed(path):
2832
log.warning(
2933
f"Tar file {path!r} is compressed, which will affect performance. "
3034
"Consider uncompressing the archive before passing the tar file to Dissect."
3135
)
3236

33-
self.tar = tarfile.open(path)
37+
self.tar = tarfile.open(fileobj=path.open("rb"))
3438

3539
@staticmethod
3640
def detect(path: Path) -> bool:
3741
return path.name.lower().endswith((".tar", ".tar.gz", ".tgz"))
3842

39-
def is_compressed(self, path: Union[Path, str]) -> bool:
43+
def is_compressed(self, path: Path | str) -> bool:
4044
return str(path).lower().endswith((".tar.gz", ".tgz"))
4145

4246
def map(self, target: target.Target) -> None:

dissect/target/loaders/velociraptor.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
import zipfile
55
from pathlib import Path
6-
from typing import TYPE_CHECKING, Optional
6+
from typing import TYPE_CHECKING
77

88
from dissect.target.loaders.dir import DirLoader, find_dirs, map_dirs
99
from dissect.target.plugin import OperatingSystem
@@ -18,7 +18,7 @@
1818
WINDOWS_ACCESSORS = ["mft", "ntfs", "lazy_ntfs", "ntfs_vss", "auto"]
1919

2020

21-
def find_fs_directories(path: Path) -> tuple[Optional[OperatingSystem], Optional[list[Path]]]:
21+
def find_fs_directories(path: Path) -> tuple[OperatingSystem | None, list[Path] | None]:
2222
fs_root = path.joinpath(FILESYSTEMS_ROOT)
2323

2424
# Unix
@@ -56,7 +56,7 @@ def find_fs_directories(path: Path) -> tuple[Optional[OperatingSystem], Optional
5656
return None, None
5757

5858

59-
def extract_drive_letter(name: str) -> Optional[str]:
59+
def extract_drive_letter(name: str) -> str | None:
6060
# \\.\X: in URL encoding
6161
if len(name) == 14 and name.startswith("%5C%5C.%5C") and name.endswith("%3A"):
6262
return name[10].lower()
@@ -91,7 +91,7 @@ def __init__(self, path: Path, **kwargs):
9191
f"Velociraptor target {path!r} is compressed, which will slightly affect performance. "
9292
"Consider uncompressing the archive and passing the uncompressed folder to Dissect."
9393
)
94-
self.root = zipfile.Path(path)
94+
self.root = zipfile.Path(path.open("rb"))
9595
else:
9696
self.root = path
9797

@@ -105,8 +105,8 @@ def detect(path: Path) -> bool:
105105
# results/
106106
# uploads.json
107107
# [...] 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"))
110110

111111
if path.joinpath(FILESYSTEMS_ROOT).exists() and path.joinpath("uploads.json").exists():
112112
_, dirs = find_fs_directories(path)

dissect/target/plugin.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
33
See dissect/target/plugins/general/example.py for an example plugin.
44
"""
5+
56
from __future__ import annotations
67

78
import fnmatch
9+
import functools
810
import importlib
911
import importlib.util
1012
import inspect
@@ -196,6 +198,8 @@ class attribute. Namespacing results in your plugin needing to be prefixed
196198
The :func:`internal` decorator and :class:`InternalPlugin` set the ``__internal__`` attribute.
197199
Finally. :func:`args` decorator sets the ``__args__`` attribute.
198200
201+
The :func:`alias` decorator populates the ``__aliases__`` private attribute of :class:`Plugin` methods.
202+
199203
Args:
200204
target: The :class:`~dissect.target.target.Target` object to load the plugin for.
201205
"""
@@ -448,6 +452,11 @@ def register(plugincls: Type[Plugin]) -> None:
448452
exports = []
449453
functions = []
450454

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+
451460
for attr in get_nonprivate_attributes(plugincls):
452461
if isinstance(attr, property):
453462
attr = attr.fget
@@ -542,6 +551,47 @@ def decorator(obj):
542551
return decorator
543552

544553

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+
545595
def plugins(
546596
osfilter: Optional[type[OSPlugin]] = None,
547597
special_keys: set[str] = set(),

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
99
from dissect.target.helpers.fsutil import TargetPath
1010
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
1212

1313
CommandHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
1414
"unix/history",
@@ -36,6 +36,7 @@ class CommandHistoryPlugin(Plugin):
3636
("sqlite", ".sqlite_history"),
3737
("zsh", ".zsh_history"),
3838
("ash", ".ash_history"),
39+
("dissect", ".dissect_history"), # wow so meta
3940
)
4041

4142
def __init__(self, target: Target):
@@ -56,12 +57,7 @@ def _find_history_files(self) -> List[Tuple[str, TargetPath, UnixUserRecord]]:
5657
history_files.append((shell, history_path, user_details.user))
5758
return history_files
5859

59-
@export(record=CommandHistoryRecord)
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")
6561
@export(record=CommandHistoryRecord)
6662
def commandhistory(self):
6763
"""Return shell history for all users.

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

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from dissect.target.exceptions import UnsupportedPluginError
77
from dissect.target.helpers.record import TargetRecordDescriptor
8+
from dissect.target.helpers.utils import findall
89
from dissect.target.plugin import Plugin, export
910

1011
try:
@@ -36,17 +37,6 @@
3637
)
3738

3839

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-
5040
def _get_package_name(sequence: Sequence) -> str:
5141
"""Parse sequences within a sequence and return the 'PackageName' value if it exists."""
5242
for value in sequence.native.values():

0 commit comments

Comments
 (0)