Fix AndroidQF artifact discovery in root-level ZIP archives

This commit is contained in:
Janik Besendorf committed 2026-09-28 16:23:33 +02:00
1 parent fd37c4d8d9
commit 0c0b1b977e
3 files changed
+115 -2

No files matched your search

+16 -2
View File
@@ -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):
"""
+60
View File
@@ -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") == []
+39
View File
@@ -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