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
148 changes: 148 additions & 0 deletions admin/test/scripts/test_lava_db_source_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""Pin what an artifact that declares no search paths records as its source path.

An artifact whose `paths` is None searches for nothing. The main script hands it
'<report folder>/_lava_artifacts.db' as its only files_found entry, because it reads
the rows an earlier artifact wrote into the LAVA database rather than any file in the
extraction. All 35 such artifacts in iLEAPP are in logarchive.py, and each returns that
path back as its source_path.

`Context.get_relative_path` used to strip the data folder and nothing else. The LAVA
database sits in the report folder, one level above the data folder, so the prefix never
matched and the path was returned unchanged. The examiner's own output directory then
reached the artifact page's "located at" line and the LAVA manifest's `source_path`,
which travels with the report.

Measured on a real run before the fix: 4 of 5 manifest entries and all 3 artifact pages
carried the absolute path.

These tests drive `Context.get_relative_path` directly with both folders set the way a
run sets them, so they fail on the unfixed function rather than on a recorded baseline.
"""
import os
import pathlib
import sys
import unittest

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

from scripts.context import Context # pylint: disable=wrong-import-position

# The layout OutputParameters builds: the data folder is inside the report folder.
REPORT = os.path.join('/Users', 'examiner', 'Cases', 'iLEAPP_Output_2026')
DATA = os.path.join(REPORT, 'data')
LAVA_DB = os.path.join(REPORT, '_lava_artifacts.db')


class RelativePathTestCase(unittest.TestCase):
"""Both folders set the way a run sets them."""

def setUp(self):
Context.clear()
Context._data_folder = DATA # pylint: disable=protected-access
Context._output_folder_base = REPORT # pylint: disable=protected-access

def tearDown(self):
Context._data_folder = None # pylint: disable=protected-access
Context._output_folder_base = None # pylint: disable=protected-access
Context.clear()


class TestTheLavaDatabaseIsReportedByName(RelativePathTestCase):
"""The file the runner hands a paths-None artifact."""

def test_the_lava_database_loses_the_examiners_report_folder(self):
self.assertEqual(Context.get_relative_path(LAVA_DB), '_lava_artifacts.db')

def test_no_part_of_the_report_folder_survives(self):
got = Context.get_relative_path(LAVA_DB)
self.assertNotIn(REPORT, got)
self.assertFalse(os.path.isabs(got), f'still absolute: {got!r}')

def test_another_file_the_run_writes_keeps_its_place_inside_the_report(self):
page = os.path.join(REPORT, '_HTML', 'logarchive_wifi_status.html')
self.assertEqual(Context.get_relative_path(page),
os.path.join('_HTML', 'logarchive_wifi_status.html'))


class TestTheDataFolderStillWins(RelativePathTestCase):
"""The data folder is inside the report folder, so it has to be stripped first.

Stripping the report folder first would leave 'data/' on the front of every staged
evidence path, which is the regression this ordering exists to prevent.
"""

def test_a_staged_evidence_file_is_reported_without_the_data_prefix(self):
staged = os.path.join(DATA, 'private', 'var', 'mobile', 'Library', 'x.db')
self.assertEqual(Context.get_relative_path(staged),
os.path.join('private', 'var', 'mobile', 'Library', 'x.db'))

def test_a_staged_path_does_not_come_back_prefixed_with_data(self):
staged = os.path.join(DATA, 'private', 'var', 'x.db')
got = Context.get_relative_path(staged)
self.assertFalse(got.startswith('data'), f'data prefix survived: {got!r}')

def test_several_staged_paths_in_one_string_are_all_reduced(self):
joined = '\n'.join([os.path.join(DATA, 'a', 'one.db'),
os.path.join(DATA, 'b', 'two.db')])
self.assertEqual(Context.get_relative_path(joined),
'\n'.join([os.path.join('a', 'one.db'),
os.path.join('b', 'two.db')]))


class TestNothingElseChanged(RelativePathTestCase):
"""Values that carry neither prefix are still handed back untouched."""

def test_a_path_outside_both_folders_is_unchanged(self):
other = os.path.join('/Users', 'examiner', 'Desktop', 'notes.txt')
self.assertEqual(Context.get_relative_path(other), other)

def test_an_already_relative_path_is_unchanged(self):
self.assertEqual(Context.get_relative_path('export/logarchive.json'),
'export/logarchive.json')

def test_an_empty_value_is_unchanged(self):
self.assertEqual(Context.get_relative_path(''), '')
self.assertIsNone(Context.get_relative_path(None))


class TestWithNeitherFolderKnown(unittest.TestCase):
"""The committed harness sets no folders, so the function stays a no-op there."""

def setUp(self):
Context.clear()
Context._data_folder = None # pylint: disable=protected-access
Context._output_folder_base = None # pylint: disable=protected-access

def test_every_path_is_returned_unchanged(self):
self.assertEqual(Context.get_relative_path(LAVA_DB), LAVA_DB)

def test_the_report_folder_alone_is_enough_to_reduce_the_lava_database(self):
Context._output_folder_base = REPORT # pylint: disable=protected-access
try:
self.assertEqual(Context.get_relative_path(LAVA_DB), '_lava_artifacts.db')
finally:
Context._output_folder_base = None # pylint: disable=protected-access


class TestTheRunPopulatesTheReportFolder(unittest.TestCase):
"""set_output_params has to record the base, or the strip above never fires."""

def tearDown(self):
Context._output_params = None # pylint: disable=protected-access
Context._data_folder = None # pylint: disable=protected-access
Context._output_folder_base = None # pylint: disable=protected-access

def test_both_folders_are_taken_from_the_output_parameters(self):
class FakeOutputParameters: # pylint: disable=too-few-public-methods
output_folder_base = REPORT
data_folder = DATA

Context.set_output_params(FakeOutputParameters())
self.assertEqual(Context._output_folder_base, REPORT) # pylint: disable=protected-access
self.assertEqual(Context._data_folder, DATA) # pylint: disable=protected-access
self.assertEqual(Context.get_relative_path(LAVA_DB), '_lava_artifacts.db')


if __name__ == '__main__':
unittest.main()
48 changes: 35 additions & 13 deletions scripts/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Context:
_files_found = []
_filename_lookup_map = {}
_data_folder = None
_output_folder_base = None
_metadata = {}
_installed_os_version = ""
# Run-level, like the output parameters: set once from the CLI or GUI and
Expand All @@ -46,6 +47,8 @@ def set_output_params(output_params):
"""
Context._output_params = output_params
Context._data_folder = getattr(output_params, 'data_folder', None)
Context._output_folder_base = getattr(
output_params, 'output_folder_base', None)

@staticmethod
def set_data_folder(data_folder):
Expand Down Expand Up @@ -544,27 +547,46 @@ def _normalize_source_path(source_path):
@staticmethod
def get_relative_path(full_path):
"""
Converts a full on-disk path (from files_found) to a relative
extraction path by removing the global data_folder prefix.
Converts a full on-disk path into one that carries none of the
examiner's own filesystem layout.

Two prefixes are stripped, in this order:

1. the data folder, where the seeker stages evidence, so a staged
file is reported by its path inside the extraction;
2. the report folder, so a file the run itself writes is reported by
its path inside the report.

The second case exists for artifacts that declare no search paths. The
main script hands those '<report folder>/_lava_artifacts.db' as their
source file, because they read the rows a previous artifact wrote
rather than any file in the extraction. That path is outside the data
folder, so before this it was reported unchanged and the examiner's own
report directory reached both the "located at" line and the LAVA
manifest's source_path.

The data folder is tried first because it sits inside the report
folder: stripping the report folder first would leave every staged
evidence path prefixed with 'data/'.

Args:
full_path (str): The full path to the file.

Returns:
str: The relative extraction path, or the original path if
the data_folder is not available.
str: The relative path, or the original path if neither prefix is
known or present.
"""
if not full_path or not Context._data_folder:
if not full_path:
return full_path

if Context._data_folder in full_path:
# Strip the base path everywhere it appears, including inside path
# strings concatenated with arbitrary separators (', ', '; ', ...)
base = Context._data_folder
return (full_path.replace(base + '/', '')
.replace(base + '\\', '')
.replace(base, '')
.lstrip('/\\'))
for base in (Context._data_folder, Context._output_folder_base):
if base and base in full_path:
# Strip the base path everywhere it appears, including inside path
# strings concatenated with arbitrary separators (', ', '; ', ...)
return (full_path.replace(base + '/', '')
.replace(base + '\\', '')
.replace(base, '')
.lstrip('/\\'))

return full_path

Expand Down
Loading