mirror of
https://github.com/mvt-project/mvt.git
synced 2026-10-31 10:16:21 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c0b1b977e |
No files matched your search
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -48,17 +48,12 @@ class DumpsysAccessibility(DumpsysAccessibilityArtifact, BugReportModule):
|
||||
)
|
||||
self.parse(content)
|
||||
|
||||
listed = unnamed = enabled_count = 0
|
||||
listed = unnamed = 0
|
||||
for result in self.results:
|
||||
if result.get("component"):
|
||||
listed += 1
|
||||
enabled = result.get("enabled")
|
||||
if enabled is True:
|
||||
enabled_count += 1
|
||||
self.log.info(
|
||||
'Found accessibility service "%s" (enabled: %s)',
|
||||
result.get("component"),
|
||||
"not stated" if enabled is None else enabled,
|
||||
'Found accessibility service "%s"', result.get("component")
|
||||
)
|
||||
continue
|
||||
# The operator gets this per user as a LOW alert from
|
||||
@@ -67,9 +62,8 @@ class DumpsysAccessibility(DumpsysAccessibilityArtifact, BugReportModule):
|
||||
unnamed += result.get("unnamed_service_count") or 0
|
||||
|
||||
self.log.info(
|
||||
"Identified a total of %d accessibility services, %d reported enabled "
|
||||
"(%d more stated by the dump without a component name)",
|
||||
"Identified a total of %d accessibility services (%d more stated by "
|
||||
"the dump without a component name)",
|
||||
listed,
|
||||
enabled_count,
|
||||
unnamed,
|
||||
)
|
||||
@@ -46,23 +46,6 @@ class TestDumpsysAccessibilityArtifact:
|
||||
# "not stated" into "not installed", so it reads None here.
|
||||
assert da.results[0]["installed"] is None
|
||||
|
||||
def test_parsing_installed_and_enabled(self):
|
||||
da = DumpsysAccessibilityArtifact()
|
||||
with open(get_artifact("android_data/dumpsys_accessibility_enabled.txt")) as f:
|
||||
da.parse(f.read())
|
||||
|
||||
assert len(da.results) == 5
|
||||
assert all(result["component"] for result in da.results)
|
||||
assert all(result["installed"] is True for result in da.results)
|
||||
enabled = [result for result in da.results if result["enabled"] is True]
|
||||
assert len(enabled) == 1
|
||||
assert enabled[0]["package_name"] == "com.samsung.accessibility"
|
||||
assert (
|
||||
enabled[0]["component"]
|
||||
== "com.samsung.accessibility/.universalswitch.UniversalSwitchService"
|
||||
)
|
||||
assert sum(result["enabled"] is False for result in da.results) == 4
|
||||
|
||||
def test_accessibility_service_alert(self):
|
||||
da = DumpsysAccessibilityArtifact()
|
||||
file = get_artifact("android_data/dumpsys_accessibility_v14_or_later.txt")
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-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 logging
|
||||
|
||||
from mvt.android.modules.bugreport.dumpsys_accessibility import DumpsysAccessibility
|
||||
|
||||
from ..utils import get_artifact
|
||||
|
||||
|
||||
def _run(content, monkeypatch, caplog):
|
||||
module = DumpsysAccessibility()
|
||||
dumpstate = f"DUMP OF SERVICE accessibility:\n{content}".encode()
|
||||
monkeypatch.setattr(module, "_get_dumpstate_file", lambda: dumpstate)
|
||||
with caplog.at_level(logging.INFO):
|
||||
module.run()
|
||||
|
||||
|
||||
def test_logs_mixed_enabled_states(monkeypatch, caplog):
|
||||
with open(get_artifact("android_data/dumpsys_accessibility_enabled.txt")) as handle:
|
||||
_run(handle.read(), monkeypatch, caplog)
|
||||
|
||||
assert (
|
||||
'Found accessibility service "com.samsung.accessibility/'
|
||||
'.universalswitch.UniversalSwitchService" (enabled: True)' in caplog.messages
|
||||
)
|
||||
assert sum("(enabled: False)" in message for message in caplog.messages) == 4
|
||||
assert (
|
||||
"Identified a total of 5 accessibility services, 1 reported enabled "
|
||||
"(0 more stated by the dump without a component name)" in caplog.messages
|
||||
)
|
||||
|
||||
|
||||
def test_logs_enabled_count_alongside_unnamed_count(monkeypatch, caplog):
|
||||
with open(
|
||||
get_artifact("android_data/dumpsys_accessibility_v14_or_later.txt")
|
||||
) as handle:
|
||||
_run(handle.read(), monkeypatch, caplog)
|
||||
|
||||
assert (
|
||||
"Identified a total of 1 accessibility services, 1 reported enabled "
|
||||
"(1 more stated by the dump without a component name)" in caplog.messages
|
||||
)
|
||||
assert (
|
||||
sum("Found accessibility service" in message for message in caplog.messages)
|
||||
== 1
|
||||
)
|
||||
|
||||
|
||||
def test_logs_unknown_enabled_state_without_treating_it_as_disabled(
|
||||
monkeypatch, caplog
|
||||
):
|
||||
_run(
|
||||
"""User state[attributes:{id=0, installedServiceCount=2}
|
||||
installed services: {
|
||||
0 : com.example/.Service
|
||||
}
|
||||
""",
|
||||
monkeypatch,
|
||||
caplog,
|
||||
)
|
||||
|
||||
assert (
|
||||
'Found accessibility service "com.example/.Service" (enabled: not stated)'
|
||||
in caplog.messages
|
||||
)
|
||||
assert not any("(enabled: False)" in message for message in caplog.messages)
|
||||
assert (
|
||||
"Identified a total of 1 accessibility services, 0 reported enabled "
|
||||
"(1 more stated by the dump without a component name)" in caplog.messages
|
||||
)
|
||||
@@ -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") == []
|
||||
@@ -1,16 +0,0 @@
|
||||
ACCESSIBILITY MANAGER (dumpsys accessibility)
|
||||
|
||||
currentUserId=0
|
||||
User state[
|
||||
attributes:{id=0, touchExplorationEnabled=false, installedServiceCount=5}
|
||||
installed services: {
|
||||
0 : com.google.android.apps.accessibility.voiceaccess/.JustSpeakService (A11yTool)
|
||||
1 : com.microsoft.appmanager/com.microsoft.mmx.screenmirroringsrc.accessibility.ScreenMirroringAccessibilityService
|
||||
2 : com.samsung.accessibility/.assistantmenu.serviceframework.AssistantMenuService (A11yTool)
|
||||
3 : com.samsung.accessibility/.universalswitch.UniversalSwitchService (A11yTool)
|
||||
4 : com.samsung.android.accessibility.talkback/com.samsung.android.marvin.talkback.TalkBackService (A11yTool)
|
||||
}
|
||||
Bound services:{}
|
||||
Enabled services:{{com.samsung.accessibility/.universalswitch.UniversalSwitchService}}
|
||||
Binding services:{}
|
||||
Crashed services:{}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user