Skip to content

Commit

Permalink
Collect empty directories when the source is volatile
Browse files Browse the repository at this point in the history
(DIS-1931)
  • Loading branch information
pyrco committed Aug 23, 2023
1 parent 27ee680 commit 36fac3f
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 114 deletions.
4 changes: 2 additions & 2 deletions acquire/acquire.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Check warning on line 554 in acquire/acquire.py

View check run for this annotation

Codecov / codecov/patch

acquire/acquire.py#L553-L554

Added lines #L553 - L554 were not covered by tests
collector.report.add_command_collected(cls.__name__, command_parts)
mem_dump_path.unlink()
mem_dump_errors_path.unlink()
Expand Down
20 changes: 13 additions & 7 deletions acquire/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Check warning on line 307 in acquire/collector.py

View check run for this annotation

Codecov / codecov/patch

acquire/collector.py#L307

Added line #L307 was not covered by tests
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"
Expand All @@ -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"
Expand Down Expand Up @@ -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, path)

except OSError as error:
if error.errno == errno.ENOENT:
self.report.add_dir_missing(module_name, path)
Expand Down Expand Up @@ -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)

Check warning on line 494 in acquire/collector.py

View check run for this annotation

Codecov / codecov/patch

acquire/collector.py#L494

Added line #L494 was not covered by tests
self.report.add_file_collected(self.bound_module_name, destination_path)


Expand Down
48 changes: 24 additions & 24 deletions acquire/outputs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,18 @@
from pathlib import Path
from typing import BinaryIO, Optional, Union

from dissect.target import Target
from dissect.target.filesystem import FilesystemEntry

import acquire.utils
from acquire.volatilestream import VolatileStream


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(
Expand All @@ -27,31 +23,34 @@ 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()

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,
Expand All @@ -63,31 +62,32 @@ 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:
fh = acquire.utils.VolatileStream(Path(entry.path))
fh = VolatileStream(Path(entry.path))

Check warning on line 90 in acquire/outputs/base.py

View check run for this annotation

Codecov / codecov/patch

acquire/outputs/base.py#L90

Added line #L90 was not covered by tests
buf = fh.read()
size = size or len(buf)
except (OSError, PermissionError):
Expand All @@ -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)

Check warning on line 99 in acquire/outputs/base.py

View check run for this annotation

Codecov / codecov/patch

acquire/outputs/base.py#L99

Added line #L99 was not covered by tests

def close(self) -> None:
"""Closes the output."""
Expand Down
35 changes: 28 additions & 7 deletions acquire/outputs/dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
out_path = self.path.joinpath(output_path.lstrip("/"))

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)
with out_path.open("wb") as fhout:
shutil.copyfileobj(fh, fhout)

def close(self) -> None:
pass
10 changes: 7 additions & 3 deletions acquire/outputs/tar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()

Expand Down
66 changes: 1 addition & 65 deletions acquire/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,83 +4,19 @@
import getpass
import json
import os
import pathlib
import re
import sys
import textwrap
import traceback
from enum import Enum
from io import SEEK_SET, UnsupportedOperation
from pathlib import Path
from stat import S_IRGRP, S_IROTH, S_IRUSR
from typing import Any, Optional

from dissect.target import Target
from dissect.util.stream import AlignedStream

from acquire.outputs import OUTPUTS
from acquire.uploaders.plugin_registry import UploaderRegistry

try:
# Windows systems do not have the fcntl module.
from fcntl import F_SETFL, fcntl

HAS_FCNTL = True
except ImportError:
HAS_FCNTL = False


class VolatileStream(AlignedStream):
"""Streaming class to handle various procfs and sysfs edge-cases. Backed by `AlignedStream`.
Args:
path: Path of the file to obtain a file-handle from.
mode: Mode string to open the file-handle with. Such as "rt" and "rb".
flags: Flags to open the file-descriptor with.
size: The maximum size of the stream. None if unknown.
"""

def __init__(
self,
path: Path,
mode: str = "rb",
# Windows and Darwin systems don't have O_NOATIME or O_NONBLOCK. Add them if they are available.
flags: int = (os.O_RDONLY | getattr(os, "O_NOATIME", 0) | getattr(os, "O_NONBLOCK", 0)),
size: int = 1024 * 1024 * 5,
):
self.fh = path.open(mode)
self.fd = self.fh.fileno()

if HAS_FCNTL:
fcntl(self.fd, F_SETFL, flags)

st_mode = os.fstat(self.fd).st_mode
write_only = (st_mode & (S_IRUSR | S_IRGRP | S_IROTH)) == 0 # novermin

super().__init__(0 if write_only else size)

def seek(self, pos: int, whence: int = SEEK_SET) -> int:
raise UnsupportedOperation("VolatileStream is not seekable")

def seekable(self) -> bool:
return False

def _read(self, offset: int, length: int) -> bytes:
result = []
while length:
try:
buf = os.read(self.fd, min(length, self.size - offset))
except BlockingIOError:
break

if not buf:
break

result.append(buf)
offset += len(buf)
length -= len(buf)
return b"".join(result)


class StrEnum(str, Enum):
"""Sortable and serializible string-based enum"""
Expand Down Expand Up @@ -400,7 +336,7 @@ def persist_execution_report(path: Path, report_data: dict) -> Path:
SYSVOL_SUBST = re.compile(r"^(/\?\?/)?[cC]:")


def normalize_path(target: Target, path: pathlib.Path, resolve: bool = False) -> str:
def normalize_path(target: Target, path: Path, resolve: bool = False) -> str:
if resolve:
path = path.resolve()

Expand Down
Loading

0 comments on commit 36fac3f

Please sign in to comment.