acquire 3.9.dev9__tar.gz → 3.9.dev11__tar.gz
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.
- {acquire-3.9.dev9/acquire.egg-info → acquire-3.9.dev11}/PKG-INFO +1 -1
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/acquire.py +3 -22
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/collector.py +13 -7
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/outputs/base.py +24 -24
- acquire-3.9.dev11/acquire/outputs/dir.py +49 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/outputs/tar.py +8 -4
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/utils.py +1 -65
- acquire-3.9.dev11/acquire/version.py +4 -0
- acquire-3.9.dev11/acquire/volatilestream.py +66 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11/acquire.egg-info}/PKG-INFO +1 -1
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire.egg-info/SOURCES.txt +3 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/conftest.py +7 -6
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_collector.py +33 -2
- acquire-3.9.dev11/tests/test_outputs_dir.py +58 -0
- acquire-3.9.dev11/tests/test_outputs_tar.py +41 -0
- acquire-3.9.dev9/acquire/outputs/dir.py +0 -30
- acquire-3.9.dev9/acquire/version.py +0 -4
- {acquire-3.9.dev9 → acquire-3.9.dev11}/COPYRIGHT +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/LICENSE +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/MANIFEST.in +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/README.md +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/__init__.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/crypt.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/__init__.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/__init__.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/collect.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/exceptions.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/handles.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/named_objects.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/ntdll.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/types.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/esxi.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/hashes.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/log.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/outputs/__init__.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/tools/__init__.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/tools/decrypter.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/uploaders/__init__.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/uploaders/minio.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/uploaders/plugin.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/uploaders/plugin_registry.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire.egg-info/dependency_links.txt +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire.egg-info/entry_points.txt +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire.egg-info/requires.txt +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire.egg-info/top_level.txt +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/pyproject.toml +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/setup.cfg +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/__init__.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/docs/Makefile +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/docs/conf.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/docs/index.rst +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_acquire_command.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_acquire_modules.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_decryptor_funcs.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_esxi_memory.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_file_sorting.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_minio_uploader.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_misc_users.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_plugin.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_utils.py +0 -0
- {acquire-3.9.dev9 → acquire-3.9.dev11}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: acquire
|
|
3
|
-
Version: 3.9.
|
|
3
|
+
Version: 3.9.dev11
|
|
4
4
|
Summary: A tool to quickly gather forensic artifacts from disk images or a live system into a lightweight container
|
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
|
6
6
|
License: Affero General Public License v3
|
|
@@ -18,7 +18,7 @@ from typing import Iterator, Optional, Union
|
|
|
18
18
|
|
|
19
19
|
from dissect.target import Target, exceptions
|
|
20
20
|
from dissect.target.filesystem import Filesystem
|
|
21
|
-
from dissect.target.filesystems import
|
|
21
|
+
from dissect.target.filesystems import ntfs
|
|
22
22
|
from dissect.target.helpers import fsutil
|
|
23
23
|
from dissect.target.loaders.remote import RemoteStreamConnection
|
|
24
24
|
from dissect.target.loaders.targetd import TargetdLoader
|
|
@@ -281,17 +281,7 @@ class Sys(Module):
|
|
|
281
281
|
|
|
282
282
|
@classmethod
|
|
283
283
|
def _run(cls, target: Target, cli_args: argparse.Namespace, collector: Collector) -> None:
|
|
284
|
-
if not Path("/sys").exists():
|
|
285
|
-
log.error("/sys is unavailable! Skipping...")
|
|
286
|
-
return
|
|
287
|
-
|
|
288
284
|
spec = [("dir", "/sys")]
|
|
289
|
-
|
|
290
|
-
sysfs = dir.DirectoryFilesystem(Path("/sys"))
|
|
291
|
-
|
|
292
|
-
target.filesystems.add(sysfs)
|
|
293
|
-
target.fs.mount("/sys", sysfs)
|
|
294
|
-
|
|
295
285
|
collector.collect(spec, follow=False, volatile=True)
|
|
296
286
|
|
|
297
287
|
|
|
@@ -303,16 +293,7 @@ class Proc(Module):
|
|
|
303
293
|
|
|
304
294
|
@classmethod
|
|
305
295
|
def _run(cls, target: Target, cli_args: argparse.Namespace, collector: Collector) -> None:
|
|
306
|
-
if not Path("/proc").exists():
|
|
307
|
-
log.error("/proc is unavailable! Skipping...")
|
|
308
|
-
return
|
|
309
|
-
|
|
310
296
|
spec = [("dir", "/proc")]
|
|
311
|
-
procfs = dir.DirectoryFilesystem(Path("/proc"))
|
|
312
|
-
|
|
313
|
-
target.filesystems.add(procfs)
|
|
314
|
-
target.fs.mount("/proc", procfs)
|
|
315
|
-
|
|
316
297
|
collector.collect(spec, follow=False, volatile=True)
|
|
317
298
|
|
|
318
299
|
|
|
@@ -550,8 +531,8 @@ class WinMemDump(Module):
|
|
|
550
531
|
)
|
|
551
532
|
return
|
|
552
533
|
|
|
553
|
-
collector.output.write_entry(mem_dump_output_path,
|
|
554
|
-
collector.output.write_entry(mem_dump_errors_output_path,
|
|
534
|
+
collector.output.write_entry(mem_dump_output_path, mem_dump_path)
|
|
535
|
+
collector.output.write_entry(mem_dump_errors_output_path, mem_dump_errors_path)
|
|
555
536
|
collector.report.add_command_collected(cls.__name__, command_parts)
|
|
556
537
|
mem_dump_path.unlink()
|
|
557
538
|
mem_dump_errors_path.unlink()
|
|
@@ -230,7 +230,7 @@ class Collector:
|
|
|
230
230
|
def close(self) -> None:
|
|
231
231
|
self.output.close()
|
|
232
232
|
|
|
233
|
-
def
|
|
233
|
+
def _output_path(self, path: Path, base: Optional[str] = None) -> str:
|
|
234
234
|
base = base or self.base
|
|
235
235
|
outpath = str(path)
|
|
236
236
|
|
|
@@ -299,14 +299,14 @@ class Collector:
|
|
|
299
299
|
log.info("- Collecting file %s: Skipped (DEDUP)", path)
|
|
300
300
|
return
|
|
301
301
|
|
|
302
|
-
outpath = self.
|
|
302
|
+
outpath = self._output_path(outpath or path, base)
|
|
303
303
|
|
|
304
304
|
try:
|
|
305
305
|
entry = path.get()
|
|
306
306
|
if volatile:
|
|
307
|
-
self.output.write_volatile(outpath, entry, size)
|
|
307
|
+
self.output.write_volatile(outpath, entry, size=size)
|
|
308
308
|
else:
|
|
309
|
-
self.output.write_entry(outpath, entry, size)
|
|
309
|
+
self.output.write_entry(outpath, entry, size=size)
|
|
310
310
|
|
|
311
311
|
self.report.add_file_collected(module_name, path)
|
|
312
312
|
result = "OK"
|
|
@@ -322,8 +322,8 @@ class Collector:
|
|
|
322
322
|
|
|
323
323
|
def collect_symlink(self, path: fsutil.TargetPath, module_name: Optional[str] = None) -> None:
|
|
324
324
|
try:
|
|
325
|
-
outpath = self.
|
|
326
|
-
self.output.
|
|
325
|
+
outpath = self._output_path(path)
|
|
326
|
+
self.output.write_entry(outpath, path.get())
|
|
327
327
|
|
|
328
328
|
self.report.add_symlink_collected(module_name, path)
|
|
329
329
|
result = "OK"
|
|
@@ -359,11 +359,17 @@ class Collector:
|
|
|
359
359
|
return
|
|
360
360
|
seen_paths.add(resolved)
|
|
361
361
|
|
|
362
|
+
dir_is_empty = True
|
|
362
363
|
for entry in path.iterdir():
|
|
364
|
+
dir_is_empty = False
|
|
363
365
|
self.collect_path(
|
|
364
366
|
entry, seen_paths=seen_paths, module_name=module_name, follow=follow, volatile=volatile
|
|
365
367
|
)
|
|
366
368
|
|
|
369
|
+
if dir_is_empty and volatile:
|
|
370
|
+
outpath = self._output_path(path)
|
|
371
|
+
self.output.write_entry(outpath, path)
|
|
372
|
+
|
|
367
373
|
except OSError as error:
|
|
368
374
|
if error.errno == errno.ENOENT:
|
|
369
375
|
self.report.add_dir_missing(module_name, path)
|
|
@@ -485,7 +491,7 @@ class Collector:
|
|
|
485
491
|
return
|
|
486
492
|
|
|
487
493
|
def write_bytes(self, destination_path: str, data: bytes) -> None:
|
|
488
|
-
self.output.write_bytes(destination_path, data
|
|
494
|
+
self.output.write_bytes(destination_path, data)
|
|
489
495
|
self.report.add_file_collected(self.bound_module_name, destination_path)
|
|
490
496
|
|
|
491
497
|
|
|
@@ -2,22 +2,18 @@ import io
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import BinaryIO, Optional, Union
|
|
4
4
|
|
|
5
|
-
from dissect.target import Target
|
|
6
5
|
from dissect.target.filesystem import FilesystemEntry
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
from acquire.volatilestream import VolatileStream
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class Output:
|
|
12
11
|
"""Base class to implement acquire output formats with.
|
|
13
12
|
|
|
14
13
|
New output formats must sub-class this class.
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
target: The target that we're using acquire on.
|
|
18
14
|
"""
|
|
19
15
|
|
|
20
|
-
def init(self,
|
|
16
|
+
def init(self, path: Path, **kwargs) -> None:
|
|
21
17
|
pass
|
|
22
18
|
|
|
23
19
|
def write(
|
|
@@ -27,12 +23,12 @@ class Output:
|
|
|
27
23
|
entry: Optional[Union[FilesystemEntry, Path]],
|
|
28
24
|
size: Optional[int] = None,
|
|
29
25
|
) -> None:
|
|
30
|
-
"""Write a
|
|
26
|
+
"""Write a file-like object to the output.
|
|
31
27
|
|
|
32
28
|
Args:
|
|
33
|
-
output_path: The path of the entry in the output
|
|
29
|
+
output_path: The path of the entry in the output.
|
|
34
30
|
fh: The file-like object of the entry to write.
|
|
35
|
-
entry: The optional filesystem entry
|
|
31
|
+
entry: The optional filesystem entry to write.
|
|
36
32
|
size: The optional file size in bytes of the entry to write.
|
|
37
33
|
"""
|
|
38
34
|
raise NotImplementedError()
|
|
@@ -40,18 +36,21 @@ class Output:
|
|
|
40
36
|
def write_entry(
|
|
41
37
|
self,
|
|
42
38
|
output_path: str,
|
|
43
|
-
entry:
|
|
39
|
+
entry: Union[FilesystemEntry, Path],
|
|
44
40
|
size: Optional[int] = None,
|
|
45
41
|
) -> None:
|
|
46
|
-
"""Write a filesystem entry to the output
|
|
42
|
+
"""Write a filesystem entry to the output.
|
|
47
43
|
|
|
48
44
|
Args:
|
|
49
|
-
output_path: The path of the entry in the output
|
|
50
|
-
entry: The
|
|
45
|
+
output_path: The path of the entry in the output.
|
|
46
|
+
entry: The filesystem entry to write.
|
|
51
47
|
size: The optional file size in bytes of the entry to write.
|
|
52
48
|
"""
|
|
53
|
-
|
|
54
|
-
self.
|
|
49
|
+
if entry.is_dir() or entry.is_symlink():
|
|
50
|
+
self.write_bytes(output_path, b"", entry=entry, size=0)
|
|
51
|
+
else:
|
|
52
|
+
with entry.open() as fh:
|
|
53
|
+
self.write(output_path, fh, entry=entry, size=size)
|
|
55
54
|
|
|
56
55
|
def write_bytes(
|
|
57
56
|
self,
|
|
@@ -63,31 +62,32 @@ class Output:
|
|
|
63
62
|
"""Write raw bytes to the output format.
|
|
64
63
|
|
|
65
64
|
Args:
|
|
66
|
-
output_path: The path of the entry in the output
|
|
65
|
+
output_path: The path of the entry in the output.
|
|
67
66
|
data: The raw bytes to write.
|
|
68
|
-
entry: The optional filesystem entry
|
|
67
|
+
entry: The optional filesystem entry to write.
|
|
69
68
|
size: The optional file size in bytes of the entry to write.
|
|
70
69
|
"""
|
|
71
70
|
|
|
72
71
|
stream = io.BytesIO(data)
|
|
73
|
-
self.write(output_path, stream, entry, size)
|
|
72
|
+
self.write(output_path, stream, entry=entry, size=size)
|
|
74
73
|
|
|
75
74
|
def write_volatile(
|
|
76
75
|
self,
|
|
77
76
|
output_path: str,
|
|
78
|
-
entry:
|
|
77
|
+
entry: Union[FilesystemEntry, Path],
|
|
79
78
|
size: Optional[int] = None,
|
|
80
79
|
) -> None:
|
|
81
|
-
"""Write
|
|
80
|
+
"""Write a filesystem entry to the output.
|
|
81
|
+
|
|
82
82
|
Handles files that live in volatile filesystems. Such as procfs and sysfs.
|
|
83
83
|
|
|
84
84
|
Args:
|
|
85
|
-
output_path: The path of the entry in the output
|
|
86
|
-
entry: The
|
|
85
|
+
output_path: The path of the entry in the output.
|
|
86
|
+
entry: The filesystem entry to write.
|
|
87
87
|
size: The optional file size in bytes of the entry to write.
|
|
88
88
|
"""
|
|
89
89
|
try:
|
|
90
|
-
fh =
|
|
90
|
+
fh = VolatileStream(Path(entry.path))
|
|
91
91
|
buf = fh.read()
|
|
92
92
|
size = size or len(buf)
|
|
93
93
|
except (OSError, PermissionError):
|
|
@@ -96,7 +96,7 @@ class Output:
|
|
|
96
96
|
buf = b""
|
|
97
97
|
size = 0
|
|
98
98
|
|
|
99
|
-
self.write_bytes(output_path, buf, entry, size)
|
|
99
|
+
self.write_bytes(output_path, buf, entry=entry, size=size)
|
|
100
100
|
|
|
101
101
|
def close(self) -> None:
|
|
102
102
|
"""Closes the output."""
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import BinaryIO, Optional, Union
|
|
5
|
+
|
|
6
|
+
from dissect.target.filesystem import FilesystemEntry
|
|
7
|
+
|
|
8
|
+
from acquire.outputs.base import Output
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DirectoryOutput(Output):
|
|
12
|
+
def __init__(self, path: Path, **kwargs):
|
|
13
|
+
self.path = path
|
|
14
|
+
|
|
15
|
+
def write(
|
|
16
|
+
self,
|
|
17
|
+
output_path: str,
|
|
18
|
+
fh: BinaryIO,
|
|
19
|
+
entry: Optional[Union[FilesystemEntry, Path]] = None,
|
|
20
|
+
size: Optional[int] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Write a file-like object to a directory.
|
|
23
|
+
|
|
24
|
+
The data from ``fh`` is written, while ``entry`` is used to get some properties of the file.
|
|
25
|
+
|
|
26
|
+
On Windows platforms ``:`` is replaced with ``_`` in the output_path.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
output_path: The path of the entry in the output.
|
|
30
|
+
fh: The file-like object of the entry to write.
|
|
31
|
+
entry: The optional filesystem entry to write.
|
|
32
|
+
size: The optional file size in bytes of the entry to write.
|
|
33
|
+
"""
|
|
34
|
+
if platform.system() == "Windows":
|
|
35
|
+
output_path = output_path.replace(":", "_")
|
|
36
|
+
|
|
37
|
+
out_path = self.path.joinpath(output_path.lstrip("/"))
|
|
38
|
+
|
|
39
|
+
if entry and entry.is_dir():
|
|
40
|
+
out_path.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
else:
|
|
43
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
|
|
45
|
+
with out_path.open("wb") as fhout:
|
|
46
|
+
shutil.copyfileobj(fh, fhout)
|
|
47
|
+
|
|
48
|
+
def close(self) -> None:
|
|
49
|
+
pass
|
|
@@ -49,15 +49,17 @@ class TarOutput(Output):
|
|
|
49
49
|
self,
|
|
50
50
|
output_path: str,
|
|
51
51
|
fh: BinaryIO,
|
|
52
|
-
entry: Optional[Union[FilesystemEntry, Path]],
|
|
52
|
+
entry: Optional[Union[FilesystemEntry, Path]] = None,
|
|
53
53
|
size: Optional[int] = None,
|
|
54
54
|
) -> None:
|
|
55
|
-
"""Write a
|
|
55
|
+
"""Write a file-like object to a tar file.
|
|
56
|
+
|
|
57
|
+
The data from ``fh`` is written, while ``entry`` is used to get some properties of the file.
|
|
56
58
|
|
|
57
59
|
Args:
|
|
58
|
-
output_path: The path of the entry in the output
|
|
60
|
+
output_path: The path of the entry in the output.
|
|
59
61
|
fh: The file-like object of the entry to write.
|
|
60
|
-
entry: The optional filesystem entry
|
|
62
|
+
entry: The optional filesystem entry to write.
|
|
61
63
|
size: The optional file size in bytes of the entry to write.
|
|
62
64
|
"""
|
|
63
65
|
stat = None
|
|
@@ -79,6 +81,8 @@ class TarOutput(Output):
|
|
|
79
81
|
if entry.is_symlink():
|
|
80
82
|
info.type = tarfile.SYMTYPE
|
|
81
83
|
info.linkname = entry.readlink()
|
|
84
|
+
elif entry.is_dir():
|
|
85
|
+
info.type = tarfile.DIRTYPE
|
|
82
86
|
|
|
83
87
|
stat = entry.lstat()
|
|
84
88
|
|
|
@@ -4,83 +4,19 @@ import datetime
|
|
|
4
4
|
import getpass
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
-
import pathlib
|
|
8
7
|
import re
|
|
9
8
|
import sys
|
|
10
9
|
import textwrap
|
|
11
10
|
import traceback
|
|
12
11
|
from enum import Enum
|
|
13
|
-
from io import SEEK_SET, UnsupportedOperation
|
|
14
12
|
from pathlib import Path
|
|
15
|
-
from stat import S_IRGRP, S_IROTH, S_IRUSR
|
|
16
13
|
from typing import Any, Optional
|
|
17
14
|
|
|
18
15
|
from dissect.target import Target
|
|
19
|
-
from dissect.util.stream import AlignedStream
|
|
20
16
|
|
|
21
17
|
from acquire.outputs import OUTPUTS
|
|
22
18
|
from acquire.uploaders.plugin_registry import UploaderRegistry
|
|
23
19
|
|
|
24
|
-
try:
|
|
25
|
-
# Windows systems do not have the fcntl module.
|
|
26
|
-
from fcntl import F_SETFL, fcntl
|
|
27
|
-
|
|
28
|
-
HAS_FCNTL = True
|
|
29
|
-
except ImportError:
|
|
30
|
-
HAS_FCNTL = False
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class VolatileStream(AlignedStream):
|
|
34
|
-
"""Streaming class to handle various procfs and sysfs edge-cases. Backed by `AlignedStream`.
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
path: Path of the file to obtain a file-handle from.
|
|
38
|
-
mode: Mode string to open the file-handle with. Such as "rt" and "rb".
|
|
39
|
-
flags: Flags to open the file-descriptor with.
|
|
40
|
-
size: The maximum size of the stream. None if unknown.
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
def __init__(
|
|
44
|
-
self,
|
|
45
|
-
path: Path,
|
|
46
|
-
mode: str = "rb",
|
|
47
|
-
# Windows and Darwin systems don't have O_NOATIME or O_NONBLOCK. Add them if they are available.
|
|
48
|
-
flags: int = (os.O_RDONLY | getattr(os, "O_NOATIME", 0) | getattr(os, "O_NONBLOCK", 0)),
|
|
49
|
-
size: int = 1024 * 1024 * 5,
|
|
50
|
-
):
|
|
51
|
-
self.fh = path.open(mode)
|
|
52
|
-
self.fd = self.fh.fileno()
|
|
53
|
-
|
|
54
|
-
if HAS_FCNTL:
|
|
55
|
-
fcntl(self.fd, F_SETFL, flags)
|
|
56
|
-
|
|
57
|
-
st_mode = os.fstat(self.fd).st_mode
|
|
58
|
-
write_only = (st_mode & (S_IRUSR | S_IRGRP | S_IROTH)) == 0 # novermin
|
|
59
|
-
|
|
60
|
-
super().__init__(0 if write_only else size)
|
|
61
|
-
|
|
62
|
-
def seek(self, pos: int, whence: int = SEEK_SET) -> int:
|
|
63
|
-
raise UnsupportedOperation("VolatileStream is not seekable")
|
|
64
|
-
|
|
65
|
-
def seekable(self) -> bool:
|
|
66
|
-
return False
|
|
67
|
-
|
|
68
|
-
def _read(self, offset: int, length: int) -> bytes:
|
|
69
|
-
result = []
|
|
70
|
-
while length:
|
|
71
|
-
try:
|
|
72
|
-
buf = os.read(self.fd, min(length, self.size - offset))
|
|
73
|
-
except BlockingIOError:
|
|
74
|
-
break
|
|
75
|
-
|
|
76
|
-
if not buf:
|
|
77
|
-
break
|
|
78
|
-
|
|
79
|
-
result.append(buf)
|
|
80
|
-
offset += len(buf)
|
|
81
|
-
length -= len(buf)
|
|
82
|
-
return b"".join(result)
|
|
83
|
-
|
|
84
20
|
|
|
85
21
|
class StrEnum(str, Enum):
|
|
86
22
|
"""Sortable and serializible string-based enum"""
|
|
@@ -400,7 +336,7 @@ def persist_execution_report(path: Path, report_data: dict) -> Path:
|
|
|
400
336
|
SYSVOL_SUBST = re.compile(r"^(/\?\?/)?[cC]:")
|
|
401
337
|
|
|
402
338
|
|
|
403
|
-
def normalize_path(target: Target, path:
|
|
339
|
+
def normalize_path(target: Target, path: Path, resolve: bool = False) -> str:
|
|
404
340
|
if resolve:
|
|
405
341
|
path = path.resolve()
|
|
406
342
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from io import SEEK_SET, UnsupportedOperation
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from stat import S_IRGRP, S_IROTH, S_IRUSR
|
|
5
|
+
|
|
6
|
+
from dissect.util.stream import AlignedStream
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
# Windows systems do not have the fcntl module.
|
|
10
|
+
from fcntl import F_SETFL, fcntl
|
|
11
|
+
|
|
12
|
+
HAS_FCNTL = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
HAS_FCNTL = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VolatileStream(AlignedStream):
|
|
18
|
+
"""Streaming class to handle various procfs and sysfs edge-cases. Backed by `AlignedStream`.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
path: Path of the file to obtain a file-handle from.
|
|
22
|
+
mode: Mode string to open the file-handle with. Such as "rt" and "rb".
|
|
23
|
+
flags: Flags to open the file-descriptor with.
|
|
24
|
+
size: The maximum size of the stream. None if unknown.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
path: Path,
|
|
30
|
+
mode: str = "rb",
|
|
31
|
+
# Windows and Darwin systems don't have O_NOATIME or O_NONBLOCK. Add them if they are available.
|
|
32
|
+
flags: int = (os.O_RDONLY | getattr(os, "O_NOATIME", 0) | getattr(os, "O_NONBLOCK", 0)),
|
|
33
|
+
size: int = 1024 * 1024 * 5,
|
|
34
|
+
):
|
|
35
|
+
self.fh = path.open(mode)
|
|
36
|
+
self.fd = self.fh.fileno()
|
|
37
|
+
|
|
38
|
+
if HAS_FCNTL:
|
|
39
|
+
fcntl(self.fd, F_SETFL, flags)
|
|
40
|
+
|
|
41
|
+
st_mode = os.fstat(self.fd).st_mode
|
|
42
|
+
write_only = (st_mode & (S_IRUSR | S_IRGRP | S_IROTH)) == 0 # novermin
|
|
43
|
+
|
|
44
|
+
super().__init__(0 if write_only else size)
|
|
45
|
+
|
|
46
|
+
def seek(self, pos: int, whence: int = SEEK_SET) -> int:
|
|
47
|
+
raise UnsupportedOperation("VolatileStream is not seekable")
|
|
48
|
+
|
|
49
|
+
def seekable(self) -> bool:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
def _read(self, offset: int, length: int) -> bytes:
|
|
53
|
+
result = []
|
|
54
|
+
while length:
|
|
55
|
+
try:
|
|
56
|
+
buf = os.read(self.fd, min(length, self.size - offset))
|
|
57
|
+
except BlockingIOError:
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
if not buf:
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
result.append(buf)
|
|
64
|
+
offset += len(buf)
|
|
65
|
+
length -= len(buf)
|
|
66
|
+
return b"".join(result)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: acquire
|
|
3
|
-
Version: 3.9.
|
|
3
|
+
Version: 3.9.dev11
|
|
4
4
|
Summary: A tool to quickly gather forensic artifacts from disk images or a live system into a lightweight container
|
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
|
6
6
|
License: Affero General Public License v3
|
|
@@ -13,6 +13,7 @@ acquire/hashes.py
|
|
|
13
13
|
acquire/log.py
|
|
14
14
|
acquire/utils.py
|
|
15
15
|
acquire/version.py
|
|
16
|
+
acquire/volatilestream.py
|
|
16
17
|
acquire.egg-info/PKG-INFO
|
|
17
18
|
acquire.egg-info/SOURCES.txt
|
|
18
19
|
acquire.egg-info/dependency_links.txt
|
|
@@ -47,6 +48,8 @@ tests/test_esxi_memory.py
|
|
|
47
48
|
tests/test_file_sorting.py
|
|
48
49
|
tests/test_minio_uploader.py
|
|
49
50
|
tests/test_misc_users.py
|
|
51
|
+
tests/test_outputs_dir.py
|
|
52
|
+
tests/test_outputs_tar.py
|
|
50
53
|
tests/test_plugin.py
|
|
51
54
|
tests/test_utils.py
|
|
52
55
|
tests/docs/Makefile
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import io
|
|
2
|
+
from typing import BinaryIO
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
4
5
|
from dissect.target import Target
|
|
@@ -6,14 +7,14 @@ from dissect.target.filesystem import VirtualFile, VirtualFilesystem, VirtualSym
|
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
@pytest.fixture
|
|
9
|
-
def mock_file() ->
|
|
10
|
-
return Mock
|
|
10
|
+
def mock_file() -> BinaryIO:
|
|
11
|
+
return io.BytesIO(b"Mock File")
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@pytest.fixture
|
|
14
|
-
def mock_fs(mock_file) -> VirtualFilesystem:
|
|
15
|
+
def mock_fs(mock_file: BinaryIO) -> VirtualFilesystem:
|
|
15
16
|
fs = VirtualFilesystem(case_sensitive=False)
|
|
16
|
-
fs.makedirs("/foo/bar")
|
|
17
|
+
fs.makedirs("/foo/bar/some-dir")
|
|
17
18
|
fs.map_file_entry("/foo/bar/some-file", VirtualFile(fs, "some-file", mock_file))
|
|
18
19
|
fs.map_file_entry("/foo/bar/own-file", VirtualFile(fs, "own-file", mock_file))
|
|
19
20
|
fs.map_file_entry("/foo/bar/some-symlink", VirtualSymlink(fs, "some-symlink", "/foo/bar/some-file"))
|
|
@@ -22,7 +23,7 @@ def mock_fs(mock_file) -> VirtualFilesystem:
|
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
@pytest.fixture
|
|
25
|
-
def mock_target(mock_fs) -> Target:
|
|
26
|
+
def mock_target(mock_fs: VirtualFilesystem) -> Target:
|
|
26
27
|
target = Target()
|
|
27
28
|
target.fs.mount("/", mock_fs)
|
|
28
29
|
target.filesystems.add(mock_fs)
|
|
@@ -39,8 +39,9 @@ def test_collector() -> None:
|
|
|
39
39
|
|
|
40
40
|
@pytest.fixture
|
|
41
41
|
def mock_collector(mock_target) -> Collector:
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
with patch("acquire.outputs.base.Output", autospec=True) as mock_output:
|
|
43
|
+
collector = Collector(mock_target, mock_output)
|
|
44
|
+
return collector
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
MOCK_SEEN_PATHS = set()
|
|
@@ -279,6 +280,36 @@ def test_collector_collect_path_with_exception(mock_target, mock_collector, repo
|
|
|
279
280
|
assert mock_log.error.call_args.args[0] == log_msg
|
|
280
281
|
|
|
281
282
|
|
|
283
|
+
@pytest.mark.parametrize(
|
|
284
|
+
"path_name, volatile, collect_path_called, write_entry_called",
|
|
285
|
+
[
|
|
286
|
+
("/foo/bar/some-dir", False, False, False),
|
|
287
|
+
("/foo/bar/some-dir", True, False, True),
|
|
288
|
+
("/foo/bar", False, True, False),
|
|
289
|
+
("foo/bar", True, True, False),
|
|
290
|
+
],
|
|
291
|
+
)
|
|
292
|
+
def test_collector_collect_dir(
|
|
293
|
+
mock_target: Target,
|
|
294
|
+
mock_collector: Collector,
|
|
295
|
+
path_name: str,
|
|
296
|
+
volatile: bool,
|
|
297
|
+
collect_path_called: bool,
|
|
298
|
+
write_entry_called: bool,
|
|
299
|
+
) -> None:
|
|
300
|
+
path = mock_target.fs.path(path_name)
|
|
301
|
+
with patch.object(mock_collector, "collect_path", autospec=True):
|
|
302
|
+
mock_collector.collect_dir(
|
|
303
|
+
path,
|
|
304
|
+
seen_paths=MOCK_SEEN_PATHS,
|
|
305
|
+
module_name=MOCK_MODULE_NAME,
|
|
306
|
+
follow=False,
|
|
307
|
+
volatile=volatile,
|
|
308
|
+
)
|
|
309
|
+
assert mock_collector.collect_path.called == collect_path_called
|
|
310
|
+
assert mock_collector.output.write_entry.called == write_entry_called
|
|
311
|
+
|
|
312
|
+
|
|
282
313
|
def create_temp_files(tmp_path: Path, paths: list[str]) -> None:
|
|
283
314
|
for path in paths:
|
|
284
315
|
creation_path = tmp_path.joinpath(path)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from dissect.target.filesystem import VirtualFilesystem
|
|
5
|
+
|
|
6
|
+
from acquire.outputs import DirectoryOutput
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def dir_output(tmp_path: Path) -> DirectoryOutput:
|
|
11
|
+
tmp_path.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
return DirectoryOutput(tmp_path)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def leaves(path: Path) -> list[Path]:
|
|
16
|
+
leave_paths = []
|
|
17
|
+
|
|
18
|
+
dir_is_empty = True
|
|
19
|
+
for path in path.iterdir():
|
|
20
|
+
dir_is_empty = False
|
|
21
|
+
if path.is_dir():
|
|
22
|
+
leave_paths.extend(leaves(path))
|
|
23
|
+
else:
|
|
24
|
+
leave_paths.append(path)
|
|
25
|
+
|
|
26
|
+
if dir_is_empty:
|
|
27
|
+
leave_paths.append(path)
|
|
28
|
+
|
|
29
|
+
return leave_paths
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize(
|
|
33
|
+
"entry_name",
|
|
34
|
+
[
|
|
35
|
+
"/foo/bar/some-file",
|
|
36
|
+
"/foo/bar/some-symlink",
|
|
37
|
+
"/foo/bar/some-dir",
|
|
38
|
+
],
|
|
39
|
+
)
|
|
40
|
+
def test_dir_output_write_entry(mock_fs: VirtualFilesystem, dir_output: DirectoryOutput, entry_name: str) -> None:
|
|
41
|
+
entry = mock_fs.get(entry_name)
|
|
42
|
+
dir_output.write_entry(entry_name, entry)
|
|
43
|
+
dir_output.close()
|
|
44
|
+
|
|
45
|
+
path = dir_output.path
|
|
46
|
+
files = leaves(path)
|
|
47
|
+
|
|
48
|
+
assert len(files) == 1
|
|
49
|
+
|
|
50
|
+
file = files[0]
|
|
51
|
+
assert str(file)[len(str(path)) :] == entry_name
|
|
52
|
+
|
|
53
|
+
if entry.is_dir():
|
|
54
|
+
assert file.is_dir()
|
|
55
|
+
elif entry.is_symlink():
|
|
56
|
+
assert file.is_file()
|
|
57
|
+
elif entry.is_file():
|
|
58
|
+
assert file.is_file()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import tarfile
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from dissect.target.filesystem import VirtualFilesystem
|
|
6
|
+
|
|
7
|
+
from acquire.outputs import TarOutput
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def tar_output(tmp_path: Path) -> TarOutput:
|
|
12
|
+
return TarOutput(tmp_path)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize(
|
|
16
|
+
"entry_name",
|
|
17
|
+
[
|
|
18
|
+
"/foo/bar/some-file",
|
|
19
|
+
"/foo/bar/some-symlink",
|
|
20
|
+
"/foo/bar/some-dir",
|
|
21
|
+
],
|
|
22
|
+
)
|
|
23
|
+
def test_tar_output_write_entry(mock_fs: VirtualFilesystem, tar_output: TarOutput, entry_name: str) -> None:
|
|
24
|
+
entry = mock_fs.get(entry_name)
|
|
25
|
+
tar_output.write_entry(entry_name, entry)
|
|
26
|
+
tar_output.close()
|
|
27
|
+
|
|
28
|
+
tar_file = tarfile.open(tar_output.path)
|
|
29
|
+
files = tar_file.getmembers()
|
|
30
|
+
|
|
31
|
+
assert len(files) == 1
|
|
32
|
+
|
|
33
|
+
file = files[0]
|
|
34
|
+
assert file.path == entry_name
|
|
35
|
+
|
|
36
|
+
if entry.is_dir():
|
|
37
|
+
assert file.isdir()
|
|
38
|
+
elif entry.is_symlink():
|
|
39
|
+
assert file.issym()
|
|
40
|
+
elif entry.is_file():
|
|
41
|
+
assert file.isfile()
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import platform
|
|
2
|
-
import shutil
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import BinaryIO, Optional, Union
|
|
5
|
-
|
|
6
|
-
from dissect.target.filesystem import FilesystemEntry
|
|
7
|
-
|
|
8
|
-
from acquire.outputs.base import Output
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class DirectoryOutput(Output):
|
|
12
|
-
def __init__(self, path: Path, **kwargs):
|
|
13
|
-
self.path = path
|
|
14
|
-
|
|
15
|
-
def write(
|
|
16
|
-
self, output_path: str, fh: BinaryIO, entry: Optional[Union[FilesystemEntry, Path]], size: Optional[int] = None
|
|
17
|
-
) -> None:
|
|
18
|
-
if platform.system() == "Windows":
|
|
19
|
-
output_path = output_path.replace(":", "_")
|
|
20
|
-
|
|
21
|
-
out_path = self.path.joinpath(output_path)
|
|
22
|
-
out_dir = out_path.parent
|
|
23
|
-
if not out_dir.exists():
|
|
24
|
-
out_dir.mkdir(parents=True)
|
|
25
|
-
|
|
26
|
-
with out_path.open("wb") as fhout:
|
|
27
|
-
shutil.copyfileobj(fh, fhout)
|
|
28
|
-
|
|
29
|
-
def close(self) -> None:
|
|
30
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|