Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/windows_smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,9 @@ jobs:

- name: Open SQLite on a path longer than 260 characters
run: python admin/test/scripts/test_sqlite_longpath_uri.py

# The entry points hand a directory input to the seeker with the extended-length
# prefix on Windows, so the recorded evidence path and the staged path are
# checked here with that prefix as well as without it.
- name: Stage from a directory input with and without the extended-length prefix
run: python admin/test/scripts/test_seeker_source_path.py
101 changes: 101 additions & 0 deletions admin/test/scripts/test_seeker_source_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Pin what the directory and single-file seekers record in file_infos.

The zip, tar, raw image and iTunes seekers record where a file sits in the
evidence. The directory seeker used to record the absolute path on the
examiner's machine and the single-file seeker its absolute input path, so the
LAVA file list, every media source path and any artifact that reads file_infos
carried the examiner's folder layout whenever the input was a directory or a
single file (iLEAPP issue #2057).

The directory seeker also built each staged path by slicing the input path off
the front of the match and dropping one more character. An input ending in a
separator lost the first letter of every staged path ('rivate/var/...') and an
input of '.' removed every dot from every staged path ('Cachedb').

These tests build a small evidence tree and run the real seekers against it,
with the input given as an absolute path, that path with a trailing separator,
a relative path and '.'. On Windows the absolute form is also tried with the
extended-length prefix the entry points add for a directory input.
"""
import os
import pathlib
import shutil
import sys
import tempfile
import unittest

REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
sys.path.insert(0, str(REPO_ROOT))

from scripts.search_files import FileSeekerDir, FileSeekerFile # pylint: disable=wrong-import-position

REL = 'private/var/mobile/Library/Preferences/com.apple.MobileSMS.plist'
CONTENT = b'stand-in plist bytes'
PATTERN = '*/mobile/Library/Preferences/com.apple.MobileSMS.plist'


class TestDirectoryAndSingleFileSourcePaths(unittest.TestCase):
"""The recorded source path and the staged path, per input form."""

def setUp(self):
self.tmp = tempfile.mkdtemp(prefix='leapp_seeker_src_')
self.root = os.path.join(self.tmp, 'extraction')
self.evidence_file = os.path.join(self.root, *REL.split('/'))
os.makedirs(os.path.dirname(self.evidence_file))
with open(self.evidence_file, 'wb') as fout:
fout.write(CONTENT)
self.cwd = os.getcwd()

def tearDown(self):
os.chdir(self.cwd)
shutil.rmtree(self.tmp, ignore_errors=True)

def _data_folder(self, label):
folder = os.path.join(self.tmp, 'data_' + label)
os.makedirs(folder)
return folder

def _check_directory_input(self, label, directory):
data_folder = self._data_folder(label)
seeker = FileSeekerDir(directory, data_folder)
found = seeker.search(PATTERN)
self.assertEqual(len(found), 1, (label, found))
staged = found[0]
self.assertEqual(seeker.file_infos[staged].source_path, REL, label)
self.assertEqual(os.path.relpath(staged, data_folder).replace(os.sep, '/'), REL, label)
with open(staged, 'rb') as fin:
self.assertEqual(fin.read(), CONTENT, label)

def test_absolute_directory_input(self):
self._check_directory_input('absolute', self.root)

def test_directory_input_with_a_trailing_separator(self):
self._check_directory_input('trailing', self.root + os.sep)

def test_relative_directory_input(self):
os.chdir(self.tmp)
self._check_directory_input('relative', 'extraction')

def test_dot_as_the_directory_input(self):
os.chdir(self.root)
self._check_directory_input('dot', '.')

@unittest.skipUnless(os.name == 'nt', 'the extended-length prefix is a Windows path form')
def test_extended_length_prefix_on_windows(self):
prefixed = '\\\\?\\' + self.root.replace('/', '\\')
self._check_directory_input('prefixed', prefixed)
self._check_directory_input('prefixed_trailing', prefixed + '\\')

def test_single_file_input_records_the_file_name(self):
data_folder = self._data_folder('file')
seeker = FileSeekerFile(self.evidence_file, data_folder)
found = seeker.search(PATTERN)
self.assertEqual(len(found), 1, found)
self.assertEqual(seeker.file_infos[found[0]].source_path, 'com.apple.MobileSMS.plist')
self.assertEqual(os.path.basename(found[0]), 'com.apple.MobileSMS.plist')
with open(found[0], 'rb') as fin:
self.assertEqual(fin.read(), CONTENT)


if __name__ == '__main__':
unittest.main()
26 changes: 18 additions & 8 deletions scripts/search_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ class FileInfo:
"""
A class to store file metadata information.
Attributes:
source_path (str): The full path to the source file.
source_path (str): Where the file sits in the evidence: an archive
member name as stored, a path relative to the input directory, a
volume path inside a raw image, or the file name for a single-file
input. Never a path on the examiner's machine.
creation_date (datetime): The date and time when the file was created.
modification_date (datetime): The date and time when the file was last modified.
"""
Expand Down Expand Up @@ -250,23 +253,28 @@ def search(self, filepattern, return_on_first_hit=False, force=False):
root = normcase("root/")
for item in self._all_files:
if pat(root + normcase(item)) is not None:
item_rel_path = item.replace(self.directory, '')
data_path = os.path.join(self.data_folder, item_rel_path[1:])
if is_platform_windows():
data_path = data_path.replace('/', '\\')
# Relative to the input root, so the staged tree and the recorded
# source path do not depend on where the extraction sits on the
# examiner's machine, on a trailing separator in the input path or
# on the \\?\ prefix the entry points add on Windows. The former
# prefix slice dropped the first character of every staged path
# after a trailing separator, and every dot when the input was '.'.
item_rel_path = os.path.relpath(item, self.directory)
source_path = item_rel_path.replace('\\', '/')
data_path = os.path.join(self.data_folder, item_rel_path)
if item not in self.copied or force:
try:
if os.path.isdir(item):
pass
elif os.path.isfile(item):
data_path = self._unique_data_path(
data_path, item, hash_source=item_rel_path)
data_path, item, hash_source=source_path)
os.makedirs(os.path.dirname(data_path), exist_ok=True)
copy2(item, data_path)
self.copied[item] = data_path
creation_date = Path(item).stat().st_ctime
modification_date = Path(item).stat().st_mtime
file_info = FileInfo(item, creation_date, modification_date)
file_info = FileInfo(source_path, creation_date, modification_date)
self.file_infos[data_path] = file_info
else:
logfunc(f"INFO: Item '{item}' is neither a file nor a directory "
Expand Down Expand Up @@ -729,7 +737,9 @@ def search(self, filepattern, return_on_first_hit=False, force=False):
copy2(self.single_file_abs_path, dest_data_path)
self.copied[self.single_file_abs_path] = dest_data_path
s = Path(self.single_file_abs_path).stat()
file_info_obj = FileInfo(self.single_file_abs_path, s.st_ctime, s.st_mtime)
# The file name is all that places this input in the evidence;
# the directory it came from is the examiner's, not the device's.
file_info_obj = FileInfo(self.single_file_basename, s.st_ctime, s.st_mtime)
self.file_infos[dest_data_path] = file_info_obj
found_data_paths.append(dest_data_path)
# logfunc(f"FileSeekerFile: Matched and copied. Dest: {dest_data_path}")
Expand Down
Loading