Deduplicate ADB AndroidQF and other modules (#606)

* Run bugreport and backup modules during check-androidqf

Adding support to automatically run ADB backup and bugreport modules
automatically when running the check-androidqf command. This is a first
step to deduplicate the code for Android modules.

* Deduplicate modules which are run by the sub-commands.

* Raise the proper NoAndroidQFBackup exception when a back-up isn't found

* add missing import

* Fix imports and remove duplicate hashes param

* Rename from_folder to from_dir in tests

---------

Co-authored-by: besendorf <janik@besendorf.org>
This commit is contained in:
Donncha Ó Cearbhaill
2025-10-31 13:46:33 +01:00
committed by GitHub
parent 7009cddc8c
commit 339a1d0712
71 changed files with 363 additions and 1302 deletions

View File

@@ -22,7 +22,7 @@ class TestBackupModule:
for root, subdirs, subfiles in os.walk(os.path.abspath(backup_path)):
for fname in subfiles:
files.append(os.path.relpath(os.path.join(root, fname), backup_path))
mod.from_folder(backup_path, files)
mod.from_dir(backup_path, files)
run_module(mod)
assert len(mod.results) == 2
assert len(mod.results[0]["links"]) == 1

View File

@@ -1,27 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_adb import DumpsysADBState
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysADBModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysADBState(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 1
assert len(m.detected) == 0
adb_statedump = m.results[0]
assert "user_keys" in adb_statedump
assert len(adb_statedump["user_keys"]) == 1

View File

@@ -1,24 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_battery_daily import DumpsysBatteryDaily
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysBatteryDailyModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysBatteryDaily(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 3
assert len(m.timeline) == 3
assert len(m.detected) == 0

View File

@@ -1,24 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_battery_history import DumpsysBatteryHistory
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysBatteryHistoryModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysBatteryHistory(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 6
assert len(m.timeline) == 0
assert len(m.detected) == 0

View File

@@ -1,24 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_dbinfo import DumpsysDBInfo
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysDBInfoModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysDBInfo(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 6
assert len(m.timeline) == 0
assert len(m.detected) == 0

View File

@@ -1,23 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_platform_compat import DumpsysPlatformCompat
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysPlatformCompatModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysPlatformCompat(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 2
assert len(m.detected) == 0

View File

@@ -1,23 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_accessibility import DumpsysAccessibility
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysAccessibilityModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysAccessibility(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 4
assert len(m.detected) == 0

View File

@@ -1,29 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_appops import DumpsysAppops
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysAppOpsModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysAppops(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 12
assert len(m.timeline) == 16
detected_by_ioc = [
detected for detected in m.detected if detected.get("matched_indicator")
]
assert len(m.detected) == 1
assert len(detected_by_ioc) == 0

View File

@@ -1,46 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 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 pathlib import Path
from mvt.android.modules.androidqf.dumpsys_packages import DumpsysPackages
from mvt.common.indicators import Indicators
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysPackagesModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysPackages(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 2
assert len(m.detected) == 0
assert len(m.timeline) == 6
assert (
m.results[0]["package_name"]
== "com.samsung.android.provider.filterprovider"
)
def test_detection_pkgname(self, indicator_file):
data_path = get_android_androidqf()
m = DumpsysPackages(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
ind = Indicators(log=logging.getLogger())
ind.parse_stix2(indicator_file)
ind.ioc_collections[0]["app_ids"].append("com.sec.android.app.DataCreate")
m.indicators = ind
run_module(m)
assert len(m.results) == 2
assert len(m.detected) == 1
assert len(m.timeline) == 6
assert m.detected[0]["package_name"] == "com.sec.android.app.DataCreate"

View File

@@ -1,23 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 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/
from pathlib import Path
from mvt.android.modules.androidqf.dumpsys_receivers import DumpsysReceivers
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
class TestDumpsysReceiversModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = DumpsysReceivers(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
run_module(m)
assert len(m.results) == 4
assert len(m.detected) == 0

View File

@@ -6,7 +6,7 @@
import logging
from pathlib import Path
from mvt.android.modules.androidqf.files import Files
from mvt.android.modules.androidqf.aqf_files import AQFFiles
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
@@ -15,10 +15,10 @@ from ..utils import get_android_androidqf, list_files
class TestAndroidqfFilesAnalysis:
def test_androidqf_files(self):
data_path = get_android_androidqf()
m = Files(target_path=data_path, log=logging)
m = AQFFiles(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 3
assert len(m.timeline) == 6

View File

@@ -7,7 +7,7 @@ import logging
import zipfile
from pathlib import Path
from mvt.android.modules.androidqf.getprop import Getprop
from mvt.android.modules.androidqf.aqf_getprop import AQFGetProp
from mvt.common.indicators import Indicators
from mvt.common.module import run_module
@@ -17,10 +17,10 @@ from ..utils import get_android_androidqf, get_artifact, list_files
class TestAndroidqfGetpropAnalysis:
def test_androidqf_getprop(self):
data_path = get_android_androidqf()
m = Getprop(target_path=data_path, log=logging)
m = AQFGetProp(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 10
assert m.results[0]["name"] == "dalvik.vm.appimageformat"
@@ -30,9 +30,9 @@ class TestAndroidqfGetpropAnalysis:
def test_getprop_parsing_zip(self):
fpath = get_artifact("androidqf.zip")
m = Getprop(target_path=fpath, log=logging)
m = AQFGetProp(target_path=fpath, log=logging)
archive = zipfile.ZipFile(fpath)
m.from_zip_file(archive, archive.namelist())
m.from_zip(archive, archive.namelist())
run_module(m)
assert len(m.results) == 10
assert m.results[0]["name"] == "dalvik.vm.appimageformat"
@@ -42,10 +42,10 @@ class TestAndroidqfGetpropAnalysis:
def test_androidqf_getprop_detection(self, indicator_file):
data_path = get_android_androidqf()
m = Getprop(target_path=data_path, log=logging)
m = AQFGetProp(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
ind = Indicators(log=logging.getLogger())
ind.parse_stix2(indicator_file)
ind.ioc_collections[0]["android_property_names"].append("dalvik.vm.heapmaxfree")

View File

@@ -85,7 +85,7 @@ class TestAndroidqfMountsModule:
m = Mounts(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)

View File

@@ -8,7 +8,7 @@ from pathlib import Path
import pytest
from mvt.android.modules.androidqf.packages import Packages
from mvt.android.modules.androidqf.aqf_packages import AQFPackages
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
@@ -31,8 +31,8 @@ def file_list(data_path):
@pytest.fixture()
def module(parent_data_path, file_list):
m = Packages(target_path=parent_data_path, log=logging)
m.from_folder(parent_data_path, file_list)
m = AQFPackages(target_path=parent_data_path, log=logging)
m.from_dir(parent_data_path, file_list)
return m

View File

@@ -6,7 +6,7 @@
import logging
from pathlib import Path
from mvt.android.modules.androidqf.processes import Processes
from mvt.android.modules.androidqf.aqf_processes import AQFProcesses
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
@@ -15,10 +15,10 @@ from ..utils import get_android_androidqf, list_files
class TestAndroidqfProcessesAnalysis:
def test_androidqf_processes(self):
data_path = get_android_androidqf()
m = Processes(target_path=data_path, log=logging)
m = AQFProcesses(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 15
assert len(m.timeline) == 0

View File

@@ -32,7 +32,7 @@ def file_list(data_path):
@pytest.fixture()
def module(parent_data_path, file_list):
m = RootBinaries(target_path=parent_data_path, log=logging)
m.from_folder(parent_data_path, file_list)
m.from_dir(parent_data_path, file_list)
return m
@@ -108,7 +108,7 @@ class TestAndroidqfRootBinaries:
# Test behavior when no root_binaries.json file is present
empty_file_list = []
m = RootBinaries(target_path=parent_data_path, log=logging)
m.from_folder(parent_data_path, empty_file_list)
m.from_dir(parent_data_path, empty_file_list)
run_module(m)

View File

@@ -5,7 +5,7 @@
from pathlib import Path
from mvt.android.modules.androidqf.settings import Settings
from mvt.android.modules.androidqf.aqf_settings import AQFSettings
from mvt.common.module import run_module
from ..utils import get_android_androidqf, list_files
@@ -14,10 +14,10 @@ from ..utils import get_android_androidqf, list_files
class TestSettingsModule:
def test_parsing(self):
data_path = get_android_androidqf()
m = Settings(target_path=data_path)
m = AQFSettings(target_path=data_path)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 1
assert "random" in m.results.keys()

View File

@@ -21,7 +21,7 @@ class TestAndroidqfSMSAnalysis:
m = SMS(target_path=data_path, log=logging)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 2
assert len(m.timeline) == 0
@@ -36,7 +36,7 @@ class TestAndroidqfSMSAnalysis:
)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 1
@@ -52,7 +52,7 @@ class TestAndroidqfSMSAnalysis:
)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert prompt_mock.call_count == 1
assert len(m.results) == 1
@@ -67,7 +67,7 @@ class TestAndroidqfSMSAnalysis:
)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 0
assert "Invalid backup password" in caplog.text
@@ -82,7 +82,7 @@ class TestAndroidqfSMSAnalysis:
)
files = list_files(data_path)
parent_path = Path(data_path).absolute().parent.as_posix()
m.from_folder(parent_path, files)
m.from_dir(parent_path, files)
run_module(m)
assert len(m.results) == 0
assert (

View File

@@ -6,9 +6,9 @@
import os
from pathlib import Path
from mvt.android.modules.bugreport.appops import Appops
from mvt.android.modules.bugreport.getprop import Getprop
from mvt.android.modules.bugreport.packages import Packages
from mvt.android.modules.bugreport.dumpsys_appops import DumpsysAppops
from mvt.android.modules.bugreport.dumpsys_getprop import DumpsysGetProp
from mvt.android.modules.bugreport.dumpsys_packages import DumpsysPackages
from mvt.android.modules.bugreport.tombstones import Tombstones
from mvt.common.module import run_module
@@ -26,12 +26,12 @@ class TestBugreportAnalysis:
folder_files.append(
os.path.relpath(os.path.join(root, file_name), parent_path)
)
m.from_folder(fpath, folder_files)
m.from_dir(fpath, folder_files)
run_module(m)
return m
def test_appops_module(self):
m = self.launch_bug_report_module(Appops)
m = self.launch_bug_report_module(DumpsysAppops)
assert len(m.results) == 12
assert len(m.timeline) == 16
@@ -42,7 +42,7 @@ class TestBugreportAnalysis:
assert len(detected_by_ioc) == 0
def test_packages_module(self):
m = self.launch_bug_report_module(Packages)
m = self.launch_bug_report_module(DumpsysPackages)
assert len(m.results) == 2
assert (
m.results[0]["package_name"]
@@ -53,7 +53,7 @@ class TestBugreportAnalysis:
assert len(m.results[1]["permissions"]) == 32
def test_getprop_module(self):
m = self.launch_bug_report_module(Getprop)
m = self.launch_bug_report_module(DumpsysGetProp)
assert len(m.results) == 0
def test_tombstones_modules(self):

View File

@@ -32,7 +32,8 @@ class TestCheckAndroidqfCommand:
path = os.path.join(get_artifact_folder(), "androidqf_encrypted")
result = runner.invoke(check_androidqf, [path])
assert prompt_mock.call_count == 1
# Called twice, once in AnroidQF SMS module and once in Backup SMS module
assert prompt_mock.call_count == 2
assert result.exit_code == 0
def test_check_encrypted_backup_cli(self, mocker):