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.
Files changed (61) hide show
  1. {acquire-3.9.dev9/acquire.egg-info → acquire-3.9.dev11}/PKG-INFO +1 -1
  2. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/acquire.py +3 -22
  3. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/collector.py +13 -7
  4. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/outputs/base.py +24 -24
  5. acquire-3.9.dev11/acquire/outputs/dir.py +49 -0
  6. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/outputs/tar.py +8 -4
  7. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/utils.py +1 -65
  8. acquire-3.9.dev11/acquire/version.py +4 -0
  9. acquire-3.9.dev11/acquire/volatilestream.py +66 -0
  10. {acquire-3.9.dev9 → acquire-3.9.dev11/acquire.egg-info}/PKG-INFO +1 -1
  11. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire.egg-info/SOURCES.txt +3 -0
  12. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/conftest.py +7 -6
  13. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_collector.py +33 -2
  14. acquire-3.9.dev11/tests/test_outputs_dir.py +58 -0
  15. acquire-3.9.dev11/tests/test_outputs_tar.py +41 -0
  16. acquire-3.9.dev9/acquire/outputs/dir.py +0 -30
  17. acquire-3.9.dev9/acquire/version.py +0 -4
  18. {acquire-3.9.dev9 → acquire-3.9.dev11}/COPYRIGHT +0 -0
  19. {acquire-3.9.dev9 → acquire-3.9.dev11}/LICENSE +0 -0
  20. {acquire-3.9.dev9 → acquire-3.9.dev11}/MANIFEST.in +0 -0
  21. {acquire-3.9.dev9 → acquire-3.9.dev11}/README.md +0 -0
  22. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/__init__.py +0 -0
  23. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/crypt.py +0 -0
  24. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/__init__.py +0 -0
  25. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/__init__.py +0 -0
  26. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/collect.py +0 -0
  27. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/exceptions.py +0 -0
  28. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/handles.py +0 -0
  29. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/named_objects.py +0 -0
  30. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/ntdll.py +0 -0
  31. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/dynamic/windows/types.py +0 -0
  32. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/esxi.py +0 -0
  33. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/hashes.py +0 -0
  34. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/log.py +0 -0
  35. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/outputs/__init__.py +0 -0
  36. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/tools/__init__.py +0 -0
  37. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/tools/decrypter.py +0 -0
  38. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/uploaders/__init__.py +0 -0
  39. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/uploaders/minio.py +0 -0
  40. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/uploaders/plugin.py +0 -0
  41. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire/uploaders/plugin_registry.py +0 -0
  42. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire.egg-info/dependency_links.txt +0 -0
  43. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire.egg-info/entry_points.txt +0 -0
  44. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire.egg-info/requires.txt +0 -0
  45. {acquire-3.9.dev9 → acquire-3.9.dev11}/acquire.egg-info/top_level.txt +0 -0
  46. {acquire-3.9.dev9 → acquire-3.9.dev11}/pyproject.toml +0 -0
  47. {acquire-3.9.dev9 → acquire-3.9.dev11}/setup.cfg +0 -0
  48. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/__init__.py +0 -0
  49. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/docs/Makefile +0 -0
  50. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/docs/conf.py +0 -0
  51. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/docs/index.rst +0 -0
  52. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_acquire_command.py +0 -0
  53. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_acquire_modules.py +0 -0
  54. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_decryptor_funcs.py +0 -0
  55. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_esxi_memory.py +0 -0
  56. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_file_sorting.py +0 -0
  57. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_minio_uploader.py +0 -0
  58. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_misc_users.py +0 -0
  59. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_plugin.py +0 -0
  60. {acquire-3.9.dev9 → acquire-3.9.dev11}/tests/test_utils.py +0 -0
  61. {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.dev9
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 dir, ntfs
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, entry=mem_dump_path)
554
- collector.output.write_entry(mem_dump_errors_output_path, entry=mem_dump_errors_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 _create_output_path(self, path: Path, base: Optional[str] = None) -> str:
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._create_output_path(outpath or path, base)
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._create_output_path(path)
326
- self.output.write_bytes(outpath, b"", path.get(), 0)
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, None)
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
- import acquire.utils
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, target: Target):
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 filesystem entry or file-like object to the implemented output type.
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 format.
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 of the entry to write.
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: Optional[Union[FilesystemEntry, Path]],
39
+ entry: Union[FilesystemEntry, Path],
44
40
  size: Optional[int] = None,
45
41
  ) -> None:
46
- """Write a filesystem entry to the output format.
42
+ """Write a filesystem entry to the output.
47
43
 
48
44
  Args:
49
- output_path: The path of the entry in the output format.
50
- entry: The optional filesystem entry of the entry to write.
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
- with entry.open() as fh:
54
- self.write(output_path, fh, entry, size)
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 format.
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 of the entry to write.
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: Optional[Union[FilesystemEntry, Path]],
77
+ entry: Union[FilesystemEntry, Path],
79
78
  size: Optional[int] = None,
80
79
  ) -> None:
81
- """Write specified path to the output format.
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 format.
86
- entry: The optional filesystem entry of the entry to write.
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 = acquire.utils.VolatileStream(Path(entry.path))
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 filesystem entry or file-like object to a tar file.
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 format.
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 of the entry to write.
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: pathlib.Path, resolve: bool = False) -> str:
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,4 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ __version__ = version = '3.9.dev11'
4
+ __version_tuple__ = version_tuple = (3, 9, 'dev11')
@@ -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.dev9
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
- from unittest.mock import Mock
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() -> Mock:
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
- collector = Collector(mock_target, Mock())
43
- return collector
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
@@ -1,4 +0,0 @@
1
- # file generated by setuptools_scm
2
- # don't change, don't track in version control
3
- __version__ = version = '3.9.dev9'
4
- __version_tuple__ = version_tuple = (3, 9, 'dev9')
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