From 5395f5aa9336230145065ce1a8778b5601cbf122 Mon Sep 17 00:00:00 2001 From: pyrco <105293448+pyrco@users.noreply.github.com> Date: Tue, 22 Aug 2023 12:14:44 +0200 Subject: [PATCH] Collect empty directories when the source is volatile (DIS-1931) --- acquire/acquire.py | 4 ++-- acquire/collector.py | 20 ++++++++++++------- acquire/outputs/base.py | 44 ++++++++++++++++++++--------------------- acquire/outputs/dir.py | 33 +++++++++++++++++++++++++------ acquire/outputs/tar.py | 10 +++++++--- 5 files changed, 71 insertions(+), 40 deletions(-) diff --git a/acquire/acquire.py b/acquire/acquire.py index 7e078685..d6d61bea 100644 --- a/acquire/acquire.py +++ b/acquire/acquire.py @@ -550,8 +550,8 @@ def _run(cls, target: Target, cli_args: argparse.Namespace, collector: Collector ) return - collector.output.write_entry(mem_dump_output_path, entry=mem_dump_path) - collector.output.write_entry(mem_dump_errors_output_path, entry=mem_dump_errors_path) + collector.output.write_entry(mem_dump_output_path, mem_dump_path) + collector.output.write_entry(mem_dump_errors_output_path, mem_dump_errors_path) collector.report.add_command_collected(cls.__name__, command_parts) mem_dump_path.unlink() mem_dump_errors_path.unlink() diff --git a/acquire/collector.py b/acquire/collector.py index a7e2f8bc..3e22cca1 100644 --- a/acquire/collector.py +++ b/acquire/collector.py @@ -230,7 +230,7 @@ def unbind(self) -> None: def close(self) -> None: self.output.close() - def _create_output_path(self, path: Path, base: Optional[str] = None) -> str: + def _get_output_path(self, path: Path, base: Optional[str] = None) -> str: base = base or self.base outpath = str(path) @@ -299,14 +299,14 @@ def collect_file( log.info("- Collecting file %s: Skipped (DEDUP)", path) return - outpath = self._create_output_path(outpath or path, base) + outpath = self._get_output_path(outpath or path, base) try: entry = path.get() if volatile: - self.output.write_volatile(outpath, entry, size) + self.output.write_volatile(outpath, entry, size=size) else: - self.output.write_entry(outpath, entry, size) + self.output.write_entry(outpath, entry, size=size) self.report.add_file_collected(module_name, path) result = "OK" @@ -322,8 +322,8 @@ def collect_file( def collect_symlink(self, path: fsutil.TargetPath, module_name: Optional[str] = None) -> None: try: - outpath = self._create_output_path(path) - self.output.write_bytes(outpath, b"", path.get(), 0) + outpath = self._get_output_path(path) + self.output.write_entry(outpath, path.get()) self.report.add_symlink_collected(module_name, path) result = "OK" @@ -359,11 +359,17 @@ def collect_dir( return seen_paths.add(resolved) + dir_is_empty = True for entry in path.iterdir(): + dir_is_empty = False self.collect_path( entry, seen_paths=seen_paths, module_name=module_name, follow=follow, volatile=volatile ) + if dir_is_empty and volatile: + outpath = self._get_output_path(path) + self.output.write_entry(outpath, entry) + except OSError as error: if error.errno == errno.ENOENT: self.report.add_dir_missing(module_name, path) @@ -485,7 +491,7 @@ def collect_command_output( return def write_bytes(self, destination_path: str, data: bytes) -> None: - self.output.write_bytes(destination_path, data, None) + self.output.write_bytes(destination_path, data) self.report.add_file_collected(self.bound_module_name, destination_path) diff --git a/acquire/outputs/base.py b/acquire/outputs/base.py index 8a883108..2e469eee 100644 --- a/acquire/outputs/base.py +++ b/acquire/outputs/base.py @@ -2,7 +2,6 @@ from pathlib import Path from typing import BinaryIO, Optional, Union -from dissect.target import Target from dissect.target.filesystem import FilesystemEntry import acquire.utils @@ -12,12 +11,9 @@ class Output: """Base class to implement acquire output formats with. New output formats must sub-class this class. - - Args: - target: The target that we're using acquire on. """ - def init(self, target: Target): + def init(self, path: Path, **kwargs) -> None: pass def write( @@ -27,12 +23,12 @@ def write( entry: Optional[Union[FilesystemEntry, Path]], size: Optional[int] = None, ) -> None: - """Write a filesystem entry or file-like object to the implemented output type. + """Write a file-like object to the output. Args: - output_path: The path of the entry in the output format. + output_path: The path of the entry in the output. fh: The file-like object of the entry to write. - entry: The optional filesystem entry of the entry to write. + entry: The optional filesystem entry to write. size: The optional file size in bytes of the entry to write. """ raise NotImplementedError() @@ -40,18 +36,21 @@ def write( def write_entry( self, output_path: str, - entry: Optional[Union[FilesystemEntry, Path]], + entry: Union[FilesystemEntry, Path], size: Optional[int] = None, ) -> None: - """Write a filesystem entry to the output format. + """Write a filesystem entry to the output. Args: - output_path: The path of the entry in the output format. - entry: The optional filesystem entry of the entry to write. + output_path: The path of the entry in the output. + entry: The filesystem entry to write. size: The optional file size in bytes of the entry to write. """ - with entry.open() as fh: - self.write(output_path, fh, entry, size) + if entry.is_dir() or entry.is_symlink(): + self.write_bytes(output_path, b"", entry, size=0) + else: + with entry.open() as fh: + self.write(output_path, fh, entry, size=size) def write_bytes( self, @@ -63,27 +62,28 @@ def write_bytes( """Write raw bytes to the output format. Args: - output_path: The path of the entry in the output format. + output_path: The path of the entry in the output. data: The raw bytes to write. - entry: The optional filesystem entry of the entry to write. + entry: The optional filesystem entry to write. size: The optional file size in bytes of the entry to write. """ stream = io.BytesIO(data) - self.write(output_path, stream, entry, size) + self.write(output_path, stream, entry, size=size) def write_volatile( self, output_path: str, - entry: Optional[Union[FilesystemEntry, Path]], + entry: Union[FilesystemEntry, Path], size: Optional[int] = None, ) -> None: - """Write specified path to the output format. + """Write a filesystem entry to the output. + Handles files that live in volatile filesystems. Such as procfs and sysfs. Args: - output_path: The path of the entry in the output format. - entry: The optional filesystem entry of the entry to write. + output_path: The path of the entry in the output. + entry: The filesystem entry to write. size: The optional file size in bytes of the entry to write. """ try: @@ -96,7 +96,7 @@ def write_volatile( buf = b"" size = 0 - self.write_bytes(output_path, buf, entry, size) + self.write_bytes(output_path, buf, entry, size=size) def close(self) -> None: """Closes the output.""" diff --git a/acquire/outputs/dir.py b/acquire/outputs/dir.py index 2e165067..4f40ba5f 100644 --- a/acquire/outputs/dir.py +++ b/acquire/outputs/dir.py @@ -13,18 +13,39 @@ def __init__(self, path: Path, **kwargs): self.path = path def write( - self, output_path: str, fh: BinaryIO, entry: Optional[Union[FilesystemEntry, Path]], size: Optional[int] = None + self, + output_path: str, + fh: BinaryIO, + entry: Optional[Union[FilesystemEntry, Path]], + size: Optional[int] = None, ) -> None: + """Write a file-like object to a directory. + + The data from ``fh`` is written, while ``entry`` is used to get some properties of the file. + + On Windows platforms ``:`` is replaced with ``_`` in the output_path. + + Args: + output_path: The path of the entry in the output. + fh: The file-like object of the entry to write. + entry: The optional filesystem entry to write. + size: The optional file size in bytes of the entry to write. + """ if platform.system() == "Windows": output_path = output_path.replace(":", "_") out_path = self.path.joinpath(output_path) - out_dir = out_path.parent - if not out_dir.exists(): - out_dir.mkdir(parents=True) - with out_path.open("wb") as fhout: - shutil.copyfileobj(fh, fhout) + if entry and entry.is_dir(): + out_dir = out_path + out_dir.mkdir(parents=True, exist_ok=True) + + else: + out_dir = out_path.parent + out_dir.mkdir(parents=True, exist_ok=True) + + with out_path.open("wb") as fhout: + shutil.copyfileobj(fh, fhout) def close(self) -> None: pass diff --git a/acquire/outputs/tar.py b/acquire/outputs/tar.py index 9f93449b..6d50ebce 100644 --- a/acquire/outputs/tar.py +++ b/acquire/outputs/tar.py @@ -52,12 +52,14 @@ def write( entry: Optional[Union[FilesystemEntry, Path]], size: Optional[int] = None, ) -> None: - """Write a filesystem entry or file-like object to a tar file. + """Write a file-like object to a tar file. + + The data from ``fh`` is written, while ``entry`` is used to get some properties of the file. Args: - output_path: The path of the entry in the output format. + output_path: The path of the entry in the output. fh: The file-like object of the entry to write. - entry: The optional filesystem entry of the entry to write. + entry: The optional filesystem entry to write. size: The optional file size in bytes of the entry to write. """ stat = None @@ -79,6 +81,8 @@ def write( if entry.is_symlink(): info.type = tarfile.SYMTYPE info.linkname = entry.readlink() + elif entry.is_dir(): + info.type = tarfile.DIRTYPE stat = entry.lstat()