diff --git a/src/mvt/android/modules/androidqf/base.py b/src/mvt/android/modules/androidqf/base.py index b0304d0c..93f16800 100644 --- a/src/mvt/android/modules/androidqf/base.py +++ b/src/mvt/android/modules/androidqf/base.py @@ -46,8 +46,22 @@ class AndroidQFModule(MVTModule): self.archive = archive self.files = files - def _get_files_by_pattern(self, pattern: str): - return fnmatch.filter(self.files, pattern) + def _get_files_by_pattern(self, pattern: str) -> List[str]: + pattern = pattern.replace("\\", "/") + patterns = [pattern] + # ZIPs may store artifacts at their root, without the acquisition + # directory prefix present in paths collected by from_dir(). + if pattern.startswith("*/"): + patterns.append(pattern[2:]) + + return [ + file_path + for file_path in self.files + if any( + fnmatch.fnmatch(file_path.replace("\\", "/"), candidate) + for candidate in patterns + ) + ] def _get_device_timezone(self): """ diff --git a/tests/android_androidqf/test_base.py b/tests/android_androidqf/test_base.py new file mode 100644 index 00000000..c36ab764 --- /dev/null +++ b/tests/android_androidqf/test_base.py @@ -0,0 +1,60 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2026 The MVT Authors. +# Use of this software is governed by the MVT License 1.1 that can be found at +# https://license.mvt.re/1.1/ + +import zipfile + +import pytest + +from mvt.android.modules.androidqf.base import AndroidQFModule + + +@pytest.mark.parametrize("prefix", ["", "./", "acquisition/", "outer/acquisition/"]) +@pytest.mark.parametrize( + "pattern, filename", + [ + ("*/packages.json", "packages.json"), + ("*/getprop.txt", "getprop.txt"), + ("*/ps.txt", "ps.txt"), + ("*/settings_*.txt", "settings_global.txt"), + ("*/files.json", "files.json"), + ("*/logs/*", "logs/logcat.txt"), + ("*/mounts.json", "mounts.json"), + ("*/root_binaries.json", "root_binaries.json"), + ], +) +def test_artifact_lookup_preserves_archive_names(tmp_path, prefix, pattern, filename): + member = prefix + filename + with zipfile.ZipFile(tmp_path / "acquisition.zip", "w") as archive: + archive.writestr(member, b"artifact") + archive.writestr(prefix + "unrelated.txt", b"unrelated") + module = AndroidQFModule() + module.from_zip(archive, archive.namelist()) + + assert module._get_files_by_pattern(pattern) == [member] + assert module._get_file_content(member) == b"artifact" + + +def test_artifact_lookup_preserves_windows_paths(): + module = AndroidQFModule() + module.from_dir("parent", [r"acquisition\logs\logcat.txt"]) + + assert module._get_files_by_pattern("*/logs/*") == [r"acquisition\logs\logcat.txt"] + + +def test_artifact_lookup_preserves_order_without_duplicates(): + module = AndroidQFModule() + module.files = [ + "acquisition/settings_global.txt", + "settings_secure.txt", + "./settings_system.txt", + "not_settings_global.txt", + "settings_global.txt.bak", + ] + + assert module._get_files_by_pattern("*/settings_*.txt") == module.files[:3] + assert module._get_files_by_pattern("settings_secure.txt") == [ + "settings_secure.txt" + ] + assert module._get_files_by_pattern("*/missing.txt") == [] diff --git a/tests/test_check_android_androidqf.py b/tests/test_check_android_androidqf.py index 65973109..e28e97ba 100644 --- a/tests/test_check_android_androidqf.py +++ b/tests/test_check_android_androidqf.py @@ -10,6 +10,7 @@ import shutil import tempfile import zipfile +import pytest from click.testing import CliRunner from mvt.android.cli import check_androidqf @@ -23,6 +24,44 @@ from .utils import get_artifact_folder TEST_BACKUP_PASSWORD = "123456" +@pytest.mark.parametrize( + "layout", ["directory", "", "./", "acquisition/", "outer/acquisition/"] +) +def test_androidqf_property_detection_across_layouts( + tmp_path, indicators_factory, monkeypatch, layout +): + monkeypatch.setattr(settings, "NETWORK_ACCESS_ALLOWED", False) + content = ( + "[persist.sys.timezone]: [Europe/Berlin]\n" + "[test.suspicious.property]: [enabled]\n" + ) + if layout == "directory": + target = tmp_path / "acquisition" + target.mkdir() + (target / "getprop.txt").write_text(content) + else: + target = tmp_path / "acquisition.zip" + with zipfile.ZipFile(target, "w") as archive: + archive.writestr(layout + "getprop.txt", content) + + command = CmdAndroidCheckAndroidQF( + target_path=str(target), + module_name="AQFGetProp", + iocs=indicators_factory(android_property_names=["test.suspicious.property"]), + ) + command.run() + + assert len(command.executed) == 1 + module = command.executed[0] + assert module.results == [ + {"name": "persist.sys.timezone", "value": "Europe/Berlin"}, + {"name": "test.suspicious.property", "value": "enabled"}, + ] + assert module._get_device_timezone() == "Europe/Berlin" + assert len(command.alertstore.alerts) == 1 + assert command.alertstore.alerts[0].event == module.results[1] + + class TestCheckAndroidqfCommand: def test_log_timestamps_module_is_registered(self): assert AQFLogTimestamps in ANDROIDQF_MODULES