From 5d696350dc5f0238ea3bbaac46a64c4c9da1ee40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Fri, 7 Feb 2025 10:05:16 +0100 Subject: [PATCH 01/37] 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. --- src/mvt/android/cmd_check_adb.py | 7 + src/mvt/android/cmd_check_androidqf.py | 150 ++++++++++++++++-- src/mvt/android/cmd_check_backup.py | 68 ++++---- src/mvt/android/cmd_check_bugreport.py | 65 +++++--- src/mvt/android/modules/androidqf/base.py | 4 +- src/mvt/android/modules/backup/base.py | 11 +- src/mvt/android/modules/backup/sms.py | 4 +- src/mvt/android/modules/bugreport/base.py | 4 +- src/mvt/common/cmd_check_iocs.py | 4 + src/mvt/common/command.py | 17 +- src/mvt/ios/cmd_check_backup.py | 5 + src/mvt/ios/cmd_check_fs.py | 5 + tests/android/test_backup_module.py | 2 +- .../test_dumpsys_adbstate.py | 2 +- .../test_dumpsys_battery_daily.py | 2 +- .../test_dumpsys_battery_history.py | 2 +- .../android_androidqf/test_dumpsys_dbinfo.py | 2 +- .../test_dumpsys_platform_compat.py | 2 +- .../test_dumpsysaccessbility.py | 2 +- tests/android_androidqf/test_dumpsysappops.py | 2 +- .../android_androidqf/test_dumpsyspackages.py | 4 +- .../test_dumpsysreceivers.py | 2 +- tests/android_androidqf/test_files.py | 2 +- tests/android_androidqf/test_getprop.py | 6 +- tests/android_androidqf/test_packages.py | 2 +- tests/android_androidqf/test_processes.py | 2 +- tests/android_androidqf/test_settings.py | 2 +- tests/android_androidqf/test_sms.py | 10 +- tests/android_bugreport/test_bugreport.py | 2 +- tests/test_check_android_androidqf.py | 3 +- 30 files changed, 289 insertions(+), 106 deletions(-) diff --git a/src/mvt/android/cmd_check_adb.py b/src/mvt/android/cmd_check_adb.py index e274040aa..c1444f4e5 100644 --- a/src/mvt/android/cmd_check_adb.py +++ b/src/mvt/android/cmd_check_adb.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.common.command import Command +from mvt.common.indicators import Indicators from .modules.adb import ADB_MODULES @@ -19,17 +20,23 @@ def __init__( target_path: Optional[str] = None, results_path: Optional[str] = None, ioc_files: Optional[list] = None, + iocs: Optional[Indicators] = None, module_name: Optional[str] = None, serial: Optional[str] = None, module_options: Optional[dict] = None, + hashes: Optional[bool] = False, + sub_command: Optional[bool] = False, ) -> None: super().__init__( target_path=target_path, results_path=results_path, ioc_files=ioc_files, + iocs=iocs, module_name=module_name, serial=serial, module_options=module_options, + hashes=hashes, + sub_command=sub_command, log=log, ) diff --git a/src/mvt/android/cmd_check_androidqf.py b/src/mvt/android/cmd_check_androidqf.py index e0798075e..628548c59 100644 --- a/src/mvt/android/cmd_check_androidqf.py +++ b/src/mvt/android/cmd_check_androidqf.py @@ -10,58 +10,182 @@ from typing import List, Optional from mvt.common.command import Command +from mvt.common.indicators import Indicators + +from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport +from mvt.android.cmd_check_backup import CmdAndroidCheckBackup from .modules.androidqf import ANDROIDQF_MODULES +from .modules.androidqf.base import AndroidQFModule log = logging.getLogger(__name__) +class NoAndroidQFTargetPath(Exception): + pass + + +class NoAndroidQFBugReport(Exception): + pass + + +class NoAndroidQFBackup(Exception): + pass + + class CmdAndroidCheckAndroidQF(Command): def __init__( self, target_path: Optional[str] = None, results_path: Optional[str] = None, ioc_files: Optional[list] = None, + iocs: Optional[Indicators] = None, module_name: Optional[str] = None, serial: Optional[str] = None, module_options: Optional[dict] = None, - hashes: bool = False, + hashes: Optional[bool] = False, + sub_command: Optional[bool] = False, ) -> None: super().__init__( target_path=target_path, results_path=results_path, ioc_files=ioc_files, + iocs=iocs, module_name=module_name, serial=serial, module_options=module_options, hashes=hashes, + sub_command=sub_command, log=log, ) self.name = "check-androidqf" self.modules = ANDROIDQF_MODULES - self.format: Optional[str] = None - self.archive: Optional[zipfile.ZipFile] = None - self.files: List[str] = [] + self.__format: Optional[str] = None + self.__zip: Optional[zipfile.ZipFile] = None + self.__files: List[str] = [] def init(self): if os.path.isdir(self.target_path): - self.format = "dir" + self.__format = "dir" parent_path = Path(self.target_path).absolute().parent.as_posix() target_abs_path = os.path.abspath(self.target_path) for root, subdirs, subfiles in os.walk(target_abs_path): for fname in subfiles: file_path = os.path.relpath(os.path.join(root, fname), parent_path) - self.files.append(file_path) + self.__files.append(file_path) elif os.path.isfile(self.target_path): - self.format = "zip" - self.archive = zipfile.ZipFile(self.target_path) - self.files = self.archive.namelist() + self.__format = "zip" + self.__zip = zipfile.ZipFile(self.target_path) + self.__files = self.__zip.namelist() + + def module_init(self, module: AndroidQFModule) -> None: # type: ignore[override] + if self.__format == "zip" and self.__zip: + module.from_zip(self.__zip, self.__files) + return + + if not self.target_path: + raise NoAndroidQFTargetPath + + parent_path = Path(self.target_path).absolute().parent.as_posix() + module.from_dir(parent_path, self.__files) + + def load_bugreport(self) -> zipfile.ZipFile: + bugreport_zip_path = None + for file_name in self.__files: + if file_name.endswith("bugreport.zip"): + bugreport_zip_path = file_name + break + else: + raise NoAndroidQFBugReport + + if self.__format == "zip" and self.__zip: + handle = self.__zip.open(bugreport_zip_path) + return zipfile.ZipFile(handle) - def module_init(self, module): - if self.format == "zip": - module.from_zip_file(self.archive, self.files) + if self.__format == "dir" and self.target_path: + parent_path = Path(self.target_path).absolute().parent.as_posix() + bug_report_path = os.path.join(parent_path, bugreport_zip_path) + return zipfile.ZipFile(bug_report_path) + + raise NoAndroidQFBugReport + + def load_backup(self) -> bytes: + backup_ab_path = None + for file_name in self.__files: + if file_name.endswith("backup.ab"): + backup_ab_path = file_name + break else: + raise NoAndroidQFBackup + + if self.__format == "zip" and self.__zip: + backup_file_handle = self.__zip.open(backup_ab_path) + return backup_file_handle.read() + + if self.__format == "dir" and self.target_path: parent_path = Path(self.target_path).absolute().parent.as_posix() - module.from_folder(parent_path, self.files) + backup_path = os.path.join(parent_path, backup_ab_path) + with open(backup_path, "rb") as backup_file: + backup_ab_data = backup_file.read() + return backup_ab_data + + raise NoAndroidQFBackup + + def run_bugreport_cmd(self) -> bool: + try: + bugreport = self.load_bugreport() + except NoAndroidQFBugReport: + self.log.warning( + "Skipping bugreport modules as no bugreport.zip found in AndroidQF data." + ) + return False + else: + cmd = CmdAndroidCheckBugreport( + target_path=None, + results_path=self.results_path, + ioc_files=self.ioc_files, + iocs=self.iocs, + module_options=self.module_options, + hashes=self.hashes, + sub_command=True, + ) + cmd.from_zip(bugreport) + cmd.run() + + self.detected_count += cmd.detected_count + self.timeline.extend(cmd.timeline) + self.timeline_detected.extend(cmd.timeline_detected) + + def run_backup_cmd(self) -> bool: + try: + backup = self.load_backup() + except NoAndroidQFBugReport: + self.log.warning( + "Skipping backup modules as no backup.ab found in AndroidQF data." + ) + return False + else: + cmd = CmdAndroidCheckBackup( + target_path=None, + results_path=self.results_path, + ioc_files=self.ioc_files, + iocs=self.iocs, + module_options=self.module_options, + hashes=self.hashes, + sub_command=True, + ) + cmd.from_ab(backup) + cmd.run() + + self.detected_count += cmd.detected_count + self.timeline.extend(cmd.timeline) + self.timeline_detected.extend(cmd.timeline_detected) + + def finish(self) -> None: + """ + Run the bugreport and backup modules if the respective files are found in the AndroidQF data. + """ + self.run_bugreport_cmd() + self.run_backup_cmd() diff --git a/src/mvt/android/cmd_check_backup.py b/src/mvt/android/cmd_check_backup.py index 2a689003c..e366d2b5f 100644 --- a/src/mvt/android/cmd_check_backup.py +++ b/src/mvt/android/cmd_check_backup.py @@ -20,6 +20,7 @@ parse_backup_file, ) from mvt.common.command import Command +from mvt.common.indicators import Indicators from .modules.backup import BACKUP_MODULES @@ -32,19 +33,23 @@ def __init__( target_path: Optional[str] = None, results_path: Optional[str] = None, ioc_files: Optional[list] = None, + iocs: Optional[Indicators] = None, module_name: Optional[str] = None, serial: Optional[str] = None, module_options: Optional[dict] = None, - hashes: bool = False, + hashes: Optional[bool] = False, + sub_command: Optional[bool] = False, ) -> None: super().__init__( target_path=target_path, results_path=results_path, ioc_files=ioc_files, + iocs=iocs, module_name=module_name, serial=serial, module_options=module_options, hashes=hashes, + sub_command=sub_command, log=log, ) @@ -55,6 +60,34 @@ def __init__( self.backup_archive: Optional[tarfile.TarFile] = None self.backup_files: List[str] = [] + def from_ab(self, ab_file_bytes: bytes) -> None: + self.backup_type = "ab" + header = parse_ab_header(ab_file_bytes) + if not header["backup"]: + log.critical("Invalid backup format, file should be in .ab format") + sys.exit(1) + + password = None + if header["encryption"] != "none": + password = prompt_or_load_android_backup_password(log, self.module_options) + if not password: + log.critical("No backup password provided.") + sys.exit(1) + try: + tardata = parse_backup_file(ab_file_bytes, password=password) + except InvalidBackupPassword: + log.critical("Invalid backup password") + sys.exit(1) + except AndroidBackupParsingError as exc: + log.critical("Impossible to parse this backup file: %s", exc) + log.critical("Please use Android Backup Extractor (ABE) instead") + sys.exit(1) + + dbytes = io.BytesIO(tardata) + self.backup_archive = tarfile.open(fileobj=dbytes) + for member in self.backup_archive: + self.backup_files.append(member.name) + def init(self) -> None: if not self.target_path: return @@ -62,35 +95,8 @@ def init(self) -> None: if os.path.isfile(self.target_path): self.backup_type = "ab" with open(self.target_path, "rb") as handle: - data = handle.read() - - header = parse_ab_header(data) - if not header["backup"]: - log.critical("Invalid backup format, file should be in .ab format") - sys.exit(1) - - password = None - if header["encryption"] != "none": - password = prompt_or_load_android_backup_password( - log, self.module_options - ) - if not password: - log.critical("No backup password provided.") - sys.exit(1) - try: - tardata = parse_backup_file(data, password=password) - except InvalidBackupPassword: - log.critical("Invalid backup password") - sys.exit(1) - except AndroidBackupParsingError as exc: - log.critical("Impossible to parse this backup file: %s", exc) - log.critical("Please use Android Backup Extractor (ABE) instead") - sys.exit(1) - - dbytes = io.BytesIO(tardata) - self.backup_archive = tarfile.open(fileobj=dbytes) - for member in self.backup_archive: - self.backup_files.append(member.name) + ab_file_bytes = handle.read() + self.from_ab(ab_file_bytes) elif os.path.isdir(self.target_path): self.backup_type = "folder" @@ -109,6 +115,6 @@ def init(self) -> None: def module_init(self, module: BackupExtraction) -> None: # type: ignore[override] if self.backup_type == "folder": - module.from_folder(self.target_path, self.backup_files) + module.from_dir(self.target_path, self.backup_files) else: module.from_ab(self.target_path, self.backup_archive, self.backup_files) diff --git a/src/mvt/android/cmd_check_bugreport.py b/src/mvt/android/cmd_check_bugreport.py index 08a266f5d..a3d9b3bb5 100644 --- a/src/mvt/android/cmd_check_bugreport.py +++ b/src/mvt/android/cmd_check_bugreport.py @@ -11,6 +11,7 @@ from mvt.android.modules.bugreport.base import BugReportModule from mvt.common.command import Command +from mvt.common.indicators import Indicators from .modules.bugreport import BUGREPORT_MODULES @@ -23,54 +24,76 @@ def __init__( target_path: Optional[str] = None, results_path: Optional[str] = None, ioc_files: Optional[list] = None, + iocs: Optional[Indicators] = None, module_name: Optional[str] = None, serial: Optional[str] = None, module_options: Optional[dict] = None, - hashes: bool = False, + hashes: Optional[bool] = False, + sub_command: Optional[bool] = False, ) -> None: super().__init__( target_path=target_path, results_path=results_path, ioc_files=ioc_files, + iocs=iocs, module_name=module_name, serial=serial, module_options=module_options, hashes=hashes, + sub_command=sub_command, log=log, ) self.name = "check-bugreport" self.modules = BUGREPORT_MODULES - self.bugreport_format: str = "" - self.bugreport_archive: Optional[ZipFile] = None - self.bugreport_files: List[str] = [] + self.__format: str = "" + self.__zip: Optional[ZipFile] = None + self.__files: List[str] = [] + + def from_dir(self, dir_path: str) -> None: + """This method is used to initialize the bug report analysis from an + uncompressed directory. + """ + self.__format = "dir" + self.target_path = dir_path + parent_path = Path(dir_path).absolute().as_posix() + for root, _, subfiles in os.walk(os.path.abspath(dir_path)): + for file_name in subfiles: + file_path = os.path.relpath(os.path.join(root, file_name), parent_path) + self.__files.append(file_path) + + def from_zip(self, bugreport_zip: ZipFile) -> None: + """This method is used to initialize the bug report analysis from a + compressed archive. + """ + # NOTE: This will be invoked either by the CLI directly,or by the + # check-androidqf command. We need this because we want to support + # check-androidqf to analyse compressed archives itself too. + # So, we'll need to extract bugreport.zip from a 'androidqf.zip', and + # since nothing is written on disk, we need to be able to pass this + # command a ZipFile instance in memory. + + self.__format = "zip" + self.__zip = bugreport_zip + for file_name in self.__zip.namelist(): + self.__files.append(file_name) def init(self) -> None: if not self.target_path: return if os.path.isfile(self.target_path): - self.bugreport_format = "zip" - self.bugreport_archive = ZipFile(self.target_path) - for file_name in self.bugreport_archive.namelist(): - self.bugreport_files.append(file_name) + self.from_zip(ZipFile(self.target_path)) elif os.path.isdir(self.target_path): - self.bugreport_format = "dir" - parent_path = Path(self.target_path).absolute().as_posix() - for root, _, subfiles in os.walk(os.path.abspath(self.target_path)): - for file_name in subfiles: - file_path = os.path.relpath( - os.path.join(root, file_name), parent_path - ) - self.bugreport_files.append(file_path) + self.from_dir(self.target_path) def module_init(self, module: BugReportModule) -> None: # type: ignore[override] - if self.bugreport_format == "zip": - module.from_zip(self.bugreport_archive, self.bugreport_files) + if self.__format == "zip": + module.from_zip(self.__zip, self.__files) else: - module.from_folder(self.target_path, self.bugreport_files) + module.from_dir(self.target_path, self.__files) def finish(self) -> None: - if self.bugreport_archive: - self.bugreport_archive.close() + if self.__zip: + self.__zip.close() diff --git a/src/mvt/android/modules/androidqf/base.py b/src/mvt/android/modules/androidqf/base.py index d871059b7..43e62108a 100644 --- a/src/mvt/android/modules/androidqf/base.py +++ b/src/mvt/android/modules/androidqf/base.py @@ -37,11 +37,11 @@ def __init__( self.files: List[str] = [] self.archive: Optional[zipfile.ZipFile] = None - def from_folder(self, parent_path: str, files: List[str]): + def from_dir(self, parent_path: str, files: List[str]) -> None: self.parent_path = parent_path self.files = files - def from_zip_file(self, archive: zipfile.ZipFile, files: List[str]): + def from_zip(self, archive: zipfile.ZipFile, files: List[str]) -> None: self.archive = archive self.files = files diff --git a/src/mvt/android/modules/backup/base.py b/src/mvt/android/modules/backup/base.py index 5141bfe35..29238ba61 100644 --- a/src/mvt/android/modules/backup/base.py +++ b/src/mvt/android/modules/backup/base.py @@ -37,10 +37,7 @@ def __init__( self.tar = None self.files = [] - def from_folder(self, backup_path: Optional[str], files: List[str]) -> None: - """ - Get all the files and list them - """ + def from_dir(self, backup_path: Optional[str], files: List[str]) -> None: self.backup_path = backup_path self.files = files @@ -58,14 +55,16 @@ def _get_files_by_pattern(self, pattern: str) -> list: return fnmatch.filter(self.files, pattern) def _get_file_content(self, file_path: str) -> bytes: - if self.ab: + if self.tar: try: member = self.tar.getmember(file_path) except KeyError: return None handle = self.tar.extractfile(member) - else: + elif self.backup_path: handle = open(os.path.join(self.backup_path, file_path), "rb") + else: + raise ValueError("No backup path or tar file provided") data = handle.read() handle.close() diff --git a/src/mvt/android/modules/backup/sms.py b/src/mvt/android/modules/backup/sms.py index a75be261d..a194a1e2c 100644 --- a/src/mvt/android/modules/backup/sms.py +++ b/src/mvt/android/modules/backup/sms.py @@ -50,13 +50,13 @@ def check_indicators(self) -> None: def run(self) -> None: sms_path = "apps/com.android.providers.telephony/d_f/*_sms_backup" for file in self._get_files_by_pattern(sms_path): - self.log.info("Processing SMS backup file at %s", file) + self.log.debug("Processing SMS backup file at %s", file) data = self._get_file_content(file) self.results.extend(parse_sms_file(data)) mms_path = "apps/com.android.providers.telephony/d_f/*_mms_backup" for file in self._get_files_by_pattern(mms_path): - self.log.info("Processing MMS backup file at %s", file) + self.log.debug("Processing MMS backup file at %s", file) data = self._get_file_content(file) self.results.extend(parse_sms_file(data)) diff --git a/src/mvt/android/modules/bugreport/base.py b/src/mvt/android/modules/bugreport/base.py index 77802b214..158bc2882 100644 --- a/src/mvt/android/modules/bugreport/base.py +++ b/src/mvt/android/modules/bugreport/base.py @@ -39,9 +39,7 @@ def __init__( self.extract_files: List[str] = [] self.zip_files: List[str] = [] - def from_folder( - self, extract_path: Optional[str], extract_files: List[str] - ) -> None: + def from_dir(self, extract_path: str, extract_files: List[str]) -> None: self.extract_path = extract_path self.extract_files = extract_files diff --git a/src/mvt/common/cmd_check_iocs.py b/src/mvt/common/cmd_check_iocs.py index 696803a73..1f8bde5d1 100644 --- a/src/mvt/common/cmd_check_iocs.py +++ b/src/mvt/common/cmd_check_iocs.py @@ -22,6 +22,8 @@ def __init__( module_name: Optional[str] = None, serial: Optional[str] = None, module_options: Optional[dict] = None, + hashes: Optional[bool] = False, + sub_command: Optional[bool] = False, ) -> None: super().__init__( target_path=target_path, @@ -30,6 +32,8 @@ def __init__( module_name=module_name, serial=serial, module_options=module_options, + hashes=hashes, + sub_command=sub_command, log=log, ) diff --git a/src/mvt/common/command.py b/src/mvt/common/command.py index 7f6584330..b6d7aaff4 100644 --- a/src/mvt/common/command.py +++ b/src/mvt/common/command.py @@ -27,10 +27,12 @@ def __init__( target_path: Optional[str] = None, results_path: Optional[str] = None, ioc_files: Optional[list] = None, + iocs: Optional[Indicators] = None, module_name: Optional[str] = None, serial: Optional[str] = None, module_options: Optional[dict] = None, - hashes: bool = False, + hashes: Optional[bool] = False, + sub_command: Optional[bool] = False, log: logging.Logger = logging.getLogger(__name__), ) -> None: self.name = "" @@ -42,6 +44,7 @@ def __init__( self.module_name = module_name self.serial = serial self.log = log + self.sub_command = sub_command # This dictionary can contain options that will be passed down from # the Command to all modules. This can for example be used to pass @@ -60,8 +63,12 @@ def __init__( # Load IOCs self._create_storage() self._setup_logging() - self.iocs = Indicators(log=log) - self.iocs.load_indicators_files(self.ioc_files) + + if iocs is not None: + self.iocs = iocs + else: + self.iocs = Indicators(self.log) + self.iocs.load_indicators_files(self.ioc_files) def _create_storage(self) -> None: if self.results_path and not os.path.exists(self.results_path): @@ -247,6 +254,10 @@ def run(self) -> None: except NotImplementedError: pass + # We only store the timeline from the parent/main command + if self.sub_command: + return + self._store_timeline() self._store_info() diff --git a/src/mvt/ios/cmd_check_backup.py b/src/mvt/ios/cmd_check_backup.py index 66dfc8e06..dcdc013bb 100644 --- a/src/mvt/ios/cmd_check_backup.py +++ b/src/mvt/ios/cmd_check_backup.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.common.command import Command +from mvt.common.indicators import Indicators from .modules.backup import BACKUP_MODULES from .modules.mixed import MIXED_MODULES @@ -20,19 +21,23 @@ def __init__( target_path: Optional[str] = None, results_path: Optional[str] = None, ioc_files: Optional[list] = None, + iocs: Optional[Indicators] = None, module_name: Optional[str] = None, serial: Optional[str] = None, module_options: Optional[dict] = None, hashes: bool = False, + sub_command: bool = False, ) -> None: super().__init__( target_path=target_path, results_path=results_path, ioc_files=ioc_files, + iocs=iocs, module_name=module_name, serial=serial, module_options=module_options, hashes=hashes, + sub_command=sub_command, log=log, ) diff --git a/src/mvt/ios/cmd_check_fs.py b/src/mvt/ios/cmd_check_fs.py index 3484138cc..605bfd0ad 100644 --- a/src/mvt/ios/cmd_check_fs.py +++ b/src/mvt/ios/cmd_check_fs.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.common.command import Command +from mvt.common.indicators import Indicators from .modules.fs import FS_MODULES from .modules.mixed import MIXED_MODULES @@ -20,19 +21,23 @@ def __init__( target_path: Optional[str] = None, results_path: Optional[str] = None, ioc_files: Optional[list] = None, + iocs: Optional[Indicators] = None, module_name: Optional[str] = None, serial: Optional[str] = None, module_options: Optional[dict] = None, hashes: bool = False, + sub_command: bool = False, ) -> None: super().__init__( target_path=target_path, results_path=results_path, ioc_files=ioc_files, + iocs=iocs, module_name=module_name, serial=serial, module_options=module_options, hashes=hashes, + sub_command=sub_command, log=log, ) diff --git a/tests/android/test_backup_module.py b/tests/android/test_backup_module.py index 29bc8e9a2..57cb89191 100644 --- a/tests/android/test_backup_module.py +++ b/tests/android/test_backup_module.py @@ -22,7 +22,7 @@ def test_module_folder(self): 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 diff --git a/tests/android_androidqf/test_dumpsys_adbstate.py b/tests/android_androidqf/test_dumpsys_adbstate.py index c94fe34d7..a3ac72e14 100644 --- a/tests/android_androidqf/test_dumpsys_adbstate.py +++ b/tests/android_androidqf/test_dumpsys_adbstate.py @@ -17,7 +17,7 @@ def test_parsing(self): 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) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 1 assert len(m.detected) == 0 diff --git a/tests/android_androidqf/test_dumpsys_battery_daily.py b/tests/android_androidqf/test_dumpsys_battery_daily.py index 66a45d7f5..f82f3303c 100644 --- a/tests/android_androidqf/test_dumpsys_battery_daily.py +++ b/tests/android_androidqf/test_dumpsys_battery_daily.py @@ -17,7 +17,7 @@ def test_parsing(self): 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) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 3 assert len(m.timeline) == 3 diff --git a/tests/android_androidqf/test_dumpsys_battery_history.py b/tests/android_androidqf/test_dumpsys_battery_history.py index 8b7645913..fd1d0aeb8 100644 --- a/tests/android_androidqf/test_dumpsys_battery_history.py +++ b/tests/android_androidqf/test_dumpsys_battery_history.py @@ -17,7 +17,7 @@ def test_parsing(self): 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) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 6 assert len(m.timeline) == 0 diff --git a/tests/android_androidqf/test_dumpsys_dbinfo.py b/tests/android_androidqf/test_dumpsys_dbinfo.py index 83addbf71..371a3bcc3 100644 --- a/tests/android_androidqf/test_dumpsys_dbinfo.py +++ b/tests/android_androidqf/test_dumpsys_dbinfo.py @@ -17,7 +17,7 @@ def test_parsing(self): 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) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 6 assert len(m.timeline) == 0 diff --git a/tests/android_androidqf/test_dumpsys_platform_compat.py b/tests/android_androidqf/test_dumpsys_platform_compat.py index 812343291..bddc32275 100644 --- a/tests/android_androidqf/test_dumpsys_platform_compat.py +++ b/tests/android_androidqf/test_dumpsys_platform_compat.py @@ -17,7 +17,7 @@ def test_parsing(self): 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) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 2 assert len(m.detected) == 0 diff --git a/tests/android_androidqf/test_dumpsysaccessbility.py b/tests/android_androidqf/test_dumpsysaccessbility.py index 1a217d0cd..b43727573 100644 --- a/tests/android_androidqf/test_dumpsysaccessbility.py +++ b/tests/android_androidqf/test_dumpsysaccessbility.py @@ -17,7 +17,7 @@ def test_parsing(self): 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) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 4 assert len(m.detected) == 0 diff --git a/tests/android_androidqf/test_dumpsysappops.py b/tests/android_androidqf/test_dumpsysappops.py index b0649a196..ce74d53a8 100644 --- a/tests/android_androidqf/test_dumpsysappops.py +++ b/tests/android_androidqf/test_dumpsysappops.py @@ -17,7 +17,7 @@ def test_parsing(self): 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) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 12 assert len(m.timeline) == 16 diff --git a/tests/android_androidqf/test_dumpsyspackages.py b/tests/android_androidqf/test_dumpsyspackages.py index 798628ff2..e801b0a75 100644 --- a/tests/android_androidqf/test_dumpsyspackages.py +++ b/tests/android_androidqf/test_dumpsyspackages.py @@ -19,7 +19,7 @@ def test_parsing(self): 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) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 2 assert len(m.detected) == 0 @@ -34,7 +34,7 @@ def test_detection_pkgname(self, indicator_file): 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) + m.from_dir(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") diff --git a/tests/android_androidqf/test_dumpsysreceivers.py b/tests/android_androidqf/test_dumpsysreceivers.py index ce4b37a36..0a06bab19 100644 --- a/tests/android_androidqf/test_dumpsysreceivers.py +++ b/tests/android_androidqf/test_dumpsysreceivers.py @@ -17,7 +17,7 @@ def test_parsing(self): 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) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 4 assert len(m.detected) == 0 diff --git a/tests/android_androidqf/test_files.py b/tests/android_androidqf/test_files.py index 80981a225..de8269ca2 100644 --- a/tests/android_androidqf/test_files.py +++ b/tests/android_androidqf/test_files.py @@ -18,7 +18,7 @@ def test_androidqf_files(self): m = Files(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 diff --git a/tests/android_androidqf/test_getprop.py b/tests/android_androidqf/test_getprop.py index 4688b7346..89fb522ab 100644 --- a/tests/android_androidqf/test_getprop.py +++ b/tests/android_androidqf/test_getprop.py @@ -20,7 +20,7 @@ def test_androidqf_getprop(self): m = Getprop(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" @@ -32,7 +32,7 @@ def test_getprop_parsing_zip(self): fpath = get_artifact("androidqf.zip") m = Getprop(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" @@ -45,7 +45,7 @@ def test_androidqf_getprop_detection(self, indicator_file): m = Getprop(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") diff --git a/tests/android_androidqf/test_packages.py b/tests/android_androidqf/test_packages.py index a7fce958f..d911315ae 100644 --- a/tests/android_androidqf/test_packages.py +++ b/tests/android_androidqf/test_packages.py @@ -32,7 +32,7 @@ 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.from_dir(parent_data_path, file_list) return m diff --git a/tests/android_androidqf/test_processes.py b/tests/android_androidqf/test_processes.py index 8aacb0bd2..98b5d2af8 100644 --- a/tests/android_androidqf/test_processes.py +++ b/tests/android_androidqf/test_processes.py @@ -18,7 +18,7 @@ def test_androidqf_processes(self): m = Processes(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 diff --git a/tests/android_androidqf/test_settings.py b/tests/android_androidqf/test_settings.py index c2829014e..44ee89c77 100644 --- a/tests/android_androidqf/test_settings.py +++ b/tests/android_androidqf/test_settings.py @@ -17,7 +17,7 @@ def test_parsing(self): m = Settings(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() diff --git a/tests/android_androidqf/test_sms.py b/tests/android_androidqf/test_sms.py index 8bff79971..d7433cff3 100644 --- a/tests/android_androidqf/test_sms.py +++ b/tests/android_androidqf/test_sms.py @@ -21,7 +21,7 @@ def test_androidqf_sms(self): 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 @@ def test_androidqf_sms_encrypted_password_valid(self): ) 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 @@ def test_androidqf_sms_encrypted_password_prompt(self, mocker): ) 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 @@ def test_androidqf_sms_encrypted_password_invalid(self, caplog): ) 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 @@ def test_androidqf_sms_encrypted_no_interactive(self, caplog): ) 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 ( diff --git a/tests/android_bugreport/test_bugreport.py b/tests/android_bugreport/test_bugreport.py index cc9afecfa..98744a645 100644 --- a/tests/android_bugreport/test_bugreport.py +++ b/tests/android_bugreport/test_bugreport.py @@ -25,7 +25,7 @@ def launch_bug_report_module(self, module): 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 diff --git a/tests/test_check_android_androidqf.py b/tests/test_check_android_androidqf.py index 167b5b768..c6e422168 100644 --- a/tests/test_check_android_androidqf.py +++ b/tests/test_check_android_androidqf.py @@ -32,7 +32,8 @@ def test_check_encrypted_backup_prompt_valid(self, mocker): 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): From a08c24b02a2720ef182ed09f73f3d2ffddc0f94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Mon, 10 Feb 2025 20:32:51 +0100 Subject: [PATCH 02/37] Deduplicate modules which are run by the sub-commands. --- src/mvt/android/modules/adb/__init__.py | 16 ----- .../modules/adb/dumpsys_accessibility.py | 49 --------------- .../android/modules/adb/dumpsys_activities.py | 45 -------------- .../android/modules/adb/dumpsys_adbstate.py | 45 -------------- src/mvt/android/modules/adb/dumpsys_appops.py | 46 -------------- .../modules/adb/dumpsys_battery_daily.py | 44 ------------- .../modules/adb/dumpsys_battery_history.py | 42 ------------- src/mvt/android/modules/adb/dumpsys_dbinfo.py | 47 -------------- .../android/modules/adb/dumpsys_receivers.py | 44 ------------- src/mvt/android/modules/androidqf/__init__.py | 40 +++--------- .../androidqf/{files.py => aqf_files.py} | 9 ++- .../androidqf/{getprop.py => aqf_getprop.py} | 2 +- ...le_timestamps.py => aqf_log_timestamps.py} | 6 +- .../{packages.py => aqf_packages.py} | 2 +- .../{processes.py => aqf_processes.py} | 2 +- .../{settings.py => aqf_settings.py} | 2 +- .../androidqf/dumpsys_accessibility.py | 51 --------------- .../modules/androidqf/dumpsys_activities.py | 50 --------------- .../android/modules/androidqf/dumpsys_adb.py | 51 --------------- .../modules/androidqf/dumpsys_appops.py | 46 -------------- .../androidqf/dumpsys_battery_daily.py | 46 -------------- .../androidqf/dumpsys_battery_history.py | 46 -------------- .../modules/androidqf/dumpsys_dbinfo.py | 46 -------------- .../modules/androidqf/dumpsys_packages.py | 62 ------------------- .../androidqf/dumpsys_platform_compat.py | 44 ------------- .../modules/androidqf/dumpsys_receivers.py | 49 --------------- src/mvt/android/modules/androidqf/sms.py | 8 ++- src/mvt/android/modules/bugreport/__init__.py | 42 ++++++------- ...essibility.py => dumpsys_accessibility.py} | 2 +- .../{activities.py => dumpsys_activities.py} | 2 +- .../{adb_state.py => dumpsys_adb_state.py} | 0 .../{appops.py => dumpsys_appops.py} | 2 +- ...tery_daily.py => dumpsys_battery_daily.py} | 2 +- ..._history.py => dumpsys_battery_history.py} | 2 +- .../{dbinfo.py => dumpsys_dbinfo.py} | 2 +- .../{getprop.py => dumpsys_getprop.py} | 2 +- .../{packages.py => dumpsys_packages.py} | 2 +- ...m_compat.py => dumpsys_platform_compat.py} | 2 +- .../{receivers.py => dumpsys_receivers.py} | 2 +- .../test_dumpsys_adbstate.py | 27 -------- .../test_dumpsys_battery_daily.py | 24 ------- .../test_dumpsys_battery_history.py | 24 ------- .../android_androidqf/test_dumpsys_dbinfo.py | 24 ------- .../test_dumpsys_platform_compat.py | 23 ------- .../test_dumpsysaccessbility.py | 23 ------- tests/android_androidqf/test_dumpsysappops.py | 29 --------- .../android_androidqf/test_dumpsyspackages.py | 46 -------------- .../test_dumpsysreceivers.py | 23 ------- tests/android_androidqf/test_files.py | 4 +- tests/android_androidqf/test_getprop.py | 8 +-- tests/android_androidqf/test_packages.py | 4 +- tests/android_androidqf/test_processes.py | 4 +- tests/android_androidqf/test_settings.py | 4 +- tests/android_bugreport/test_bugreport.py | 12 ++-- 54 files changed, 80 insertions(+), 1201 deletions(-) delete mode 100644 src/mvt/android/modules/adb/dumpsys_accessibility.py delete mode 100644 src/mvt/android/modules/adb/dumpsys_activities.py delete mode 100644 src/mvt/android/modules/adb/dumpsys_adbstate.py delete mode 100644 src/mvt/android/modules/adb/dumpsys_appops.py delete mode 100644 src/mvt/android/modules/adb/dumpsys_battery_daily.py delete mode 100644 src/mvt/android/modules/adb/dumpsys_battery_history.py delete mode 100644 src/mvt/android/modules/adb/dumpsys_dbinfo.py delete mode 100644 src/mvt/android/modules/adb/dumpsys_receivers.py rename src/mvt/android/modules/androidqf/{files.py => aqf_files.py} (94%) rename src/mvt/android/modules/androidqf/{getprop.py => aqf_getprop.py} (96%) rename src/mvt/android/modules/androidqf/{logfile_timestamps.py => aqf_log_timestamps.py} (92%) rename src/mvt/android/modules/androidqf/{packages.py => aqf_packages.py} (99%) rename src/mvt/android/modules/androidqf/{processes.py => aqf_processes.py} (95%) rename src/mvt/android/modules/androidqf/{settings.py => aqf_settings.py} (96%) delete mode 100644 src/mvt/android/modules/androidqf/dumpsys_accessibility.py delete mode 100644 src/mvt/android/modules/androidqf/dumpsys_activities.py delete mode 100644 src/mvt/android/modules/androidqf/dumpsys_adb.py delete mode 100644 src/mvt/android/modules/androidqf/dumpsys_appops.py delete mode 100644 src/mvt/android/modules/androidqf/dumpsys_battery_daily.py delete mode 100644 src/mvt/android/modules/androidqf/dumpsys_battery_history.py delete mode 100644 src/mvt/android/modules/androidqf/dumpsys_dbinfo.py delete mode 100644 src/mvt/android/modules/androidqf/dumpsys_packages.py delete mode 100644 src/mvt/android/modules/androidqf/dumpsys_platform_compat.py delete mode 100644 src/mvt/android/modules/androidqf/dumpsys_receivers.py rename src/mvt/android/modules/bugreport/{accessibility.py => dumpsys_accessibility.py} (95%) rename src/mvt/android/modules/bugreport/{activities.py => dumpsys_activities.py} (95%) rename src/mvt/android/modules/bugreport/{adb_state.py => dumpsys_adb_state.py} (100%) rename src/mvt/android/modules/bugreport/{appops.py => dumpsys_appops.py} (96%) rename src/mvt/android/modules/bugreport/{battery_daily.py => dumpsys_battery_daily.py} (95%) rename src/mvt/android/modules/bugreport/{battery_history.py => dumpsys_battery_history.py} (95%) rename src/mvt/android/modules/bugreport/{dbinfo.py => dumpsys_dbinfo.py} (96%) rename src/mvt/android/modules/bugreport/{getprop.py => dumpsys_getprop.py} (97%) rename src/mvt/android/modules/bugreport/{packages.py => dumpsys_packages.py} (97%) rename src/mvt/android/modules/bugreport/{platform_compat.py => dumpsys_platform_compat.py} (95%) rename src/mvt/android/modules/bugreport/{receivers.py => dumpsys_receivers.py} (95%) delete mode 100644 tests/android_androidqf/test_dumpsys_adbstate.py delete mode 100644 tests/android_androidqf/test_dumpsys_battery_daily.py delete mode 100644 tests/android_androidqf/test_dumpsys_battery_history.py delete mode 100644 tests/android_androidqf/test_dumpsys_dbinfo.py delete mode 100644 tests/android_androidqf/test_dumpsys_platform_compat.py delete mode 100644 tests/android_androidqf/test_dumpsysaccessbility.py delete mode 100644 tests/android_androidqf/test_dumpsysappops.py delete mode 100644 tests/android_androidqf/test_dumpsyspackages.py delete mode 100644 tests/android_androidqf/test_dumpsysreceivers.py diff --git a/src/mvt/android/modules/adb/__init__.py b/src/mvt/android/modules/adb/__init__.py index 183213400..b2b436896 100644 --- a/src/mvt/android/modules/adb/__init__.py +++ b/src/mvt/android/modules/adb/__init__.py @@ -4,15 +4,7 @@ # https://license.mvt.re/1.1/ from .chrome_history import ChromeHistory -from .dumpsys_accessibility import DumpsysAccessibility -from .dumpsys_activities import DumpsysActivities -from .dumpsys_appops import DumpsysAppOps -from .dumpsys_battery_daily import DumpsysBatteryDaily -from .dumpsys_battery_history import DumpsysBatteryHistory -from .dumpsys_dbinfo import DumpsysDBInfo -from .dumpsys_adbstate import DumpsysADBState from .dumpsys_full import DumpsysFull -from .dumpsys_receivers import DumpsysReceivers from .files import Files from .getprop import Getprop from .logcat import Logcat @@ -32,15 +24,7 @@ Getprop, Settings, SELinuxStatus, - DumpsysBatteryHistory, - DumpsysBatteryDaily, - DumpsysReceivers, - DumpsysActivities, - DumpsysAccessibility, - DumpsysDBInfo, - DumpsysADBState, DumpsysFull, - DumpsysAppOps, Packages, Logcat, RootBinaries, diff --git a/src/mvt/android/modules/adb/dumpsys_accessibility.py b/src/mvt/android/modules/adb/dumpsys_accessibility.py deleted file mode 100644 index a987ae4e6..000000000 --- a/src/mvt/android/modules/adb/dumpsys_accessibility.py +++ /dev/null @@ -1,49 +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 typing import Optional - -from mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArtifact - -from .base import AndroidExtraction - - -class DumpsysAccessibility(DumpsysAccessibilityArtifact, AndroidExtraction): - """This module extracts stats on accessibility.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - self._adb_connect() - output = self._adb_command("dumpsys accessibility") - self._adb_disconnect() - - self.parse(output) - - for result in self.results: - self.log.info( - 'Found installed accessibility service "%s"', result.get("service") - ) - - self.log.info( - "Identified a total of %d accessibility services", len(self.results) - ) diff --git a/src/mvt/android/modules/adb/dumpsys_activities.py b/src/mvt/android/modules/adb/dumpsys_activities.py deleted file mode 100644 index 5125cbf42..000000000 --- a/src/mvt/android/modules/adb/dumpsys_activities.py +++ /dev/null @@ -1,45 +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 typing import Optional - -from mvt.android.artifacts.dumpsys_package_activities import ( - DumpsysPackageActivitiesArtifact, -) - -from .base import AndroidExtraction - - -class DumpsysActivities(DumpsysPackageActivitiesArtifact, AndroidExtraction): - """This module extracts details on receivers for risky activities.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - self.results = results if results else [] - - def run(self) -> None: - self._adb_connect() - output = self._adb_command("dumpsys package") - self._adb_disconnect() - self.parse(output) - - self.log.info("Extracted %d package activities", len(self.results)) diff --git a/src/mvt/android/modules/adb/dumpsys_adbstate.py b/src/mvt/android/modules/adb/dumpsys_adbstate.py deleted file mode 100644 index 0bcd8fd13..000000000 --- a/src/mvt/android/modules/adb/dumpsys_adbstate.py +++ /dev/null @@ -1,45 +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 typing import Optional - -from mvt.android.artifacts.dumpsys_adb import DumpsysADBArtifact - -from .base import AndroidExtraction - - -class DumpsysADBState(DumpsysADBArtifact, AndroidExtraction): - """This module extracts ADB keystore state.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - self._adb_connect() - output = self._adb_command("dumpsys adb", decode=False) - self._adb_disconnect() - - self.parse(output) - if self.results: - self.log.info( - "Identified a total of %d trusted ADB keys", - len(self.results[0].get("user_keys", [])), - ) diff --git a/src/mvt/android/modules/adb/dumpsys_appops.py b/src/mvt/android/modules/adb/dumpsys_appops.py deleted file mode 100644 index 7a7594e73..000000000 --- a/src/mvt/android/modules/adb/dumpsys_appops.py +++ /dev/null @@ -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 typing import Optional - -from mvt.android.artifacts.dumpsys_appops import DumpsysAppopsArtifact - -from .base import AndroidExtraction - - -class DumpsysAppOps(DumpsysAppopsArtifact, AndroidExtraction): - """This module extracts records from App-op Manager.""" - - slug = "dumpsys_appops" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - self._adb_connect() - output = self._adb_command("dumpsys appops") - self._adb_disconnect() - - self.parse(output) - - self.log.info( - "Extracted a total of %d records from app-ops manager", len(self.results) - ) diff --git a/src/mvt/android/modules/adb/dumpsys_battery_daily.py b/src/mvt/android/modules/adb/dumpsys_battery_daily.py deleted file mode 100644 index 3a9eee65b..000000000 --- a/src/mvt/android/modules/adb/dumpsys_battery_daily.py +++ /dev/null @@ -1,44 +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 typing import Optional - -from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtifact - -from .base import AndroidExtraction - - -class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, AndroidExtraction): - """This module extracts records from battery daily updates.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - self._adb_connect() - output = self._adb_command("dumpsys batterystats --daily") - self._adb_disconnect() - - self.parse(output) - - self.log.info( - "Extracted %d records from battery daily stats", len(self.results) - ) diff --git a/src/mvt/android/modules/adb/dumpsys_battery_history.py b/src/mvt/android/modules/adb/dumpsys_battery_history.py deleted file mode 100644 index aac134c99..000000000 --- a/src/mvt/android/modules/adb/dumpsys_battery_history.py +++ /dev/null @@ -1,42 +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 typing import Optional - -from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryArtifact - -from .base import AndroidExtraction - - -class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, AndroidExtraction): - """This module extracts records from battery history events.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - self._adb_connect() - output = self._adb_command("dumpsys batterystats --history") - self._adb_disconnect() - - self.parse(output) - - self.log.info("Extracted %d records from battery history", len(self.results)) diff --git a/src/mvt/android/modules/adb/dumpsys_dbinfo.py b/src/mvt/android/modules/adb/dumpsys_dbinfo.py deleted file mode 100644 index e6b772b80..000000000 --- a/src/mvt/android/modules/adb/dumpsys_dbinfo.py +++ /dev/null @@ -1,47 +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 typing import Optional - -from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfoArtifact - -from .base import AndroidExtraction - - -class DumpsysDBInfo(DumpsysDBInfoArtifact, AndroidExtraction): - """This module extracts records from battery daily updates.""" - - slug = "dumpsys_dbinfo" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - self._adb_connect() - output = self._adb_command("dumpsys dbinfo") - self._adb_disconnect() - - self.parse(output) - - self.log.info( - "Extracted a total of %d records from database information", - len(self.results), - ) diff --git a/src/mvt/android/modules/adb/dumpsys_receivers.py b/src/mvt/android/modules/adb/dumpsys_receivers.py deleted file mode 100644 index c4759c4b6..000000000 --- a/src/mvt/android/modules/adb/dumpsys_receivers.py +++ /dev/null @@ -1,44 +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 typing import Optional - -from mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact - -from .base import AndroidExtraction - - -class DumpsysReceivers(DumpsysReceiversArtifact, AndroidExtraction): - """This module extracts details on receivers for risky activities.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - self.results = results if results else {} - - def run(self) -> None: - self._adb_connect() - - output = self._adb_command("dumpsys package") - self.parse(output) - - self._adb_disconnect() - self.log.info("Extracted receivers for %d intents", len(self.results)) diff --git a/src/mvt/android/modules/androidqf/__init__.py b/src/mvt/android/modules/androidqf/__init__.py index cdb0af8be..c6a3e1fe7 100644 --- a/src/mvt/android/modules/androidqf/__init__.py +++ b/src/mvt/android/modules/androidqf/__init__.py @@ -3,38 +3,18 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ -from .dumpsys_accessibility import DumpsysAccessibility -from .dumpsys_activities import DumpsysActivities -from .dumpsys_appops import DumpsysAppops -from .dumpsys_battery_daily import DumpsysBatteryDaily -from .dumpsys_battery_history import DumpsysBatteryHistory -from .dumpsys_dbinfo import DumpsysDBInfo -from .dumpsys_packages import DumpsysPackages -from .dumpsys_receivers import DumpsysReceivers -from .dumpsys_adb import DumpsysADBState -from .getprop import Getprop -from .packages import Packages -from .dumpsys_platform_compat import DumpsysPlatformCompat -from .processes import Processes -from .settings import Settings +from .aqf_getprop import AQFGetProp +from .aqf_packages import AQFPackages +from .aqf_processes import AQFProcesses +from .aqf_settings import AQFSettings +from .aqf_files import AQFFiles from .sms import SMS -from .files import Files ANDROIDQF_MODULES = [ - DumpsysActivities, - DumpsysReceivers, - DumpsysAccessibility, - DumpsysAppops, - DumpsysDBInfo, - DumpsysBatteryDaily, - DumpsysBatteryHistory, - DumpsysADBState, - Packages, - DumpsysPlatformCompat, - Processes, - Getprop, - Settings, + AQFPackages, + AQFProcesses, + AQFGetProp, + AQFSettings, + AQFFiles, SMS, - DumpsysPackages, - Files, ] diff --git a/src/mvt/android/modules/androidqf/files.py b/src/mvt/android/modules/androidqf/aqf_files.py similarity index 94% rename from src/mvt/android/modules/androidqf/files.py rename to src/mvt/android/modules/androidqf/aqf_files.py index 22b832c33..90eb3b880 100644 --- a/src/mvt/android/modules/androidqf/files.py +++ b/src/mvt/android/modules/androidqf/aqf_files.py @@ -21,8 +21,13 @@ ] -class Files(AndroidQFModule): - """This module analyse list of files""" +class AQFFiles(AndroidQFModule): + """ + This module analyzes the files.json dump generated by AndroidQF. + + The format needs to be kept in sync with the AndroidQF module code. + https://github.com/mvt-project/androidqf/blob/main/android-collector/cmd/find.go#L28 + """ def __init__( self, diff --git a/src/mvt/android/modules/androidqf/getprop.py b/src/mvt/android/modules/androidqf/aqf_getprop.py similarity index 96% rename from src/mvt/android/modules/androidqf/getprop.py rename to src/mvt/android/modules/androidqf/aqf_getprop.py index e14abd9bb..35514f836 100644 --- a/src/mvt/android/modules/androidqf/getprop.py +++ b/src/mvt/android/modules/androidqf/aqf_getprop.py @@ -11,7 +11,7 @@ from .base import AndroidQFModule -class Getprop(GetPropArtifact, AndroidQFModule): +class AQFGetProp(GetPropArtifact, AndroidQFModule): """This module extracts data from get properties.""" def __init__( diff --git a/src/mvt/android/modules/androidqf/logfile_timestamps.py b/src/mvt/android/modules/androidqf/aqf_log_timestamps.py similarity index 92% rename from src/mvt/android/modules/androidqf/logfile_timestamps.py rename to src/mvt/android/modules/androidqf/aqf_log_timestamps.py index b37851d36..e5a141044 100644 --- a/src/mvt/android/modules/androidqf/logfile_timestamps.py +++ b/src/mvt/android/modules/androidqf/aqf_log_timestamps.py @@ -13,10 +13,10 @@ from mvt.android.artifacts.file_timestamps import FileTimestampsArtifact -class LogsFileTimestamps(FileTimestampsArtifact, AndroidQFModule): - """This module extracts records from battery daily updates.""" +class AQFLogTimestamps(FileTimestampsArtifact, AndroidQFModule): + """This module creates timeline for log files extracted by AQF.""" - slug = "logfile_timestamps" + slug = "aqf_log_timestamps" def __init__( self, diff --git a/src/mvt/android/modules/androidqf/packages.py b/src/mvt/android/modules/androidqf/aqf_packages.py similarity index 99% rename from src/mvt/android/modules/androidqf/packages.py rename to src/mvt/android/modules/androidqf/aqf_packages.py index 1d36777d4..500b3d4ba 100644 --- a/src/mvt/android/modules/androidqf/packages.py +++ b/src/mvt/android/modules/androidqf/aqf_packages.py @@ -19,7 +19,7 @@ from .base import AndroidQFModule -class Packages(AndroidQFModule): +class AQFPackages(AndroidQFModule): """This module examines the installed packages in packages.json""" def __init__( diff --git a/src/mvt/android/modules/androidqf/processes.py b/src/mvt/android/modules/androidqf/aqf_processes.py similarity index 95% rename from src/mvt/android/modules/androidqf/processes.py rename to src/mvt/android/modules/androidqf/aqf_processes.py index f2c5e0880..3faabb432 100644 --- a/src/mvt/android/modules/androidqf/processes.py +++ b/src/mvt/android/modules/androidqf/aqf_processes.py @@ -11,7 +11,7 @@ from .base import AndroidQFModule -class Processes(ProcessesArtifact, AndroidQFModule): +class AQFProcesses(ProcessesArtifact, AndroidQFModule): """This module analyse running processes""" def __init__( diff --git a/src/mvt/android/modules/androidqf/settings.py b/src/mvt/android/modules/androidqf/aqf_settings.py similarity index 96% rename from src/mvt/android/modules/androidqf/settings.py rename to src/mvt/android/modules/androidqf/aqf_settings.py index 79f55efb4..46a70fb23 100644 --- a/src/mvt/android/modules/androidqf/settings.py +++ b/src/mvt/android/modules/androidqf/aqf_settings.py @@ -11,7 +11,7 @@ from .base import AndroidQFModule -class Settings(SettingsArtifact, AndroidQFModule): +class AQFSettings(SettingsArtifact, AndroidQFModule): """This module analyse setting files""" def __init__( diff --git a/src/mvt/android/modules/androidqf/dumpsys_accessibility.py b/src/mvt/android/modules/androidqf/dumpsys_accessibility.py deleted file mode 100644 index 0712ef447..000000000 --- a/src/mvt/android/modules/androidqf/dumpsys_accessibility.py +++ /dev/null @@ -1,51 +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 typing import Optional - -from mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArtifact - -from .base import AndroidQFModule - - -class DumpsysAccessibility(DumpsysAccessibilityArtifact, AndroidQFModule): - """This module analyses dumpsys accessibility""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") - if not dumpsys_file: - return - - data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace") - content = self.extract_dumpsys_section(data, "DUMP OF SERVICE accessibility:") - self.parse(content) - - for result in self.results: - self.log.info( - 'Found installed accessibility service "%s"', result.get("service") - ) - - self.log.info( - "Identified a total of %d accessibility services", len(self.results) - ) diff --git a/src/mvt/android/modules/androidqf/dumpsys_activities.py b/src/mvt/android/modules/androidqf/dumpsys_activities.py deleted file mode 100644 index 950d0e5e5..000000000 --- a/src/mvt/android/modules/androidqf/dumpsys_activities.py +++ /dev/null @@ -1,50 +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 typing import Optional - -from mvt.android.artifacts.dumpsys_package_activities import ( - DumpsysPackageActivitiesArtifact, -) - -from .base import AndroidQFModule - - -class DumpsysActivities(DumpsysPackageActivitiesArtifact, AndroidQFModule): - """This module extracts details on receivers for risky activities.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - self.results = results if results else [] - - def run(self) -> None: - dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") - if not dumpsys_file: - return - - # Get data and extract the dumpsys section - data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace") - content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:") - # Parse it - self.parse(content) - - self.log.info("Extracted %d package activities", len(self.results)) diff --git a/src/mvt/android/modules/androidqf/dumpsys_adb.py b/src/mvt/android/modules/androidqf/dumpsys_adb.py deleted file mode 100644 index 10d8a4def..000000000 --- a/src/mvt/android/modules/androidqf/dumpsys_adb.py +++ /dev/null @@ -1,51 +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 typing import Optional - -from mvt.android.artifacts.dumpsys_adb import DumpsysADBArtifact - -from .base import AndroidQFModule - - -class DumpsysADBState(DumpsysADBArtifact, AndroidQFModule): - """This module extracts ADB keystore state.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") - if not dumpsys_file: - return - - full_dumpsys = self._get_file_content(dumpsys_file[0]) - content = self.extract_dumpsys_section( - full_dumpsys, - b"DUMP OF SERVICE adb:", - binary=True, - ) - self.parse(content) - if self.results: - self.log.info( - "Identified a total of %d trusted ADB keys", - len(self.results[0].get("user_keys", [])), - ) diff --git a/src/mvt/android/modules/androidqf/dumpsys_appops.py b/src/mvt/android/modules/androidqf/dumpsys_appops.py deleted file mode 100644 index 350b5c86e..000000000 --- a/src/mvt/android/modules/androidqf/dumpsys_appops.py +++ /dev/null @@ -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 typing import Optional - -from mvt.android.artifacts.dumpsys_appops import DumpsysAppopsArtifact - -from .base import AndroidQFModule - - -class DumpsysAppops(DumpsysAppopsArtifact, AndroidQFModule): - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") - if not dumpsys_file: - return - - # Extract section - data = self._get_file_content(dumpsys_file[0]) - section = self.extract_dumpsys_section( - data.decode("utf-8", errors="replace"), "DUMP OF SERVICE appops:" - ) - - # Parse it - self.parse(section) - self.log.info("Identified %d applications in AppOps Manager", len(self.results)) diff --git a/src/mvt/android/modules/androidqf/dumpsys_battery_daily.py b/src/mvt/android/modules/androidqf/dumpsys_battery_daily.py deleted file mode 100644 index 4a1917851..000000000 --- a/src/mvt/android/modules/androidqf/dumpsys_battery_daily.py +++ /dev/null @@ -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 typing import Optional - -from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtifact - -from .base import AndroidQFModule - - -class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, AndroidQFModule): - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") - if not dumpsys_file: - return - - # Extract section - data = self._get_file_content(dumpsys_file[0]) - section = self.extract_dumpsys_section( - data.decode("utf-8", errors="replace"), "DUMP OF SERVICE batterystats:" - ) - - # Parse it - self.parse(section) - self.log.info("Extracted a total of %d battery daily stats", len(self.results)) diff --git a/src/mvt/android/modules/androidqf/dumpsys_battery_history.py b/src/mvt/android/modules/androidqf/dumpsys_battery_history.py deleted file mode 100644 index 4a4bef3a5..000000000 --- a/src/mvt/android/modules/androidqf/dumpsys_battery_history.py +++ /dev/null @@ -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 typing import Optional - -from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryArtifact - -from .base import AndroidQFModule - - -class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, AndroidQFModule): - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") - if not dumpsys_file: - return - - # Extract section - data = self._get_file_content(dumpsys_file[0]) - section = self.extract_dumpsys_section( - data.decode("utf-8", errors="replace"), "DUMP OF SERVICE batterystats:" - ) - - # Parse it - self.parse(section) - self.log.info("Extracted a total of %d battery daily stats", len(self.results)) diff --git a/src/mvt/android/modules/androidqf/dumpsys_dbinfo.py b/src/mvt/android/modules/androidqf/dumpsys_dbinfo.py deleted file mode 100644 index 09c8f6fd0..000000000 --- a/src/mvt/android/modules/androidqf/dumpsys_dbinfo.py +++ /dev/null @@ -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 typing import Optional - -from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfoArtifact - -from .base import AndroidQFModule - - -class DumpsysDBInfo(DumpsysDBInfoArtifact, AndroidQFModule): - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") - if not dumpsys_file: - return - - # Extract dumpsys DBInfo section - data = self._get_file_content(dumpsys_file[0]) - section = self.extract_dumpsys_section( - data.decode("utf-8", errors="replace"), "DUMP OF SERVICE dbinfo:" - ) - - # Parse it - self.parse(section) - self.log.info("Identified %d DB Info entries", len(self.results)) diff --git a/src/mvt/android/modules/androidqf/dumpsys_packages.py b/src/mvt/android/modules/androidqf/dumpsys_packages.py deleted file mode 100644 index 8df7144e3..000000000 --- a/src/mvt/android/modules/androidqf/dumpsys_packages.py +++ /dev/null @@ -1,62 +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 typing import Any, Dict, List, Optional - -from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact -from mvt.android.modules.adb.packages import ( - DANGEROUS_PERMISSIONS, - DANGEROUS_PERMISSIONS_THRESHOLD, -) - -from .base import AndroidQFModule - - -class DumpsysPackages(DumpsysPackagesArtifact, AndroidQFModule): - """This module analyse dumpsys packages""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[List[Dict[str, Any]]] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") - if len(dumpsys_file) != 1: - self.log.info("Dumpsys file not found") - return - - data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace") - content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:") - self.parse(content) - - for result in self.results: - dangerous_permissions_count = 0 - for perm in result["permissions"]: - if perm["name"] in DANGEROUS_PERMISSIONS: - dangerous_permissions_count += 1 - - if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD: - self.log.info( - 'Found package "%s" requested %d potentially dangerous permissions', - result["package_name"], - dangerous_permissions_count, - ) - - self.log.info("Extracted details on %d packages", len(self.results)) diff --git a/src/mvt/android/modules/androidqf/dumpsys_platform_compat.py b/src/mvt/android/modules/androidqf/dumpsys_platform_compat.py deleted file mode 100644 index 869c476f5..000000000 --- a/src/mvt/android/modules/androidqf/dumpsys_platform_compat.py +++ /dev/null @@ -1,44 +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 typing import Optional - -from mvt.android.artifacts.dumpsys_platform_compat import DumpsysPlatformCompatArtifact - -from .base import AndroidQFModule - - -class DumpsysPlatformCompat(DumpsysPlatformCompatArtifact, AndroidQFModule): - """This module extracts details on uninstalled apps.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") - if not dumpsys_file: - return - - data = self._get_file_content(dumpsys_file[0]).decode("utf-8", errors="replace") - content = self.extract_dumpsys_section(data, "DUMP OF SERVICE platform_compat:") - self.parse(content) - - self.log.info("Found %d uninstalled apps", len(self.results)) diff --git a/src/mvt/android/modules/androidqf/dumpsys_receivers.py b/src/mvt/android/modules/androidqf/dumpsys_receivers.py deleted file mode 100644 index 9c64d2cf1..000000000 --- a/src/mvt/android/modules/androidqf/dumpsys_receivers.py +++ /dev/null @@ -1,49 +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 typing import Any, Dict, List, Optional, Union - -from mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact - -from .base import AndroidQFModule - - -class DumpsysReceivers(DumpsysReceiversArtifact, AndroidQFModule): - """This module analyse dumpsys receivers""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Union[List[Any], Dict[str, Any], None] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - self.results = results if results else {} - - def run(self) -> None: - dumpsys_file = self._get_files_by_pattern("*/dumpsys.txt") - if not dumpsys_file: - return - data = self._get_file_content(dumpsys_file[0]) - - dumpsys_section = self.extract_dumpsys_section( - data.decode("utf-8", errors="replace"), "DUMP OF SERVICE package:" - ) - - self.parse(dumpsys_section) - - self.log.info("Extracted receivers for %d intents", len(self.results)) diff --git a/src/mvt/android/modules/androidqf/sms.py b/src/mvt/android/modules/androidqf/sms.py index d7e3a9cb6..893e5178b 100644 --- a/src/mvt/android/modules/androidqf/sms.py +++ b/src/mvt/android/modules/androidqf/sms.py @@ -19,7 +19,13 @@ class SMS(AndroidQFModule): - """This module analyse SMS file in backup""" + """ + This module analyse SMS file in backup + + XXX: We should also de-duplicate this AQF module, but first we + need to add tests for loading encrypted SMS backups through the backup + sub-module. + """ def __init__( self, diff --git a/src/mvt/android/modules/bugreport/__init__.py b/src/mvt/android/modules/bugreport/__init__.py index b5a124779..1594af9c3 100644 --- a/src/mvt/android/modules/bugreport/__init__.py +++ b/src/mvt/android/modules/bugreport/__init__.py @@ -3,31 +3,31 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ -from .accessibility import Accessibility -from .activities import Activities -from .appops import Appops -from .battery_daily import BatteryDaily -from .battery_history import BatteryHistory -from .dbinfo import DBInfo -from .getprop import Getprop -from .packages import Packages -from .platform_compat import PlatformCompat -from .receivers import Receivers -from .adb_state import DumpsysADBState +from .dumpsys_accessibility import DumpsysAccessibility +from .dumpsys_activities import DumpsysActivities +from .dumpsys_appops import DumpsysAppops +from .dumpsys_battery_daily import DumpsysBatteryDaily +from .dumpsys_battery_history import DumpsysBatteryHistory +from .dumpsys_dbinfo import DumpsysDBInfo +from .dumpsys_getprop import DumpsysGetProp +from .dumpsys_packages import DumpsysPackages +from .dumpsys_platform_compat import DumpsysPlatformCompat +from .dumpsys_receivers import DumpsysReceivers +from .dumpsys_adb_state import DumpsysADBState from .fs_timestamps import BugReportTimestamps from .tombstones import Tombstones BUGREPORT_MODULES = [ - Accessibility, - Activities, - Appops, - BatteryDaily, - BatteryHistory, - DBInfo, - Getprop, - Packages, - PlatformCompat, - Receivers, + DumpsysAccessibility, + DumpsysActivities, + DumpsysAppops, + DumpsysBatteryDaily, + DumpsysBatteryHistory, + DumpsysDBInfo, + DumpsysGetProp, + DumpsysPackages, + DumpsysPlatformCompat, + DumpsysReceivers, DumpsysADBState, BugReportTimestamps, Tombstones, diff --git a/src/mvt/android/modules/bugreport/accessibility.py b/src/mvt/android/modules/bugreport/dumpsys_accessibility.py similarity index 95% rename from src/mvt/android/modules/bugreport/accessibility.py rename to src/mvt/android/modules/bugreport/dumpsys_accessibility.py index 7d30eb00b..e141b2fe6 100644 --- a/src/mvt/android/modules/bugreport/accessibility.py +++ b/src/mvt/android/modules/bugreport/dumpsys_accessibility.py @@ -11,7 +11,7 @@ from .base import BugReportModule -class Accessibility(DumpsysAccessibilityArtifact, BugReportModule): +class DumpsysAccessibility(DumpsysAccessibilityArtifact, BugReportModule): """This module extracts stats on accessibility.""" def __init__( diff --git a/src/mvt/android/modules/bugreport/activities.py b/src/mvt/android/modules/bugreport/dumpsys_activities.py similarity index 95% rename from src/mvt/android/modules/bugreport/activities.py rename to src/mvt/android/modules/bugreport/dumpsys_activities.py index c2a20dd81..a58c6f48c 100644 --- a/src/mvt/android/modules/bugreport/activities.py +++ b/src/mvt/android/modules/bugreport/dumpsys_activities.py @@ -13,7 +13,7 @@ from .base import BugReportModule -class Activities(DumpsysPackageActivitiesArtifact, BugReportModule): +class DumpsysActivities(DumpsysPackageActivitiesArtifact, BugReportModule): """This module extracts details on receivers for risky activities.""" def __init__( diff --git a/src/mvt/android/modules/bugreport/adb_state.py b/src/mvt/android/modules/bugreport/dumpsys_adb_state.py similarity index 100% rename from src/mvt/android/modules/bugreport/adb_state.py rename to src/mvt/android/modules/bugreport/dumpsys_adb_state.py diff --git a/src/mvt/android/modules/bugreport/appops.py b/src/mvt/android/modules/bugreport/dumpsys_appops.py similarity index 96% rename from src/mvt/android/modules/bugreport/appops.py rename to src/mvt/android/modules/bugreport/dumpsys_appops.py index 4fb1e7f42..96b479672 100644 --- a/src/mvt/android/modules/bugreport/appops.py +++ b/src/mvt/android/modules/bugreport/dumpsys_appops.py @@ -11,7 +11,7 @@ from .base import BugReportModule -class Appops(DumpsysAppopsArtifact, BugReportModule): +class DumpsysAppops(DumpsysAppopsArtifact, BugReportModule): """This module extracts information on package from App-Ops Manager.""" def __init__( diff --git a/src/mvt/android/modules/bugreport/battery_daily.py b/src/mvt/android/modules/bugreport/dumpsys_battery_daily.py similarity index 95% rename from src/mvt/android/modules/bugreport/battery_daily.py rename to src/mvt/android/modules/bugreport/dumpsys_battery_daily.py index 4fdcf744d..7fc832981 100644 --- a/src/mvt/android/modules/bugreport/battery_daily.py +++ b/src/mvt/android/modules/bugreport/dumpsys_battery_daily.py @@ -11,7 +11,7 @@ from .base import BugReportModule -class BatteryDaily(DumpsysBatteryDailyArtifact, BugReportModule): +class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, BugReportModule): """This module extracts records from battery daily updates.""" def __init__( diff --git a/src/mvt/android/modules/bugreport/battery_history.py b/src/mvt/android/modules/bugreport/dumpsys_battery_history.py similarity index 95% rename from src/mvt/android/modules/bugreport/battery_history.py rename to src/mvt/android/modules/bugreport/dumpsys_battery_history.py index 968bbbefc..729f801aa 100644 --- a/src/mvt/android/modules/bugreport/battery_history.py +++ b/src/mvt/android/modules/bugreport/dumpsys_battery_history.py @@ -11,7 +11,7 @@ from .base import BugReportModule -class BatteryHistory(DumpsysBatteryHistoryArtifact, BugReportModule): +class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, BugReportModule): """This module extracts records from battery daily updates.""" def __init__( diff --git a/src/mvt/android/modules/bugreport/dbinfo.py b/src/mvt/android/modules/bugreport/dumpsys_dbinfo.py similarity index 96% rename from src/mvt/android/modules/bugreport/dbinfo.py rename to src/mvt/android/modules/bugreport/dumpsys_dbinfo.py index 780d9fcca..73902bb34 100644 --- a/src/mvt/android/modules/bugreport/dbinfo.py +++ b/src/mvt/android/modules/bugreport/dumpsys_dbinfo.py @@ -11,7 +11,7 @@ from .base import BugReportModule -class DBInfo(DumpsysDBInfoArtifact, BugReportModule): +class DumpsysDBInfo(DumpsysDBInfoArtifact, BugReportModule): """This module extracts records from battery daily updates.""" slug = "dbinfo" diff --git a/src/mvt/android/modules/bugreport/getprop.py b/src/mvt/android/modules/bugreport/dumpsys_getprop.py similarity index 97% rename from src/mvt/android/modules/bugreport/getprop.py rename to src/mvt/android/modules/bugreport/dumpsys_getprop.py index 106d63c7e..acec15ce0 100644 --- a/src/mvt/android/modules/bugreport/getprop.py +++ b/src/mvt/android/modules/bugreport/dumpsys_getprop.py @@ -11,7 +11,7 @@ from .base import BugReportModule -class Getprop(GetPropArtifact, BugReportModule): +class DumpsysGetProp(GetPropArtifact, BugReportModule): """This module extracts device properties from getprop command.""" def __init__( diff --git a/src/mvt/android/modules/bugreport/packages.py b/src/mvt/android/modules/bugreport/dumpsys_packages.py similarity index 97% rename from src/mvt/android/modules/bugreport/packages.py rename to src/mvt/android/modules/bugreport/dumpsys_packages.py index f1b9d63be..fccf10210 100644 --- a/src/mvt/android/modules/bugreport/packages.py +++ b/src/mvt/android/modules/bugreport/dumpsys_packages.py @@ -12,7 +12,7 @@ from .base import BugReportModule -class Packages(DumpsysPackagesArtifact, BugReportModule): +class DumpsysPackages(DumpsysPackagesArtifact, BugReportModule): """This module extracts details on receivers for risky activities.""" def __init__( diff --git a/src/mvt/android/modules/bugreport/platform_compat.py b/src/mvt/android/modules/bugreport/dumpsys_platform_compat.py similarity index 95% rename from src/mvt/android/modules/bugreport/platform_compat.py rename to src/mvt/android/modules/bugreport/dumpsys_platform_compat.py index fadac9216..e9d10e66d 100644 --- a/src/mvt/android/modules/bugreport/platform_compat.py +++ b/src/mvt/android/modules/bugreport/dumpsys_platform_compat.py @@ -11,7 +11,7 @@ from mvt.android.modules.bugreport.base import BugReportModule -class PlatformCompat(DumpsysPlatformCompatArtifact, BugReportModule): +class DumpsysPlatformCompat(DumpsysPlatformCompatArtifact, BugReportModule): """This module extracts details on uninstalled apps.""" def __init__( diff --git a/src/mvt/android/modules/bugreport/receivers.py b/src/mvt/android/modules/bugreport/dumpsys_receivers.py similarity index 95% rename from src/mvt/android/modules/bugreport/receivers.py rename to src/mvt/android/modules/bugreport/dumpsys_receivers.py index 57a87ce89..591af2f7c 100644 --- a/src/mvt/android/modules/bugreport/receivers.py +++ b/src/mvt/android/modules/bugreport/dumpsys_receivers.py @@ -11,7 +11,7 @@ from .base import BugReportModule -class Receivers(DumpsysReceiversArtifact, BugReportModule): +class DumpsysReceivers(DumpsysReceiversArtifact, BugReportModule): """This module extracts details on receivers for risky activities.""" def __init__( diff --git a/tests/android_androidqf/test_dumpsys_adbstate.py b/tests/android_androidqf/test_dumpsys_adbstate.py deleted file mode 100644 index a3ac72e14..000000000 --- a/tests/android_androidqf/test_dumpsys_adbstate.py +++ /dev/null @@ -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_dir(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 diff --git a/tests/android_androidqf/test_dumpsys_battery_daily.py b/tests/android_androidqf/test_dumpsys_battery_daily.py deleted file mode 100644 index f82f3303c..000000000 --- a/tests/android_androidqf/test_dumpsys_battery_daily.py +++ /dev/null @@ -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_dir(parent_path, files) - run_module(m) - assert len(m.results) == 3 - assert len(m.timeline) == 3 - assert len(m.detected) == 0 diff --git a/tests/android_androidqf/test_dumpsys_battery_history.py b/tests/android_androidqf/test_dumpsys_battery_history.py deleted file mode 100644 index fd1d0aeb8..000000000 --- a/tests/android_androidqf/test_dumpsys_battery_history.py +++ /dev/null @@ -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_dir(parent_path, files) - run_module(m) - assert len(m.results) == 6 - assert len(m.timeline) == 0 - assert len(m.detected) == 0 diff --git a/tests/android_androidqf/test_dumpsys_dbinfo.py b/tests/android_androidqf/test_dumpsys_dbinfo.py deleted file mode 100644 index 371a3bcc3..000000000 --- a/tests/android_androidqf/test_dumpsys_dbinfo.py +++ /dev/null @@ -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_dir(parent_path, files) - run_module(m) - assert len(m.results) == 6 - assert len(m.timeline) == 0 - assert len(m.detected) == 0 diff --git a/tests/android_androidqf/test_dumpsys_platform_compat.py b/tests/android_androidqf/test_dumpsys_platform_compat.py deleted file mode 100644 index bddc32275..000000000 --- a/tests/android_androidqf/test_dumpsys_platform_compat.py +++ /dev/null @@ -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_dir(parent_path, files) - run_module(m) - assert len(m.results) == 2 - assert len(m.detected) == 0 diff --git a/tests/android_androidqf/test_dumpsysaccessbility.py b/tests/android_androidqf/test_dumpsysaccessbility.py deleted file mode 100644 index b43727573..000000000 --- a/tests/android_androidqf/test_dumpsysaccessbility.py +++ /dev/null @@ -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_dir(parent_path, files) - run_module(m) - assert len(m.results) == 4 - assert len(m.detected) == 0 diff --git a/tests/android_androidqf/test_dumpsysappops.py b/tests/android_androidqf/test_dumpsysappops.py deleted file mode 100644 index ce74d53a8..000000000 --- a/tests/android_androidqf/test_dumpsysappops.py +++ /dev/null @@ -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_dir(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 diff --git a/tests/android_androidqf/test_dumpsyspackages.py b/tests/android_androidqf/test_dumpsyspackages.py deleted file mode 100644 index e801b0a75..000000000 --- a/tests/android_androidqf/test_dumpsyspackages.py +++ /dev/null @@ -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_dir(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_dir(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" diff --git a/tests/android_androidqf/test_dumpsysreceivers.py b/tests/android_androidqf/test_dumpsysreceivers.py deleted file mode 100644 index 0a06bab19..000000000 --- a/tests/android_androidqf/test_dumpsysreceivers.py +++ /dev/null @@ -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_dir(parent_path, files) - run_module(m) - assert len(m.results) == 4 - assert len(m.detected) == 0 diff --git a/tests/android_androidqf/test_files.py b/tests/android_androidqf/test_files.py index de8269ca2..c0d45b51c 100644 --- a/tests/android_androidqf/test_files.py +++ b/tests/android_androidqf/test_files.py @@ -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,7 +15,7 @@ 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_dir(parent_path, files) diff --git a/tests/android_androidqf/test_getprop.py b/tests/android_androidqf/test_getprop.py index 89fb522ab..3947acd4f 100644 --- a/tests/android_androidqf/test_getprop.py +++ b/tests/android_androidqf/test_getprop.py @@ -7,7 +7,7 @@ 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,7 +17,7 @@ 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_dir(parent_path, files) @@ -30,7 +30,7 @@ def test_androidqf_getprop(self): 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(archive, archive.namelist()) run_module(m) @@ -42,7 +42,7 @@ def test_getprop_parsing_zip(self): 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_dir(parent_path, files) diff --git a/tests/android_androidqf/test_packages.py b/tests/android_androidqf/test_packages.py index d911315ae..966d8a6ce 100644 --- a/tests/android_androidqf/test_packages.py +++ b/tests/android_androidqf/test_packages.py @@ -8,7 +8,7 @@ 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,7 +31,7 @@ def file_list(data_path): @pytest.fixture() def module(parent_data_path, file_list): - m = Packages(target_path=parent_data_path, log=logging) + m = AQFPackages(target_path=parent_data_path, log=logging) m.from_dir(parent_data_path, file_list) return m diff --git a/tests/android_androidqf/test_processes.py b/tests/android_androidqf/test_processes.py index 98b5d2af8..bcd401339 100644 --- a/tests/android_androidqf/test_processes.py +++ b/tests/android_androidqf/test_processes.py @@ -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,7 +15,7 @@ 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_dir(parent_path, files) diff --git a/tests/android_androidqf/test_settings.py b/tests/android_androidqf/test_settings.py index 44ee89c77..75527a791 100644 --- a/tests/android_androidqf/test_settings.py +++ b/tests/android_androidqf/test_settings.py @@ -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,7 +14,7 @@ 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_dir(parent_path, files) diff --git a/tests/android_bugreport/test_bugreport.py b/tests/android_bugreport/test_bugreport.py index 98744a645..8abc8961f 100644 --- a/tests/android_bugreport/test_bugreport.py +++ b/tests/android_bugreport/test_bugreport.py @@ -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.common.module import run_module from ..utils import get_artifact_folder @@ -30,7 +30,7 @@ def launch_bug_report_module(self, module): 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 @@ -41,7 +41,7 @@ def test_appops_module(self): 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"] @@ -52,5 +52,5 @@ def test_packages_module(self): 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 From 4c1cdf5129c1b9d1dfa5d74f15c5b623dd0ffea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Tue, 11 Feb 2025 15:04:48 +0100 Subject: [PATCH 03/37] Raise the proper NoAndroidQFBackup exception when a back-up isn't found --- src/mvt/android/cmd_check_androidqf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mvt/android/cmd_check_androidqf.py b/src/mvt/android/cmd_check_androidqf.py index 628548c59..580b5e2d1 100644 --- a/src/mvt/android/cmd_check_androidqf.py +++ b/src/mvt/android/cmd_check_androidqf.py @@ -161,7 +161,7 @@ def run_bugreport_cmd(self) -> bool: def run_backup_cmd(self) -> bool: try: backup = self.load_backup() - except NoAndroidQFBugReport: + except NoAndroidQFBackup: self.log.warning( "Skipping backup modules as no backup.ab found in AndroidQF data." ) From 064b9fbeb9a9f6cba6aa42c2dfec634cf9fb64f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Sat, 15 Feb 2025 22:47:42 +0100 Subject: [PATCH 04/37] Remove check-adb command and update docs --- docs/android/adb.md | 48 ++++-------- docs/android/methodology.md | 46 +++++++++-- docs/docker.md | 19 +---- src/mvt/android/cli.py | 128 ++----------------------------- src/mvt/android/cmd_check_adb.py | 44 ----------- src/mvt/common/help.py | 18 ++--- src/mvt/common/version.py | 2 +- src/mvt/common/virustotal.py | 52 ------------- 8 files changed, 71 insertions(+), 286 deletions(-) delete mode 100644 src/mvt/android/cmd_check_adb.py delete mode 100644 src/mvt/common/virustotal.py diff --git a/docs/android/adb.md b/docs/android/adb.md index d5c066007..8e4d0700c 100644 --- a/docs/android/adb.md +++ b/docs/android/adb.md @@ -1,42 +1,26 @@ -# Check over ADB - -In order to check an Android device over the [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) you will first need to install [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools). If you have installed [Android Studio](https://developer.android.com/studio/) you should already have access to `adb` and other utilities. - -While many Linux distributions already package Android Platform Tools (for example `android-platform-tools-base` on Debian), it is preferable to install the most recent version from the official website. Packaged versions might be outdated and incompatible with most recent Android handsets. - -Next you will need to enable debugging on the Android device you are testing. [Please follow the official instructions on how to do so.](https://developer.android.com/studio/command-line/adb) - -## Connecting over USB - -The easiest way to check the device is over a USB transport. You will need to have USB debugging enabled and the device plugged into your computer. If everything is configured appropriately you should see your device when launching the command `adb devices`. - -Now you can try launching MVT with: - -```bash -mvt-android check-adb --output /path/to/results -``` - -If you have previously started an adb daemon MVT will alert you and require you to kill it with `adb kill-server` and relaunch the command. +# Deprecation of ADB command in MVT !!! warning - MVT relies on the Python library [adb-shell](https://pypi.org/project/adb-shell/) to connect to an Android device, which relies on libusb for the USB transport. Because of known driver issues, Windows users [are recommended](https://github.com/JeffLIrion/adb_shell/issues/118) to install appropriate drivers using [Zadig](https://zadig.akeo.ie/). Alternatively, an easier option might be to use the TCP transport and connect over Wi-Fi as describe next. -## Connecting over Wi-FI + The `mvt-android check-adb` command has been deprecated and removed from MVT. + +The ability to analyze Android devices over ADB (`mvt-android check-adb`) has been removed from MVT due to several technical and forensic limitations. -When connecting to the device over USB is not possible or not working properly, an alternative option is to connect over the network. In order to do so, first launch an adb daemon at a fixed port number: +## Reasons for Deprecation -```bash -adb tcpip 5555 -``` +1. **Inconsistent Data Collection Across Devices** + Android devices vary significantly in their system architecture, security policies, and available diagnostic logs. This inconsistency makes it difficult to ensure that MVT can reliably collect necessary forensic data across all devices. -Then you can specify the IP address of the phone with the adb port number to MVT like so: +2. **Incomplete Forensic Data Acquisition** + The `check-adb` command did not retrieve a full forensic snapshot of all available data on the device. For example, critical logs such as the **full bugreport** were not systematically collected, leading to potential gaps in forensic analysis. This can be a serious problem in scenarios where the analyst only had one time access to the Android device. -```bash -mvt-android check-adb --serial 192.168.1.20:5555 --output /path/to/results -``` +4. **Code Duplication and Difficulty Ensuring Consistent Behavior Across Sources** + Similar forensic data such as "dumpsys" logs were being loaded and parsed by MVT's ADB, AndroidQF and Bugreport commands. Multiple modules were needed to handle each source format which created duplication leading to inconsistent + behavior and difficulties in maintaining the code base. -Where `192.168.1.20` is the correct IP address of your device. +5. **Alignment with iOS Workflow** + MVT’s forensic workflow for iOS relies on pre-extracted artifacts, such as iTunes backups or filesystem dumps, rather than preforming commands or interactions directly on a live device. Removing the ADB functionality ensures a more consistent methodology across both Android and iOS mobile forensic. -## MVT modules requiring root privileges +## Alternative: Using AndroidQF for Forensic Data Collection -Of the currently available `mvt-android check-adb` modules a handful require root privileges to function correctly. This is because certain files, such as browser history and SMS messages databases are not accessible with user privileges through adb. These modules are to be considered OPTIONALLY available in case the device was already jailbroken. **Do NOT jailbreak your own device unless you are sure of what you are doing!** Jailbreaking your phone exposes it to considerable security risks! +To replace the deprecated ADB-based approach, forensic analysts should use [AndroidQF](https://github.com/mvt-project/androidqf) for comprehensive data collection, followed by MVT for forensic analysis. The workflow is outlined in the MVT [Android methodology](./methodology.md) diff --git a/docs/android/methodology.md b/docs/android/methodology.md index e8062c6d1..958f4475a 100644 --- a/docs/android/methodology.md +++ b/docs/android/methodology.md @@ -1,23 +1,53 @@ # Methodology for Android forensic -Unfortunately Android devices provide much less observability than their iOS cousins. Android stores very little diagnostic information useful to triage potential compromises, and because of this `mvt-android` capabilities are limited as well. +Unfortunately Android devices provide fewer complete forensically useful datasources than their iOS cousins. Unlike iOS, the Android backup feature only provides a limited about of relevant data. + +Android diagnostic logs such as *bugreport files* can be inconsistent in format and structure across different Android versions and device vendors. The limited diagnostic information available makes it difficult to triage potential compromises, and because of this `mvt-android` capabilities are limited as well. However, not all is lost. -## Check installed Apps +## Check Android devices with AndroidQF and MVT + +The [AndroidQF](https://github.com/mvt-project/androidqf) tool can be used to collect a wide range of forensic artifacts from an Android device including an Android backup, a bugreport file, and a range of system logs. MVT natively supports analyzing the generated AndroidQF output for signs of device compromise. + +### Why Use AndroidQF? + +- **Complete and raw data extraction** + AndroidQF collects full forensic artifacts using an on-device forensic collection agent, ensuring that no crucial data is overlooked. The data collection does not depended on the shell environment or utilities available on the device. + +- **Consistent and standardized output** + By collecting a predefined and complete set of forensic files, AndroidQF ensures consistency in data acquisition across different Android devices. + +- **Future-proof analysis** + Since the full forensic artifacts are preserved, analysts can extract new evidence or apply updated analysis techniques without requiring access to the original device. -Because malware attacks over Android typically take the form of malicious or backdoored apps, the very first thing you might want to do is to extract and verify all installed Android packages and triage quickly if there are any which stand out as malicious or which might be atypical. +- **Cross-platform tool without dependencies** + AndroidQF is a standalone Go binary which can be used to remotely collect data from an Android device without the device owner needing to install MVT or a Python environment. -While it is out of the scope of this documentation to dwell into details on how to analyze Android apps, MVT does allow to easily and automatically extract information about installed apps, download copies of them, and quickly look them up on services such as [VirusTotal](https://www.virustotal.com). +### Workflow for Android Forensic Analysis with AndroidQF -!!! info "Using VirusTotal" - Please note that in order to use VirusTotal lookups you are required to provide your own API key through the `MVT_VT_API_KEY` environment variable. You should also note that VirusTotal enforces strict API usage. Be mindful that MVT might consume your hourly search quota. +With AndroidQF the analysis process is split into a separate data collection and data analysis stages. + +1. **Extract Data Using AndroidQF** + Deploy the AndroidQF forensic collector to acquire all relevant forensic artifacts from the Android device. + +2. **Analyze Extracted Data with MVT** + Use the `mvt-android check-androidqf` command to perform forensic analysis on the extracted artifacts. + +By separating artifact collection from forensic analysis, this approach ensures a more reliable and scalable methodology for Android forensic investigations. + +For more information, refer to the [AndroidQF project documentation](https://github.com/mvt-project/androidqf). ## Check the device over Android Debug Bridge -Some additional diagnostic information can be extracted from the phone using the [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb). `mvt-android` allows to automatically extract information including [dumpsys](https://developer.android.com/studio/command-line/dumpsys) results, details on installed packages (without download), running processes, presence of root binaries and packages, and more. +The ability to analyze Android devices over ADB (`mvt-android check-adb`) has been removed from MVT. +See the [Android ADB documentation](./adb.md) for more information. ## Check an Android Backup (SMS messages) -Although Android backups are becoming deprecated, it is still possible to generate one. Unfortunately, because apps these days typically favor backup over the cloud, the amount of data available is limited. Currently, `mvt-android check-backup` only supports checking SMS messages containing links. +Although Android backups are becoming deprecated, it is still possible to generate one. Unfortunately, because apps these days typically favor backup over the cloud, the amount of data available is limited. + +The `mvt-android check-androidqf` command will automatically check an Android backup and SMS messages if an SMS backup is included in the AndroidQF extraction. + +The `mvt-android check-backup` command can also be used directly with an Android backup file. diff --git a/docs/docker.md b/docs/docker.md index be8631ce1..d8f4ff8e5 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -31,21 +31,4 @@ Test if the image was created successfully: docker run -it mvt ``` -If a prompt is spawned successfully, you can close it with `exit`. - - -## Docker usage with Android devices - -If you wish to use MVT to test an Android device you will need to enable the container's access to the host's USB devices. You can do so by enabling the `--privileged` flag and mounting the USB bus device as a volume: - -```bash -docker run -it --privileged -v /dev/bus/usb:/dev/bus/usb mvt -``` - -**Please note:** the `--privileged` parameter is generally regarded as a security risk. If you want to learn more about this check out [this explainer on container escapes](https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/) as it gives access to the whole system. - -Recent versions of Docker provide a `--device` parameter allowing to specify a precise USB device without enabling `--privileged`: - -```bash -docker run -it --device=/dev/ mvt -``` +If a prompt is spawned successfully, you can close it with `exit`. \ No newline at end of file diff --git a/src/mvt/android/cli.py b/src/mvt/android/cli.py index 8e9086fa3..5d23411ca 100644 --- a/src/mvt/android/cli.py +++ b/src/mvt/android/cli.py @@ -11,20 +11,14 @@ from mvt.common.help import ( HELP_MSG_VERSION, HELP_MSG_OUTPUT, - HELP_MSG_SERIAL, - HELP_MSG_DOWNLOAD_APKS, - HELP_MSG_DOWNLOAD_ALL_APKS, - HELP_MSG_VIRUS_TOTAL, - HELP_MSG_APK_OUTPUT, - HELP_MSG_APKS_FROM_FILE, HELP_MSG_VERBOSE, - HELP_MSG_CHECK_ADB, HELP_MSG_IOC, - HELP_MSG_FAST, HELP_MSG_LIST_MODULES, HELP_MSG_MODULE, HELP_MSG_NONINTERACTIVE, HELP_MSG_ANDROID_BACKUP_PASSWORD, + HELP_MSG_CHECK_ADB_REMOVED, + HELP_MSG_CHECK_ADB_REMOVED_DESCRIPTION, HELP_MSG_CHECK_BUGREPORT, HELP_MSG_CHECK_ANDROID_BACKUP, HELP_MSG_CHECK_ANDROIDQF, @@ -36,13 +30,10 @@ from mvt.common.updates import IndicatorsUpdates from mvt.common.utils import init_logging, set_verbose_logging -from .cmd_check_adb import CmdAndroidCheckADB + from .cmd_check_androidqf import CmdAndroidCheckAndroidQF from .cmd_check_backup import CmdAndroidCheckBackup from .cmd_check_bugreport import CmdAndroidCheckBugreport -from .cmd_download_apks import DownloadAPKs -from .modules.adb import ADB_MODULES -from .modules.adb.packages import Packages from .modules.backup import BACKUP_MODULES from .modules.backup.helpers import cli_load_android_backup_password from .modules.bugreport import BUGREPORT_MODULES @@ -70,117 +61,14 @@ def version(): # ============================================================================== -# Command: download-apks +# Command: check-adb (removed) # ============================================================================== @cli.command( - "download-apks", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_DOWNLOAD_APKS -) -@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL) -@click.option("--all-apks", "-a", is_flag=True, help=HELP_MSG_DOWNLOAD_ALL_APKS) -@click.option("--virustotal", "-V", is_flag=True, help=HELP_MSG_VIRUS_TOTAL) -@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_APK_OUTPUT) -@click.option( - "--from-file", "-f", type=click.Path(exists=True), help=HELP_MSG_APKS_FROM_FILE -) -@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE) -@click.pass_context -def download_apks(ctx, all_apks, virustotal, output, from_file, serial, verbose): - set_verbose_logging(verbose) - try: - if from_file: - download = DownloadAPKs.from_json(from_file) - else: - # TODO: Do we actually want to be able to run without storing any - # file? - if not output: - log.critical("You need to specify an output folder with --output!") - ctx.exit(1) - - download = DownloadAPKs(results_path=output, all_apks=all_apks) - if serial: - download.serial = serial - download.run() - - packages_to_lookup = [] - if all_apks: - packages_to_lookup = download.packages - else: - for package in download.packages: - if not package.get("system", False): - packages_to_lookup.append(package) - - if len(packages_to_lookup) == 0: - return - - if virustotal: - m = Packages() - m.check_virustotal(packages_to_lookup) - except KeyboardInterrupt: - print("") - ctx.exit(1) - - -# ============================================================================== -# Command: check-adb -# ============================================================================== -@cli.command("check-adb", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_CHECK_ADB) -@click.option("--serial", "-s", type=str, help=HELP_MSG_SERIAL) -@click.option( - "--iocs", - "-i", - type=click.Path(exists=True), - multiple=True, - default=[], - help=HELP_MSG_IOC, + "check-adb", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_CHECK_ADB_REMOVED ) -@click.option("--output", "-o", type=click.Path(exists=False), help=HELP_MSG_OUTPUT) -@click.option("--fast", "-f", is_flag=True, help=HELP_MSG_FAST) -@click.option("--list-modules", "-l", is_flag=True, help=HELP_MSG_LIST_MODULES) -@click.option("--module", "-m", help=HELP_MSG_MODULE) -@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE) -@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD) -@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE) @click.pass_context -def check_adb( - ctx, - serial, - iocs, - output, - fast, - list_modules, - module, - non_interactive, - backup_password, - verbose, -): - set_verbose_logging(verbose) - module_options = { - "fast_mode": fast, - "interactive": not non_interactive, - "backup_password": cli_load_android_backup_password(log, backup_password), - } - - cmd = CmdAndroidCheckADB( - results_path=output, - ioc_files=iocs, - module_name=module, - serial=serial, - module_options=module_options, - ) - - if list_modules: - cmd.list_modules() - return - - log.info("Checking Android device over debug bridge") - - cmd.run() - - if cmd.detected_count > 0: - log.warning( - "The analysis of the Android device produced %d detections!", - cmd.detected_count, - ) +def check_adb(ctx): + log.error(HELP_MSG_CHECK_ADB_REMOVED_DESCRIPTION) # ============================================================================== @@ -373,7 +261,7 @@ def check_androidqf( @click.pass_context def check_iocs(ctx, iocs, list_modules, module, folder): cmd = CmdCheckIOCS(target_path=folder, ioc_files=iocs, module_name=module) - cmd.modules = BACKUP_MODULES + ADB_MODULES + BUGREPORT_MODULES + cmd.modules = BACKUP_MODULES + BUGREPORT_MODULES if list_modules: cmd.list_modules() diff --git a/src/mvt/android/cmd_check_adb.py b/src/mvt/android/cmd_check_adb.py deleted file mode 100644 index c1444f4e5..000000000 --- a/src/mvt/android/cmd_check_adb.py +++ /dev/null @@ -1,44 +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 typing import Optional - -from mvt.common.command import Command -from mvt.common.indicators import Indicators - -from .modules.adb import ADB_MODULES - -log = logging.getLogger(__name__) - - -class CmdAndroidCheckADB(Command): - def __init__( - self, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - ioc_files: Optional[list] = None, - iocs: Optional[Indicators] = None, - module_name: Optional[str] = None, - serial: Optional[str] = None, - module_options: Optional[dict] = None, - hashes: Optional[bool] = False, - sub_command: Optional[bool] = False, - ) -> None: - super().__init__( - target_path=target_path, - results_path=results_path, - ioc_files=ioc_files, - iocs=iocs, - module_name=module_name, - serial=serial, - module_options=module_options, - hashes=hashes, - sub_command=sub_command, - log=log, - ) - - self.name = "check-adb" - self.modules = ADB_MODULES diff --git a/src/mvt/common/help.py b/src/mvt/common/help.py index 0cca7ab8c..3046e36a8 100644 --- a/src/mvt/common/help.py +++ b/src/mvt/common/help.py @@ -33,19 +33,15 @@ HELP_MSG_CHECK_FS = "Extract artifacts from a full filesystem dump" # Android Specific -HELP_MSG_SERIAL = "Specify a device serial number or HOST:PORT connection string" -HELP_MSG_DOWNLOAD_APKS = "Download all or only non-system installed APKs" HELP_MSG_ANDROID_BACKUP_PASSWORD = "The backup password to use for an Android backup" -HELP_MSG_DOWNLOAD_ALL_APKS = ( - "Extract all packages installed on the phone, including system packages" +HELP_MSG_CHECK_ADB_REMOVED = "REMOVED: Check an Android device over ADB" +HELP_MSG_CHECK_ADB_REMOVED_DESCRIPTION = ( + "The 'mvt-android check-adb' command has been removed from MVT. " + "Use AndroidQF to collect full forensic artifacts from an Android device. \n\n" + "The 'mvt-android check-androidqf' command in MVT can be used to fully analyze " + "forensic data collected with AndroidQF. Minimal checks can also be performed " + "on an Android bugreport using the 'mvt-android check-bugreport' command." ) -HELP_MSG_VIRUS_TOTAL = "Check packages on VirusTotal" -HELP_MSG_APK_OUTPUT = "Specify a path to a folder where you want to store the APKs" -HELP_MSG_APKS_FROM_FILE = ( - "Instead of acquiring APKs from a phone, load an existing packages.json file for " - "lookups (mainly for debug purposes)" -) -HELP_MSG_CHECK_ADB = "Check an Android device over ADB" HELP_MSG_CHECK_BUGREPORT = "Check an Android Bug Report" HELP_MSG_CHECK_ANDROID_BACKUP = "Check an Android Backup" HELP_MSG_CHECK_ANDROIDQF = "Check data collected with AndroidQF" diff --git a/src/mvt/common/version.py b/src/mvt/common/version.py index dd19c1e72..b31392c9d 100644 --- a/src/mvt/common/version.py +++ b/src/mvt/common/version.py @@ -3,4 +3,4 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ -MVT_VERSION = "2.6.0" +MVT_VERSION = "3.0.0" diff --git a/src/mvt/common/virustotal.py b/src/mvt/common/virustotal.py deleted file mode 100644 index e0749ea98..000000000 --- a/src/mvt/common/virustotal.py +++ /dev/null @@ -1,52 +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 -import os - -import requests - -log = logging.getLogger(__name__) - -MVT_VT_API_KEY = "MVT_VT_API_KEY" - - -class VTNoKey(Exception): - pass - - -class VTQuotaExceeded(Exception): - pass - - -def virustotal_lookup(file_hash: str): - if MVT_VT_API_KEY not in os.environ: - raise VTNoKey( - "No VirusTotal API key provided: to use VirusTotal " - "lookups please provide your API key with " - "`export MVT_VT_API_KEY=`" - ) - - headers = { - "User-Agent": "VirusTotal", - "Content-Type": "application/json", - "x-apikey": os.environ[MVT_VT_API_KEY], - } - res = requests.get( - f"https://www.virustotal.com/api/v3/files/{file_hash}", headers=headers - ) - - if res.status_code == 200: - report = res.json() - return report["data"] - - if res.status_code == 404: - log.info("Could not find results for file with hash %s", file_hash) - elif res.status_code == 429: - raise VTQuotaExceeded("You have exceeded the quota for your VirusTotal API key") - else: - raise Exception(f"Unexpected response from VirusTotal: {res.status_code}") - - return None From 6bac787cb544e1b43e37e84afa344ac1b7391525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Sun, 16 Feb 2025 00:00:09 +0100 Subject: [PATCH 05/37] Remove check-apk code and old dependencies --- pyproject.toml | 2 - src/mvt/android/cmd_download_apks.py | 184 --------------------------- 2 files changed, 186 deletions(-) delete mode 100644 src/mvt/android/cmd_download_apks.py diff --git a/pyproject.toml b/pyproject.toml index eec0824ef..74e89d8c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,8 +27,6 @@ dependencies = [ "packaging >=21.3", "appdirs >=1.4.4", "iOSbackup >=0.9.923", - "adb-shell[usb] >=0.4.3", - "libusb1 >=3.0.0", "cryptography >=42.0.5", "pyyaml >=6.0", "pyahocorasick >= 2.0.0", diff --git a/src/mvt/android/cmd_download_apks.py b/src/mvt/android/cmd_download_apks.py deleted file mode 100644 index deacb0551..000000000 --- a/src/mvt/android/cmd_download_apks.py +++ /dev/null @@ -1,184 +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 json -import logging -import os -from typing import Callable, Optional, Union - -from rich.progress import track - -from mvt.common.module import InsufficientPrivileges - -from .modules.adb.base import AndroidExtraction -from .modules.adb.packages import Packages - -log = logging.getLogger(__name__) - - -class DownloadAPKs(AndroidExtraction): - """DownloadAPKs is the main class operating the download of APKs - from the device. - """ - - def __init__( - self, - results_path: Optional[str] = None, - all_apks: bool = False, - packages: Optional[list] = None, - ) -> None: - """Initialize module. - :param results_path: Path to the folder where data should be stored - :param all_apks: Boolean indicating whether to download all packages - or filter known-goods - :param packages: Provided list of packages, typically for JSON checks - """ - super().__init__(results_path=results_path, log=log) - - self.packages = packages - self.all_apks = all_apks - self.results_path_apks = None - - @classmethod - def from_json(cls, json_path: str) -> Callable: - """Initialize this class from an existing apks.json file. - - :param json_path: Path to the apks.json file to parse. - - """ - with open(json_path, "r", encoding="utf-8") as handle: - packages = json.load(handle) - return cls(packages=packages) - - def pull_package_file( - self, package_name: str, remote_path: str - ) -> Union[str, None]: - """Pull files related to specific package from the device. - - :param package_name: Name of the package to download - :param remote_path: Path to the file to download - :returns: Path to the local copy - - """ - log.info("Downloading %s ...", remote_path) - - file_name = "" - if "==/" in remote_path: - file_name = "_" + remote_path.split("==/")[1].replace(".apk", "") - - local_path = os.path.join( - self.results_path_apks, f"{package_name}{file_name}.apk" - ) - name_counter = 0 - while True: - if not os.path.exists(local_path): - break - - name_counter += 1 - local_path = os.path.join( - self.results_path_apks, f"{package_name}{file_name}_{name_counter}.apk" - ) - - try: - self._adb_download(remote_path, local_path) - except InsufficientPrivileges: - log.error( - "Unable to pull package file from %s: insufficient privileges, " - "it might be a system app", - remote_path, - ) - self._adb_reconnect() - return None - except Exception as exc: - log.exception("Failed to pull package file from %s: %s", remote_path, exc) - self._adb_reconnect() - return None - - return local_path - - def get_packages(self) -> None: - """Use the Packages adb module to retrieve the list of packages. - We reuse the same extraction logic to then download the APKs. - """ - self.log.info("Retrieving list of installed packages...") - - m = Packages() - m.log = self.log - m.serial = self.serial - m.run() - - self.packages = m.results - - def pull_packages(self) -> None: - """Download all files of all selected packages from the device.""" - log.info( - "Starting extraction of installed APKs at folder %s", self.results_path - ) - - # If the user provided the flag --all-apks we select all packages. - packages_selection = [] - if self.all_apks: - log.info("Selected all %d available packages", len(self.packages)) - packages_selection = self.packages - else: - # Otherwise we loop through the packages and get only those that - # are not marked as system. - for package in self.packages: - if not package.get("system", False): - packages_selection.append(package) - - log.info( - 'Selected only %d packages which are not marked as "system"', - len(packages_selection), - ) - - if len(packages_selection) == 0: - log.info("No packages were selected for download") - return - - log.info("Downloading packages from device. This might take some time ...") - - self.results_path_apks = os.path.join(self.results_path, "apks") - if not os.path.exists(self.results_path_apks): - os.makedirs(self.results_path_apks, exist_ok=True) - - for i in track( - range(len(packages_selection)), - description=f"Downloading {len(packages_selection)} packages...", - ): - package = packages_selection[i] - - log.info( - "[%d/%d] Package: %s", - i, - len(packages_selection), - package["package_name"], - ) - - # Sometimes the package path contains multiple lines for multiple - # apks. We loop through each line and download each file. - for package_file in package["files"]: - device_path = package_file["path"] - local_path = self.pull_package_file( - package["package_name"], device_path - ) - if not local_path: - continue - - package_file["local_path"] = local_path - - log.info("Download of selected packages completed") - - def save_json(self) -> None: - json_path = os.path.join(self.results_path, "apks.json") - with open(json_path, "w", encoding="utf-8") as handle: - json.dump(self.packages, handle, indent=4) - - def run(self) -> None: - self.get_packages() - self._adb_connect() - self.pull_packages() - self.save_json() - self._adb_disconnect() From 1b03002a00c7c8ed439c04160eac266db10c16fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Sun, 16 Feb 2025 00:10:44 +0100 Subject: [PATCH 06/37] Major refactor to add structured alerting and typed indicators This commit makes a structural change to MVT by changing binary detected/not detected logic into a structured multi-level system of alerts. This gives far more power to extend MVT and manage alerts. This commit also begins the process of adding proper typing for key objects used in MVT including Indicators, IndicatorMatches, and ModuleResults. This will also be keep to programmatically using the output of MVT. --- src/mvt/android/cli.py | 30 +-- src/mvt/android/cmd_check_androidqf.py | 9 +- src/mvt/common/alerts.py | 181 +++++++++++++++ src/mvt/common/command.py | 121 ++++++---- src/mvt/common/indicators.py | 267 +++++++++-------------- src/mvt/common/log.py | 65 ++++++ src/mvt/common/module.py | 70 ++++-- src/mvt/common/module_types.py | 29 +++ src/mvt/common/utils.py | 9 +- src/mvt/ios/cli.py | 17 +- tests/android_androidqf/test_packages.py | 7 +- 11 files changed, 543 insertions(+), 262 deletions(-) create mode 100644 src/mvt/common/alerts.py create mode 100644 src/mvt/common/log.py create mode 100644 src/mvt/common/module_types.py diff --git a/src/mvt/android/cli.py b/src/mvt/android/cli.py index 5d23411ca..ebdeeffb8 100644 --- a/src/mvt/android/cli.py +++ b/src/mvt/android/cli.py @@ -37,6 +37,7 @@ from .modules.backup import BACKUP_MODULES from .modules.backup.helpers import cli_load_android_backup_password from .modules.bugreport import BUGREPORT_MODULES +from .modules.androidqf import ANDROIDQF_MODULES init_logging() log = logging.getLogger("mvt") @@ -109,12 +110,8 @@ def check_bugreport(ctx, iocs, output, list_modules, module, verbose, bugreport_ log.info("Checking Android bug report at path: %s", bugreport_path) cmd.run() - - if cmd.detected_count > 0: - log.warning( - "The analysis of the Android bug report produced %d detections!", - cmd.detected_count, - ) + cmd.show_alerts_brief() + cmd.show_support_message() # ============================================================================== @@ -171,12 +168,8 @@ def check_backup( log.info("Checking Android backup at path: %s", backup_path) cmd.run() - - if cmd.detected_count > 0: - log.warning( - "The analysis of the Android backup produced %d detections!", - cmd.detected_count, - ) + cmd.show_alerts_brief() + cmd.show_support_message() # ============================================================================== @@ -235,12 +228,9 @@ def check_androidqf( log.info("Checking AndroidQF acquisition at path: %s", androidqf_path) cmd.run() - - if cmd.detected_count > 0: - log.warning( - "The analysis of the AndroidQF acquisition produced %d detections!", - cmd.detected_count, - ) + cmd.show_alerts_brief() + cmd.show_disable_adb_warning() + cmd.show_support_message() # ============================================================================== @@ -261,13 +251,15 @@ def check_androidqf( @click.pass_context def check_iocs(ctx, iocs, list_modules, module, folder): cmd = CmdCheckIOCS(target_path=folder, ioc_files=iocs, module_name=module) - cmd.modules = BACKUP_MODULES + BUGREPORT_MODULES + cmd.modules = BACKUP_MODULES + BUGREPORT_MODULES + ANDROIDQF_MODULES if list_modules: cmd.list_modules() return cmd.run() + cmd.show_alerts_brief() + cmd.show_support_message() # ============================================================================== diff --git a/src/mvt/android/cmd_check_androidqf.py b/src/mvt/android/cmd_check_androidqf.py index 580b5e2d1..e0f49aba5 100644 --- a/src/mvt/android/cmd_check_androidqf.py +++ b/src/mvt/android/cmd_check_androidqf.py @@ -67,6 +67,9 @@ def __init__( self.__files: List[str] = [] def init(self): + if not self.target_path: + raise NoAndroidQFTargetPath + if os.path.isdir(self.target_path): self.__format = "dir" parent_path = Path(self.target_path).absolute().parent.as_posix() @@ -154,9 +157,8 @@ def run_bugreport_cmd(self) -> bool: cmd.from_zip(bugreport) cmd.run() - self.detected_count += cmd.detected_count self.timeline.extend(cmd.timeline) - self.timeline_detected.extend(cmd.timeline_detected) + self.alertstore.extend(cmd.alertstore.alerts) def run_backup_cmd(self) -> bool: try: @@ -179,9 +181,8 @@ def run_backup_cmd(self) -> bool: cmd.from_ab(backup) cmd.run() - self.detected_count += cmd.detected_count self.timeline.extend(cmd.timeline) - self.timeline_detected.extend(cmd.timeline_detected) + self.alertstore.extend(cmd.alertstore.alerts) def finish(self) -> None: """ diff --git a/src/mvt/common/alerts.py b/src/mvt/common/alerts.py new file mode 100644 index 000000000..635520dcd --- /dev/null +++ b/src/mvt/common/alerts.py @@ -0,0 +1,181 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2025 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 csv +import logging +from enum import Enum +from dataclasses import dataclass, asdict +from typing import List, Dict, Any, Optional + +from .log import INFO_ALERT, LOW_ALERT, HIGH_ALERT, CRITICAL_ALERT, MEDIUM_ALERT +from .module_types import ModuleAtomicResult + + +class AlertLevel(Enum): + INFORMATIONAL = 0 + LOW = 10 + MEDIUM = 20 + HIGH = 30 + CRITICAL = 40 + + +@dataclass +class Alert: + level: AlertLevel + module: str + message: str + event_time: str + event: ModuleAtomicResult + + +class AlertStore: + def __init__(self, log: Optional[logging.Logger] = None) -> None: + self.__alerts: List[Alert] = [] + self.__log = log + + @property + def alerts(self) -> List[Alert]: + return self.__alerts + + def add(self, alert: Alert) -> None: + self.__alerts.append(alert) + + def extend(self, alerts: List[Alert]) -> None: + self.__alerts.extend(alerts) + + def info( + self, module: str, message: str, event_time: str, event: ModuleAtomicResult + ): + self.add( + Alert( + level=AlertLevel.INFORMATIONAL, + module=module, + message=message, + event_time=event_time, + event=event, + ) + ) + + def low( + self, module: str, message: str, event_time: str, event: ModuleAtomicResult + ): + self.add( + Alert( + level=AlertLevel.LOW, + module=module, + message=message, + event_time=event_time, + event=event, + ) + ) + + def medium( + self, module: str, message: str, event_time: str, event: ModuleAtomicResult + ): + self.add( + Alert( + level=AlertLevel.MEDIUM, + module=module, + message=message, + event_time=event_time, + event=event, + ) + ) + + def high( + self, module: str, message: str, event_time: str, event: ModuleAtomicResult + ): + self.add( + Alert( + level=AlertLevel.HIGH, + module=module, + message=message, + event_time=event_time, + event=event, + ) + ) + + def critical( + self, module: str, message: str, event_time: str, event: ModuleAtomicResult + ): + self.add( + Alert( + level=AlertLevel.CRITICAL, + module=module, + message=message, + event_time=event_time, + event=event, + ) + ) + + def log(self, alert: Alert) -> None: + if not self.__log: + return + + if not alert.message: + return + + if alert.level == AlertLevel.INFORMATIONAL: + self.__log.log(INFO_ALERT, alert.message) + elif alert.level == AlertLevel.LOW: + self.__log.log(LOW_ALERT, alert.message) + elif alert.level == AlertLevel.MEDIUM: + self.__log.log(MEDIUM_ALERT, alert.message) + elif alert.level == AlertLevel.HIGH: + self.__log.log(HIGH_ALERT, alert.message) + elif alert.level == AlertLevel.CRITICAL: + self.__log.log(CRITICAL_ALERT, alert.message) + + def log_latest(self) -> None: + self.log(self.__alerts[-1]) + + def count(self, level: AlertLevel) -> int: + count = 0 + for alert in self.__alerts: + if alert.level == level: + count += 1 + + return count + + def as_json(self) -> List[Dict[str, Any]]: + alerts = [] + for alert in self.__alerts: + alert_dict = asdict(alert) + # This is required because an Enum is not JSON serializable. + alert_dict["level"] = alert.level.name + alerts.append(alert_dict) + + return alerts + + def save_timeline(self, timeline_path: str) -> None: + with open(timeline_path, "a+", encoding="utf-8") as handle: + csvoutput = csv.writer( + handle, + delimiter=",", + quotechar='"', + quoting=csv.QUOTE_ALL, + escapechar="\\", + ) + csvoutput.writerow(["Event Time", "Module", "Message", "Event"]) + + timed_alerts = [] + for alert in self.alerts: + if not alert.event_time: + continue + + timed_alerts.append(asdict(alert)) + + for event in sorted( + timed_alerts, + key=lambda x: x["event_time"] if x["event_time"] is not None else "", + ): + csvoutput.writerow( + [ + event.get("event_time"), + event.get("module"), + event.get("message"), + event.get("event"), + ] + ) diff --git a/src/mvt/common/command.py b/src/mvt/common/command.py index b6d7aaff4..920f3a053 100644 --- a/src/mvt/common/command.py +++ b/src/mvt/common/command.py @@ -9,16 +9,20 @@ import sys from datetime import datetime from typing import Optional +from rich.console import Console +from rich.panel import Panel +from rich.text import Text -from mvt.common.indicators import Indicators -from mvt.common.module import MVTModule, run_module, save_timeline -from mvt.common.utils import ( +from .indicators import Indicators +from .module import MVTModule, run_module, save_timeline +from .utils import ( convert_datetime_to_iso, generate_hashes_from_path, get_sha256_from_file_path, ) -from mvt.common.config import settings -from mvt.common.version import MVT_VERSION +from .config import settings +from .alerts import AlertStore, AlertLevel +from .version import MVT_VERSION class Command: @@ -70,12 +74,14 @@ def __init__( self.iocs = Indicators(self.log) self.iocs.load_indicators_files(self.ioc_files) + self.alertstore = AlertStore() + def _create_storage(self) -> None: if self.results_path and not os.path.exists(self.results_path): try: os.makedirs(self.results_path) except Exception as exc: - self.log.critical( + self.log.fatal( "Unable to create output folder %s: %s", self.results_path, exc ) sys.exit(1) @@ -94,14 +100,14 @@ def _setup_logging(self): file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(formatter) - # MVT can be run in a loop - # Old file handlers stick around in subsequent loops - # Remove any existing logging.FileHandler instances + # MVT can be run in a loop. + # Old file handlers stick around in subsequent loops. + # Remove any existing logging.FileHandler instances. for handler in logger.handlers: if isinstance(handler, logging.FileHandler): logger.removeHandler(handler) - # And finally add the new one + # And finally add the new one. logger.addHandler(file_handler) def _store_timeline(self) -> None: @@ -122,12 +128,24 @@ def _store_timeline(self) -> None: is_utc=is_utc, ) - if len(self.timeline_detected) > 0: - save_timeline( - self.timeline_detected, - os.path.join(self.results_path, "timeline_detected.csv"), - is_utc=is_utc, - ) + def _store_alerts(self) -> None: + if not self.results_path: + return + + alerts = self.alertstore.as_json() + if not alerts: + return + + alerts_path = os.path.join(self.results_path, "alerts.json") + with open(alerts_path, "w+", encoding="utf-8") as handle: + json.dump(alerts, handle, indent=4) + + def _store_alerts_timeline(self) -> None: + if not self.results_path: + return + + alerts_timeline_path = os.path.join(self.results_path, "alerts_timeline.csv") + self.alertstore.save_timeline(alerts_timeline_path) def _store_info(self) -> None: if not self.results_path: @@ -187,26 +205,54 @@ def module_init(self, module: MVTModule) -> None: def finish(self) -> None: raise NotImplementedError - def _show_disable_adb_warning(self) -> None: - """Warn if ADB is enabled""" - if type(self).__name__ in ["CmdAndroidCheckADB", "CmdAndroidCheckAndroidQF"]: - self.log.info( - "Please disable Developer Options and ADB (Android Debug Bridge) on the device once finished with the acquisition. " - "ADB is a powerful tool which can allow unauthorized access to the device." + def show_alerts_brief(self) -> None: + console = Console() + + message = Text() + for i, level in enumerate(AlertLevel): + message.append( + f"MVT produced {self.alertstore.count(level)} {level.name} alerts." ) + if i < len(AlertLevel) - 1: + message.append("\n") + + panel = Panel( + message, title="ALERTS", style="sandy_brown", border_style="sandy_brown" + ) + console.print("") + console.print(panel) + + def show_disable_adb_warning(self) -> None: + console = Console() + message = Text( + "Please disable Developer Options and ADB (Android Debug Bridge) on the device once finished with the acquisition. " + "ADB is a powerful tool which can allow unauthorized access to the device." + ) + panel = Panel(message, title="NOTE", style="yellow", border_style="yellow") + console.print("") + console.print(panel) + + def show_support_message(self) -> None: + console = Console() + message = Text() - def _show_support_message(self) -> None: support_message = "Please seek reputable expert help if you have serious concerns about a possible spyware attack. Such support is available to human rights defenders and civil society through Amnesty International's Security Lab at https://securitylab.amnesty.org/get-help/?c=mvt" - if self.detected_count == 0: - self.log.info( - f"[bold]NOTE:[/bold] Using MVT with public indicators of compromise (IOCs) [bold]WILL NOT[/bold] automatically detect advanced attacks.\n\n{support_message}", - extra={"markup": True}, + if ( + self.alertstore.count(AlertLevel.HIGH) > 0 + or self.alertstore.count(AlertLevel.CRITICAL) > 0 + ): + message.append( + f"MVT produced HIGH or CRITICAL alerts. Only expert review can confirm if the detected indicators are signs of an attack.\n\n{support_message}", ) + panel = Panel(message, title="WARNING", style="red", border_style="red") else: - self.log.warning( - f"[bold]NOTE: Detected indicators of compromise[/bold]. Only expert review can confirm if the detected indicators are signs of an attack.\n\n{support_message}", - extra={"markup": True}, + message.append( + f"The lack of severe alerts does not equate to a clean bill of health.\n\n{support_message}", ) + panel = Panel(message, title="NOTE", style="yellow", border_style="yellow") + + console.print("") + console.print(panel) def run(self) -> None: try: @@ -218,6 +264,11 @@ def run(self) -> None: if self.module_name and module.__name__ != self.module_name: continue + if not module.enabled and not ( + self.module_name and module.__name__ == self.module_name + ): + continue + # FIXME: do we need the logger here module_logger = logging.getLogger(module.__module__) @@ -243,11 +294,8 @@ def run(self) -> None: run_module(m) self.executed.append(m) - - self.detected_count += len(m.detected) - self.timeline.extend(m.timeline) - self.timeline_detected.extend(m.timeline_detected) + self.alertstore.extend(m.alertstore.alerts) try: self.finish() @@ -259,7 +307,6 @@ def run(self) -> None: return self._store_timeline() + self._store_alerts_timeline() + self._store_alerts() self._store_info() - - self._show_disable_adb_warning() - self._show_support_message() diff --git a/src/mvt/common/indicators.py b/src/mvt/common/indicators.py index e23a996b3..adb448373 100644 --- a/src/mvt/common/indicators.py +++ b/src/mvt/common/indicators.py @@ -8,7 +8,8 @@ import logging import os from functools import lru_cache -from typing import Any, Dict, Iterator, List, Optional, Union +from typing import Any, Dict, Iterator, List, Optional +from dataclasses import dataclass import ahocorasick from appdirs import user_data_dir @@ -22,6 +23,20 @@ logger = logging.getLogger(__name__) +@dataclass +class Indicator: + value: str + type: str + name: str + stix2_file_name: str + + +@dataclass +class IndicatorMatch: + ioc: Indicator + message: str + + class Indicators: """This class is used to parse indicators from a STIX2 file and provide functions to compare extracted artifacts to the indicators. @@ -203,7 +218,7 @@ def parse_stix2(self, file_path: str) -> None: try: data = json.load(handle) except json.decoder.JSONDecodeError: - self.log.critical( + self.log.warning( "Unable to parse STIX2 indicator file. " "The file is corrupted or in the wrong format!" ) @@ -314,7 +329,7 @@ def load_indicators_files( if os.path.isfile(file_path): self.parse_stix2(file_path) else: - self.log.warning("No indicators file exists at path %s", file_path) + self.log.error("No indicators file exists at path %s", file_path) # Load downloaded indicators and any indicators from env variable. if load_default: @@ -323,15 +338,15 @@ def load_indicators_files( self._check_stix2_env_variable() self.log.info("Loaded a total of %d unique indicators", self.total_ioc_count) - def get_iocs(self, ioc_type: str) -> Iterator[Dict[str, Any]]: + def get_iocs(self, ioc_type: str) -> Iterator[Indicator]: for ioc_collection in self.ioc_collections: for ioc in ioc_collection.get(ioc_type, []): - yield { - "value": ioc, - "type": ioc_type, - "name": ioc_collection["name"], - "stix2_file_name": ioc_collection["stix2_file_name"], - } + yield Indicator( + value=ioc, + type=ioc_type, + name=ioc_collection["name"], + stix2_file_name=ioc_collection["stix2_file_name"], + ) @lru_cache() def get_ioc_matcher( @@ -362,12 +377,12 @@ def get_ioc_matcher( raise ValueError("Must provide either ioc_type or ioc_list") for ioc in iocs: - automaton.add_word(ioc["value"], ioc) + automaton.add_word(ioc.value, ioc) automaton.make_automaton() return automaton @lru_cache() - def check_url(self, url: str) -> Union[dict, None]: + def check_url(self, url: str) -> Optional[IndicatorMatch]: """Check if a given URL matches any of the provided domain indicators. :param url: URL to match against domain indicators @@ -375,21 +390,16 @@ def check_url(self, url: str) -> Union[dict, None]: :returns: Indicator details if matched, otherwise None """ - if not url: - return None - if not isinstance(url, str): + if not url or not isinstance(url, str): return None # Check the URL first for ioc in self.get_iocs("urls"): - if ioc["value"] == url: - self.log.warning( - 'Found a known suspicious URL %s matching indicator "%s" from "%s"', - url, - ioc["value"], - ioc["name"], + if ioc.value == url: + return IndicatorMatch( + ioc=ioc, + message=f'Found a known suspicious URL {url} matching indicator "{ioc.value}" from "{ioc.name}"', ) - return ioc # Then check the domain # Create an Aho-Corasick automaton from the list of urls @@ -426,71 +436,41 @@ def check_url(self, url: str) -> Union[dict, None]: except Exception: # If URL parsing failed, we just try to do a simple substring # match. - for idx, ioc in domain_matcher.iter(url): - if ioc["value"].lower() in url: - self.log.warning( - "Maybe found a known suspicious domain %s " - 'matching indicator "%s" from "%s"', - url, - ioc["value"], - ioc["name"], + for _, ioc in domain_matcher.iter(url): + if ioc.value.lower() in url: + return IndicatorMatch( + ioc=ioc, + message=f'Maybe found a known suspicious domain {url} matching indicator "{ioc.value}" from "{ioc.name}"', ) - return ioc # If nothing matched, we can quit here. return None # If all parsing worked, we start walking through available domain # indicators. - for idx, ioc in domain_matcher.iter(final_url.domain.lower()): + for _, ioc in domain_matcher.iter(final_url.domain.lower()): # First we check the full domain. - if final_url.domain.lower() == ioc["value"]: + if final_url.domain.lower() == ioc.value: if orig_url.is_shortened and orig_url.url != final_url.url: - self.log.warning( - "Found a known suspicious domain %s " - 'shortened as %s matching indicator "%s" from "%s"', - final_url.url, - orig_url.url, - ioc["value"], - ioc["name"], - ) + message = f'Found a known suspicious domain {final_url.url} shortened as {orig_url.url} matching indicator "{ioc.value}" from "{ioc.name}"' else: - self.log.warning( - "Found a known suspicious domain %s " - 'matching indicator "%s" from "%s"', - final_url.url, - ioc["value"], - ioc["name"], - ) - return ioc + message = f'Found a known suspicious domain {final_url.url} matching indicator "{ioc.value}" from "{ioc.name}"' + + return IndicatorMatch(ioc=ioc, message=message) # Then we just check the top level domain. - for idx, ioc in domain_matcher.iter(final_url.top_level.lower()): - if final_url.top_level.lower() == ioc["value"]: + for _, ioc in domain_matcher.iter(final_url.top_level.lower()): + if final_url.top_level.lower() == ioc.value: if orig_url.is_shortened and orig_url.url != final_url.url: - self.log.warning( - "Found a sub-domain with suspicious top " - "level %s shortened as %s matching " - 'indicator "%s" from "%s"', - final_url.url, - orig_url.url, - ioc["value"], - ioc["name"], - ) + message = f'Found a sub-domain with suspicious top level {final_url.url} shortened as {orig_url.url} matching indicator "{ioc.value}" from "{ioc.name}"' else: - self.log.warning( - "Found a sub-domain with a suspicious top " - 'level %s matching indicator "%s" from "%s"', - final_url.url, - ioc["value"], - ioc["name"], - ) + message = f'Found a sub-domain with a suspicious top level {final_url.url} matching indicator "{ioc.value}" from "{ioc.name}"' - return ioc + return IndicatorMatch(ioc=ioc, message=message) return None - def check_urls(self, urls: list) -> Union[dict, None]: + def check_urls(self, urls: list) -> Optional[IndicatorMatch]: """Check a list of URLs against the provided list of domain indicators. :param urls: List of URLs to check against domain indicators @@ -508,7 +488,7 @@ def check_urls(self, urls: list) -> Union[dict, None]: return None - def check_process(self, process: str) -> Union[dict, None]: + def check_process(self, process: str) -> Optional[IndicatorMatch]: """Check the provided process name against the list of process indicators. @@ -522,28 +502,22 @@ def check_process(self, process: str) -> Union[dict, None]: proc_name = os.path.basename(process) for ioc in self.get_iocs("processes"): - if proc_name == ioc["value"]: - self.log.warning( - 'Found a known suspicious process name "%s" ' - 'matching indicators from "%s"', - process, - ioc["name"], + if proc_name == ioc.value: + return IndicatorMatch( + ioc=ioc, + message=f'Found a known suspicious process name "{process}" matching indicators from "{ioc.name}"', ) - return ioc if len(proc_name) == 16: - if ioc["value"].startswith(proc_name): - self.log.warning( - "Found a truncated known suspicious " - 'process name "%s" matching indicators from "%s"', - process, - ioc["name"], + if ioc.value.startswith(proc_name): + return IndicatorMatch( + ioc=ioc, + message=f'Found a truncated known suspicious process name "{process}" matching indicators from "{ioc.name}"', ) - return ioc return None - def check_processes(self, processes: list) -> Union[dict, None]: + def check_processes(self, processes: list) -> Optional[IndicatorMatch]: """Check the provided list of processes against the list of process indicators. @@ -562,7 +536,7 @@ def check_processes(self, processes: list) -> Union[dict, None]: return None - def check_email(self, email: str) -> Union[dict, None]: + def check_email(self, email: str) -> Optional[IndicatorMatch]: """Check the provided email against the list of email indicators. :param email: Email address to check against email indicators @@ -574,18 +548,15 @@ def check_email(self, email: str) -> Union[dict, None]: return None for ioc in self.get_iocs("emails"): - if email.lower() == ioc["value"].lower(): - self.log.warning( - 'Found a known suspicious email address "%s" ' - 'matching indicators from "%s"', - email, - ioc["name"], + if email.lower() == ioc.value.lower(): + return IndicatorMatch( + ioc=ioc, + message=f'Found a known suspicious email address "{email}" matching indicators from "{ioc.name}"', ) - return ioc return None - def check_file_name(self, file_name: str) -> Union[dict, None]: + def check_file_name(self, file_name: str) -> Optional[IndicatorMatch]: """Check the provided file name against the list of file indicators. :param file_name: File name to check against file @@ -598,18 +569,15 @@ def check_file_name(self, file_name: str) -> Union[dict, None]: return None for ioc in self.get_iocs("file_names"): - if ioc["value"] == file_name: - self.log.warning( - 'Found a known suspicious file name "%s" ' - 'matching indicators from "%s"', - file_name, - ioc["name"], + if ioc.value == file_name: + return IndicatorMatch( + ioc=ioc, + message=f'Found a known suspicious file name "{file_name}" matching indicators from "{ioc.name}"', ) - return ioc return None - def check_file_path(self, file_path: str) -> Union[dict, None]: + def check_file_path(self, file_path: str) -> Optional[IndicatorMatch]: """Check the provided file path against the list of file indicators (both path and name). @@ -629,18 +597,15 @@ def check_file_path(self, file_path: str) -> Union[dict, None]: for ioc in self.get_iocs("file_paths"): # Strip any trailing slash from indicator paths to match # directories. - if file_path.startswith(ioc["value"].rstrip("/")): - self.log.warning( - 'Found a known suspicious file path "%s" ' - 'matching indicators form "%s"', - file_path, - ioc["name"], + if file_path.startswith(ioc.value.rstrip("/")): + return IndicatorMatch( + ioc=ioc, + message=f'Found a known suspicious file path "{file_path}" matching indicators form "{ioc.name}"', ) - return ioc return None - def check_file_path_process(self, file_path: str) -> Optional[Dict[str, Any]]: + def check_file_path_process(self, file_path: str) -> Optional[IndicatorMatch]: """Check the provided file path contains a process name from the list of indicators @@ -655,18 +620,15 @@ def check_file_path_process(self, file_path: str) -> Optional[Dict[str, Any]]: for ioc in self.get_iocs("processes"): parts = file_path.split("/") - if ioc["value"] in parts: - self.log.warning( - "Found known suspicious process name mentioned in file at " - 'path "%s" matching indicators from "%s"', - file_path, - ioc["name"], + if ioc.value in parts: + return IndicatorMatch( + ioc=ioc, + message=f'Found known suspicious process name mentioned in file at path "{file_path}" matching indicators from "{ioc.name}"', ) - return ioc return None - def check_profile(self, profile_uuid: str) -> Union[dict, None]: + def check_profile(self, profile_uuid: str) -> Optional[IndicatorMatch]: """Check the provided configuration profile UUID against the list of indicators. @@ -680,18 +642,15 @@ def check_profile(self, profile_uuid: str) -> Union[dict, None]: return None for ioc in self.get_iocs("ios_profile_ids"): - if profile_uuid in ioc["value"]: - self.log.warning( - 'Found a known suspicious profile ID "%s" ' - 'matching indicators from "%s"', - profile_uuid, - ioc["name"], + if profile_uuid in ioc.value: + return IndicatorMatch( + ioc=ioc, + message=f'Found a known suspicious profile ID "{profile_uuid}" matching indicators from "{ioc.name}"', ) - return ioc return None - def check_file_hash(self, file_hash: str) -> Union[dict, None]: + def check_file_hash(self, file_hash: str) -> Optional[IndicatorMatch]: """Check the provided file hash against the list of indicators. :param file_hash: hash to check @@ -710,18 +669,15 @@ def check_file_hash(self, file_hash: str) -> Union[dict, None]: hash_type = "sha256" for ioc in self.get_iocs("files_" + hash_type): - if file_hash.lower() == ioc["value"].lower(): - self.log.warning( - 'Found a known suspicious file with hash "%s" ' - 'matching indicators from "%s"', - file_hash, - ioc["name"], + if file_hash.lower() == ioc.value.lower(): + return IndicatorMatch( + ioc=ioc, + message=f'Found a known suspicious file with hash "{file_hash}" matching indicators from "{ioc.name}"', ) - return ioc return None - def check_app_certificate_hash(self, cert_hash: str) -> Union[dict, None]: + def check_app_certificate_hash(self, cert_hash: str) -> Optional[IndicatorMatch]: """Check the provided cert hash against the list of indicators. :param cert_hash: hash to check @@ -733,18 +689,15 @@ def check_app_certificate_hash(self, cert_hash: str) -> Union[dict, None]: return None for ioc in self.get_iocs("app_cert_hashes"): - if cert_hash.lower() == ioc["value"].lower(): - self.log.warning( - 'Found a known suspicious app certfificate with hash "%s" ' - 'matching indicators from "%s"', - cert_hash, - ioc["name"], + if cert_hash.lower() == ioc.value.lower(): + return IndicatorMatch( + ioc=ioc, + message=f'Found a known suspicious app certfificate with hash "{cert_hash}" matching indicators from "{ioc.name}"', ) - return ioc return None - def check_app_id(self, app_id: str) -> Union[dict, None]: + def check_app_id(self, app_id: str) -> Optional[IndicatorMatch]: """Check the provided app identifier (typically an Android package name) against the list of indicators. @@ -757,18 +710,17 @@ def check_app_id(self, app_id: str) -> Union[dict, None]: return None for ioc in self.get_iocs("app_ids"): - if app_id.lower() == ioc["value"].lower(): - self.log.warning( - 'Found a known suspicious app with ID "%s" ' - 'matching indicators from "%s"', - app_id, - ioc["name"], + if app_id.lower() == ioc.value.lower(): + return IndicatorMatch( + ioc=ioc, + message=f'Found a known suspicious app with ID "{app_id}" matching indicators from "{ioc.name}"', ) - return ioc return None - def check_android_property_name(self, property_name: str) -> Optional[dict]: + def check_android_property_name( + self, property_name: str + ) -> Optional[IndicatorMatch]: """Check the android property name against the list of indicators. :param property_name: Name of the Android property @@ -780,24 +732,21 @@ def check_android_property_name(self, property_name: str) -> Optional[dict]: return None for ioc in self.get_iocs("android_property_names"): - if property_name.lower() == ioc["value"].lower(): - self.log.warning( - 'Found a known suspicious Android property "%s" ' - 'matching indicators from "%s"', - property_name, - ioc["name"], + if property_name.lower() == ioc.value.lower(): + return IndicatorMatch( + ioc=ioc, + message=f'Found a known suspicious Android property "{property_name}" matching indicators from "{ioc.name}"', ) - return ioc return None - def check_domain(self, url: str) -> Union[dict, None]: + def check_domain(self, url: str) -> Optional[IndicatorMatch]: """ Renamed check_url now, kept for compatibility """ return self.check_url(url) - def check_domains(self, urls: list) -> Union[dict, None]: + def check_domains(self, urls: list) -> Optional[IndicatorMatch]: """ Renamed check_domains, kept for compatibility """ diff --git a/src/mvt/common/log.py b/src/mvt/common/log.py new file mode 100644 index 000000000..498b13d33 --- /dev/null +++ b/src/mvt/common/log.py @@ -0,0 +1,65 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2025 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 rich.console import Console +from rich.logging import RichHandler +from typing import Optional + +INFO = logging.INFO +DEBUG = logging.DEBUG +ERROR = logging.ERROR +FATAL = logging.CRITICAL +WARNING = logging.WARNING + +INFO_ALERT = 25 +LOW_ALERT = 35 +MEDIUM_ALERT = 45 +HIGH_ALERT = 55 +CRITICAL_ALERT = 65 + +logging.addLevelName(INFO_ALERT, "INFO") +logging.addLevelName(LOW_ALERT, "LOW") +logging.addLevelName(MEDIUM_ALERT, "MEDIUM") +logging.addLevelName(HIGH_ALERT, "HIGH") +logging.addLevelName(CRITICAL_ALERT, "CRITICAL") + + +class MVTLogHandler(RichHandler): + def __init__(self, console: Optional[Console] = None, level: int = logging.DEBUG): + super().__init__(console=console, level=level) + + def __add_prefix_space(self, level: str) -> str: + max_length = len("CRITICAL ALERT") + space = max_length - len(level) + return f"{level}{' ' * space}" + + def emit(self, record: logging.LogRecord): + try: + msg = rf"[grey50]\[{record.name}][/] {self.format(record)}" + + if record.levelno == ERROR: + msg = f"[bold red]{self.__add_prefix_space('ERROR')}[/bold red] {msg}" + elif record.levelno == FATAL: + msg = f"[bold red]{self.__add_prefix_space('FATAL')}[/bold red] {msg}" + elif record.levelno == WARNING: + msg = f"[yellow]{self.__add_prefix_space('WARNING')}[/yellow] {msg}" + elif record.levelno == INFO_ALERT: + msg = f"[blue]{self.__add_prefix_space('INFO ALERT')}[/blue] {msg}" + elif record.levelno == LOW_ALERT: + msg = f"[yellow]{self.__add_prefix_space('LOW ALERT')}[/yellow] {msg}" + elif record.levelno == MEDIUM_ALERT: + msg = f"[sandy_brown]{self.__add_prefix_space('MEDIUM ALERT')}[/sandy_brown] {msg}" + elif record.levelno == HIGH_ALERT: + msg = f"[red]{self.__add_prefix_space('HIGH ALERT')}[/red] {msg}" + elif record.levelno == CRITICAL_ALERT: + msg = f"[bold red]{self.__add_prefix_space('CRITICAL ALERT')}[/bold red] {msg}" + else: + msg = f"{self.__add_prefix_space('')} {msg}" + + self.console.print(msg) + + except Exception: + self.handleError(record) diff --git a/src/mvt/common/module.py b/src/mvt/common/module.py index 1468cf438..e57a9fc75 100644 --- a/src/mvt/common/module.py +++ b/src/mvt/common/module.py @@ -8,9 +8,18 @@ import logging import os import re -from typing import Any, Dict, List, Optional, Union +from dataclasses import asdict, is_dataclass +from typing import Any, Dict, Optional from .utils import CustomJSONEncoder, exec_or_profile +from .indicators import Indicators +from .alerts import AlertStore +from .module_types import ( + ModuleResults, + ModuleTimeline, + ModuleSerializedResult, + ModuleAtomicResult, +) class DatabaseNotFoundError(Exception): @@ -28,7 +37,7 @@ class InsufficientPrivileges(Exception): class MVTModule: """This class provides a base for all extraction modules.""" - enabled = True + enabled: bool = True slug: Optional[str] = None def __init__( @@ -38,7 +47,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[Dict[str, Any]] = None, log: logging.Logger = logging.getLogger(__name__), - results: Union[List[Dict[str, Any]], Dict[str, Any], None] = None, + results: ModuleResults = [], ) -> None: """Initialize module. @@ -46,7 +55,7 @@ def __init__( :type file_path: str :param target_path: Path to the target folder (backup or filesystem dump) - :type file_path: str + :type target_path: str :param results_path: Folder where results will be stored :type results_path: str :param fast_mode: Flag to enable or disable slow modules @@ -55,16 +64,21 @@ def __init__( :param results: Provided list of results entries :type results: list """ - self.file_path = file_path - self.target_path = target_path - self.results_path = results_path - self.module_options = module_options if module_options else {} + self.file_path: Optional[str] = file_path + self.target_path: Optional[str] = target_path + self.results_path: Optional[str] = results_path + self.module_options: Optional[Dict[str, Any]] = ( + module_options if module_options else {} + ) + self.log = log - self.indicators = None - self.results = results if results else [] - self.detected: List[Dict[str, Any]] = [] - self.timeline: List[Dict[str, str]] = [] - self.timeline_detected: List[Dict[str, str]] = [] + self.indicators: Optional[Indicators] = None + self.alertstore: AlertStore = AlertStore(log=log) + + self.results: ModuleResults = results if results else [] + self.detected: ModuleResults = [] + self.timeline: ModuleTimeline = [] + self.timeline_detected: ModuleTimeline = [] @classmethod def from_json(cls, json_path: str, log: logging.Logger): @@ -72,11 +86,11 @@ def from_json(cls, json_path: str, log: logging.Logger): results = json.load(handle) if log: log.info('Loaded %d results from "%s"', len(results), json_path) + return cls(results=results, log=log) @classmethod def get_slug(cls) -> str: - """Use the module's class name to retrieve a slug""" if cls.slug: return cls.slug @@ -84,26 +98,26 @@ def get_slug(cls) -> str: return re.sub("([a-z0-9])([A-Z])", r"\1_\2", sub).lower() def check_indicators(self) -> None: - """Check the results of this module against a provided list of - indicators. - - - """ raise NotImplementedError def save_to_json(self) -> None: - """Save the collected results to a json file.""" if not self.results_path: return name = self.get_slug() if self.results: + converted_results = [ + asdict(result) if is_dataclass(result) else result + for result in self.results + ] results_file_name = f"{name}.json" results_json_path = os.path.join(self.results_path, results_file_name) with open(results_json_path, "w", encoding="utf-8") as handle: try: - json.dump(self.results, handle, indent=4, cls=CustomJSONEncoder) + json.dump( + converted_results, handle, indent=4, cls=CustomJSONEncoder + ) except Exception as exc: self.log.error( "Unable to store results of module %s to file %s: %s", @@ -118,7 +132,7 @@ def save_to_json(self) -> None: with open(detected_json_path, "w", encoding="utf-8") as handle: json.dump(self.detected, handle, indent=4, cls=CustomJSONEncoder) - def serialize(self, record: dict) -> Union[dict, list, None]: + def serialize(self, result: ModuleAtomicResult) -> ModuleSerializedResult: raise NotImplementedError @staticmethod @@ -130,13 +144,21 @@ def _deduplicate_timeline(timeline: list) -> list: """ timeline_set = set() for record in timeline: - timeline_set.add(json.dumps(record, sort_keys=True)) + timeline_set.add( + json.dumps( + asdict(record) if is_dataclass(record) else record, sort_keys=True + ) + ) + return [json.loads(record) for record in timeline_set] def to_timeline(self) -> None: """Convert results into a timeline.""" + if not self.results: + return + for result in self.results: - record = self.serialize(result) + record: ModuleSerializedResult = self.serialize(result) if record: if isinstance(record, list): self.timeline.extend(record) diff --git a/src/mvt/common/module_types.py b/src/mvt/common/module_types.py new file mode 100644 index 000000000..f433482de --- /dev/null +++ b/src/mvt/common/module_types.py @@ -0,0 +1,29 @@ +# Mobile Verification Toolkit (MVT) +# Copyright (c) 2021-2025 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 .indicators import Indicator +from dataclasses import dataclass +from typing import List, Union, Optional + + +@dataclass +class ModuleAtomicResult: + timestamp: Optional[str] + matched_indicator: Optional[Indicator] + + +ModuleResults = List[ModuleAtomicResult] + + +@dataclass +class ModuleAtomicTimeline: + timestamp: str + module: str + event: str + data: str + + +ModuleTimeline = List[ModuleAtomicTimeline] +ModuleSerializedResult = Union[ModuleAtomicTimeline, ModuleTimeline] diff --git a/src/mvt/common/utils.py b/src/mvt/common/utils.py index 3d054f54f..500fd8539 100644 --- a/src/mvt/common/utils.py +++ b/src/mvt/common/utils.py @@ -12,7 +12,7 @@ import re from typing import Any, Iterator, Union -from rich.logging import RichHandler +from .log import MVTLogHandler from mvt.common.config import settings @@ -234,11 +234,10 @@ def init_logging(verbose: bool = False): """ Initialise logging for the MVT module """ - # Setup logging using Rich. log = logging.getLogger("mvt") - log.setLevel(logging.DEBUG) - consoleHandler = RichHandler(show_path=False, log_time_format="%X") - consoleHandler.setFormatter(logging.Formatter("[%(name)s] %(message)s")) + log.setLevel(logging.INFO) + consoleHandler = MVTLogHandler() + consoleHandler.setFormatter(logging.Formatter("%(message)s")) if verbose: consoleHandler.setLevel(logging.DEBUG) else: diff --git a/src/mvt/ios/cli.py b/src/mvt/ios/cli.py index 1d06c9662..6ffd106c9 100644 --- a/src/mvt/ios/cli.py +++ b/src/mvt/ios/cli.py @@ -228,11 +228,8 @@ def check_backup( log.info("Checking iTunes backup located at: %s", backup_path) cmd.run() - - if cmd.detected_count > 0: - log.warning( - "The analysis of the backup produced %d detections!", cmd.detected_count - ) + cmd.show_alerts_brief() + cmd.show_support_message() # ============================================================================== @@ -275,12 +272,8 @@ def check_fs(ctx, iocs, output, fast, list_modules, module, hashes, verbose, dum log.info("Checking iOS filesystem located at: %s", dump_path) cmd.run() - - if cmd.detected_count > 0: - log.warning( - "The analysis of the iOS filesystem produced %d detections!", - cmd.detected_count, - ) + cmd.show_alerts_brief() + cmd.show_support_message() # ============================================================================== @@ -308,6 +301,8 @@ def check_iocs(ctx, iocs, list_modules, module, folder): return cmd.run() + cmd.show_alerts_brief() + cmd.show_support_message() # ============================================================================== diff --git a/tests/android_androidqf/test_packages.py b/tests/android_androidqf/test_packages.py index 966d8a6ce..fe6332a13 100644 --- a/tests/android_androidqf/test_packages.py +++ b/tests/android_androidqf/test_packages.py @@ -91,7 +91,8 @@ def test_packages_ioc_package_names(self, module, indicators_factory): assert len(possible_detected_app) == 1 assert possible_detected_app[0]["name"] == "com.malware.blah" assert ( - possible_detected_app[0]["matched_indicator"]["value"] == "com.malware.blah" + possible_detected_app[0]["matched_indicator"].ioc.value + == "com.malware.blah" ) def test_packages_ioc_sha256(self, module, indicators_factory): @@ -109,7 +110,7 @@ def test_packages_ioc_sha256(self, module, indicators_factory): assert len(possible_detected_app) == 1 assert possible_detected_app[0]["name"] == "com.malware.muahaha" assert ( - possible_detected_app[0]["matched_indicator"]["value"] + possible_detected_app[0]["matched_indicator"].ioc.value == "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa" ) @@ -128,6 +129,6 @@ def test_packages_certificate_hash_ioc(self, module, indicators_factory): assert len(possible_detected_app) == 1 assert possible_detected_app[0]["name"] == "com.malware.muahaha" assert ( - possible_detected_app[0]["matched_indicator"]["value"] + possible_detected_app[0]["matched_indicator"].ioc.value == "c7e56178748be1441370416d4c10e34817ea0c961eb636c8e9d98e0fd79bf730" ) From ca0bc46f1146321ab73255bdfa70e02cf6eef86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Sun, 16 Feb 2025 00:30:45 +0100 Subject: [PATCH 07/37] Fix up, remove ADB module base --- src/mvt/android/modules/adb/base.py | 355 ---------------------------- 1 file changed, 355 deletions(-) delete mode 100644 src/mvt/android/modules/adb/base.py diff --git a/src/mvt/android/modules/adb/base.py b/src/mvt/android/modules/adb/base.py deleted file mode 100644 index 72df79446..000000000 --- a/src/mvt/android/modules/adb/base.py +++ /dev/null @@ -1,355 +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 base64 -import logging -import os -import random -import string -import sys -import tempfile -import time -from typing import Callable, Optional - -from adb_shell.adb_device import AdbDeviceTcp, AdbDeviceUsb -from adb_shell.auth.keygen import keygen, write_public_keyfile -from adb_shell.auth.sign_pythonrsa import PythonRSASigner -from adb_shell.exceptions import ( - AdbCommandFailureException, - DeviceAuthError, - UsbDeviceNotFoundError, - UsbReadFailedError, -) -from usb1 import USBErrorAccess, USBErrorBusy - -from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password -from mvt.android.parsers.backup import ( - InvalidBackupPassword, - parse_ab_header, - parse_backup_file, -) -from mvt.common.module import InsufficientPrivileges, MVTModule - -ADB_KEY_PATH = os.path.expanduser("~/.android/adbkey") -ADB_PUB_KEY_PATH = os.path.expanduser("~/.android/adbkey.pub") - - -class AndroidExtraction(MVTModule): - """This class provides a base for all Android extraction modules.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - self.device = None - self.serial = None - - @staticmethod - def _adb_check_keys() -> None: - """Make sure Android adb keys exist.""" - if not os.path.isdir(os.path.dirname(ADB_KEY_PATH)): - os.makedirs(os.path.dirname(ADB_KEY_PATH)) - - if not os.path.exists(ADB_KEY_PATH): - keygen(ADB_KEY_PATH) - - if not os.path.exists(ADB_PUB_KEY_PATH): - write_public_keyfile(ADB_KEY_PATH, ADB_PUB_KEY_PATH) - - def _adb_connect(self) -> None: - """Connect to the device over adb.""" - self._adb_check_keys() - - with open(ADB_KEY_PATH, "rb") as handle: - priv_key = handle.read() - - with open(ADB_PUB_KEY_PATH, "rb") as handle: - pub_key = handle.read() - - signer = PythonRSASigner(pub_key, priv_key) - - # If no serial was specified or if the serial does not seem to be - # a HOST:PORT definition, we use the USB transport. - if not self.serial or ":" not in self.serial: - try: - self.device = AdbDeviceUsb(serial=self.serial) - except UsbDeviceNotFoundError: - self.log.critical( - "No device found. Make sure it is connected and unlocked." - ) - sys.exit(-1) - # Otherwise we try to use the TCP transport. - else: - addr = self.serial.split(":") - if len(addr) < 2: - raise ValueError( - "TCP serial number must follow the format: `address:port`" - ) - - self.device = AdbDeviceTcp( - addr[0], int(addr[1]), default_transport_timeout_s=30.0 - ) - - while True: - try: - self.device.connect(rsa_keys=[signer], auth_timeout_s=5) - except (USBErrorBusy, USBErrorAccess): - self.log.critical( - "Device is busy, maybe run `adb kill-server` and try again." - ) - sys.exit(-1) - except DeviceAuthError: - self.log.error( - "You need to authorize this computer on the Android device. " - "Retrying in 5 seconds..." - ) - time.sleep(5) - except UsbReadFailedError: - self.log.error( - "Unable to connect to the device over USB. " - "Try to unplug, plug the device and start again." - ) - sys.exit(-1) - except OSError as exc: - if exc.errno == 113 and self.serial: - self.log.critical( - "Unable to connect to the device %s: " - "did you specify the correct IP address?", - self.serial, - ) - sys.exit(-1) - else: - break - - def _adb_disconnect(self) -> None: - """Close adb connection to the device.""" - self.device.close() - - def _adb_reconnect(self) -> None: - """Reconnect to device using adb.""" - self.log.info("Reconnecting ...") - self._adb_disconnect() - self._adb_connect() - - def _adb_command(self, command: str, decode: bool = True) -> str: - """Execute an adb shell command. - - :param command: Shell command to execute - :returns: Output of command - - """ - return self.device.shell(command, read_timeout_s=200.0, decode=decode) - - def _adb_check_if_root(self) -> bool: - """Check if we have a `su` binary on the Android device. - - - :returns: Boolean indicating whether a `su` binary is present or not - - """ - result = self._adb_command("command -v su && su -c true") - return bool(result) and "Permission denied" not in result - - def _adb_root_or_die(self) -> None: - """Check if we have a `su` binary, otherwise raise an Exception.""" - if not self._adb_check_if_root(): - raise InsufficientPrivileges( - "This module is optionally available " - "in case the device is already rooted." - " Do NOT root your own device!" - ) - - def _adb_command_as_root(self, command): - """Execute an adb shell command. - - :param command: Shell command to execute as root - :returns: Output of command - - """ - return self._adb_command(f"su -c {command}") - - def _adb_check_file_exists(self, file: str) -> bool: - """Verify that a file exists. - - :param file: Path of the file - :returns: Boolean indicating whether the file exists or not - - """ - - # TODO: Need to support checking files without root privileges as well. - - # Check if we have root, if not raise an Exception. - self._adb_root_or_die() - - return bool(self._adb_command_as_root(f"[ ! -f {file} ] || echo 1")) - - def _adb_download( - self, - remote_path: str, - local_path: str, - progress_callback: Optional[Callable] = None, - retry_root: Optional[bool] = True, - ) -> None: - """Download a file form the device. - - :param remote_path: Path to download from the device - :param local_path: Path to where to locally store the copy of the file - :param progress_callback: Callback for download progress bar - (Default value = None) - :param retry_root: Default value = True) - - """ - try: - self.device.pull(remote_path, local_path, progress_callback) - except AdbCommandFailureException as exc: - if retry_root: - self._adb_download_root(remote_path, local_path, progress_callback) - else: - raise Exception( - f"Unable to download file {remote_path}: {exc}" - ) from exc - - def _adb_download_root( - self, - remote_path: str, - local_path: str, - progress_callback: Optional[Callable] = None, - ) -> None: - try: - # Check if we have root, if not raise an Exception. - self._adb_root_or_die() - - # We generate a random temporary filename. - allowed_chars = ( - string.ascii_uppercase + string.ascii_lowercase + string.digits - ) - tmp_filename = "tmp_" + "".join(random.choices(allowed_chars, k=10)) - - # We create a temporary local file. - new_remote_path = f"/sdcard/{tmp_filename}" - - # We copy the file from the data folder to /sdcard/. - cp_output = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}") - if ( - cp_output.startswith("cp: ") - and "No such file or directory" in cp_output - ): - raise Exception(f"Unable to process file {remote_path}: File not found") - if cp_output.startswith("cp: ") and "Permission denied" in cp_output: - raise Exception( - f"Unable to process file {remote_path}: Permission denied" - ) - - # We download from /sdcard/ to the local temporary file. - # If it doesn't work now, don't try again (retry_root=False) - self._adb_download( - new_remote_path, local_path, progress_callback, retry_root=False - ) - - # Delete the copy on /sdcard/. - self._adb_command(f"rm -rf {new_remote_path}") - - except AdbCommandFailureException as exc: - raise Exception(f"Unable to download file {remote_path}: {exc}") from exc - - def _adb_process_file(self, remote_path: str, process_routine: Callable) -> None: - """Download a local copy of a file which is only accessible as root. - This is a wrapper around process_routine. - - :param remote_path: Path of the file on the device to process - :param process_routine: Function to be called on the local copy of the - downloaded file - - """ - # Connect to the device over adb. - # Check if we have root, if not raise an Exception. - self._adb_root_or_die() - - # We create a temporary local file. - tmp = tempfile.NamedTemporaryFile() - local_path = tmp.name - local_name = os.path.basename(tmp.name) - new_remote_path = f"/sdcard/Download/{local_name}" - - # We copy the file from the data folder to /sdcard/. - cp_output = self._adb_command_as_root(f"cp {remote_path} {new_remote_path}") - if cp_output.startswith("cp: ") and "No such file or directory" in cp_output: - raise Exception(f"Unable to process file {remote_path}: File not found") - if cp_output.startswith("cp: ") and "Permission denied" in cp_output: - raise Exception(f"Unable to process file {remote_path}: Permission denied") - - # We download from /sdcard/ to the local temporary file. - self._adb_download(new_remote_path, local_path) - - # Launch the provided process routine! - process_routine(local_path) - - # Delete the local copy. - tmp.close() - # Delete the copy on /sdcard/. - self._adb_command(f"rm -f {new_remote_path}") - - def _generate_backup(self, package_name: str) -> bytes: - self.log.info( - "Please check phone and accept Android backup prompt. " - "You may need to set a backup password. \a" - ) - - if self.module_options.get("backup_password", None): - self.log.warning( - "Backup password already set from command line or environment " - "variable. You should use the same password if enabling encryption!" - ) - - # TODO: Base64 encoding as temporary fix to avoid byte-mangling over - # the shell transport... - cmd = f"/system/bin/bu backup -nocompress '{package_name}' | base64" - backup_output_b64 = self._adb_command(cmd) - backup_output = base64.b64decode(backup_output_b64) - header = parse_ab_header(backup_output) - - if not header["backup"]: - self.log.error( - "Extracting SMS via Android backup failed. No valid backup data found." - ) - return None - - if header["encryption"] == "none": - return parse_backup_file(backup_output, password=None) - - for _ in range(0, 3): - backup_password = prompt_or_load_android_backup_password( - self.log, self.module_options - ) - if not backup_password: - # Fail as no backup password loaded for this encrypted backup - self.log.critical("No backup password provided.") - try: - decrypted_backup_tar = parse_backup_file(backup_output, backup_password) - return decrypted_backup_tar - except InvalidBackupPassword: - self.log.error("You provided the wrong password! Please try again...") - - self.log.error("All attempts to decrypt backup with password failed!") - - return None - - def run(self) -> None: - """Run the main procedure.""" - raise NotImplementedError From 2d547662f8a139c40fdcba8dc74251262b569412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Wed, 19 Feb 2025 23:46:03 +0100 Subject: [PATCH 08/37] Rework old detections tracking into stuctured alert levels --- .../artifacts/dumpsys_accessibility.py | 8 +- src/mvt/android/artifacts/dumpsys_appops.py | 65 ++++---- .../artifacts/dumpsys_battery_daily.py | 12 +- .../artifacts/dumpsys_battery_history.py | 8 +- src/mvt/android/artifacts/dumpsys_dbinfo.py | 10 +- .../artifacts/dumpsys_package_activities.py | 10 +- src/mvt/android/artifacts/dumpsys_packages.py | 26 ++-- .../artifacts/dumpsys_platform_compat.py | 8 +- .../android/artifacts/dumpsys_receivers.py | 10 +- src/mvt/android/artifacts/file_timestamps.py | 4 +- src/mvt/android/artifacts/getprop.py | 13 +- src/mvt/android/artifacts/processes.py | 16 +- .../android/artifacts/tombstone_crashes.py | 35 +++-- src/mvt/android/cmd_check_backup.py | 4 +- src/mvt/android/modules/adb/chrome_history.py | 18 ++- src/mvt/android/modules/adb/dumpsys_full.py | 3 +- src/mvt/android/modules/adb/files.py | 3 +- src/mvt/android/modules/adb/getprop.py | 3 +- src/mvt/android/modules/adb/logcat.py | 3 +- src/mvt/android/modules/adb/packages.py | 142 +++++++++--------- src/mvt/android/modules/adb/processes.py | 3 +- src/mvt/android/modules/adb/root_binaries.py | 3 +- src/mvt/android/modules/adb/selinux_status.py | 3 +- src/mvt/android/modules/adb/settings.py | 3 +- src/mvt/android/modules/adb/sms.py | 20 ++- src/mvt/android/modules/adb/whatsapp.py | 11 +- .../android/modules/androidqf/aqf_files.py | 39 ++--- .../android/modules/androidqf/aqf_getprop.py | 3 +- .../modules/androidqf/aqf_log_timestamps.py | 3 +- .../android/modules/androidqf/aqf_packages.py | 104 +++++++------ .../modules/androidqf/aqf_processes.py | 3 +- .../android/modules/androidqf/aqf_settings.py | 3 +- src/mvt/android/modules/androidqf/base.py | 5 +- src/mvt/android/modules/androidqf/sms.py | 8 +- src/mvt/android/modules/backup/base.py | 6 +- src/mvt/android/modules/backup/sms.py | 15 +- src/mvt/android/modules/bugreport/base.py | 4 +- .../bugreport/dumpsys_accessibility.py | 3 +- .../modules/bugreport/dumpsys_activities.py | 3 +- .../modules/bugreport/dumpsys_adb_state.py | 3 +- .../modules/bugreport/dumpsys_appops.py | 3 +- .../bugreport/dumpsys_battery_daily.py | 3 +- .../bugreport/dumpsys_battery_history.py | 3 +- .../modules/bugreport/dumpsys_dbinfo.py | 3 +- .../modules/bugreport/dumpsys_getprop.py | 3 +- .../modules/bugreport/dumpsys_packages.py | 3 +- .../bugreport/dumpsys_platform_compat.py | 3 +- .../modules/bugreport/dumpsys_receivers.py | 3 +- .../modules/bugreport/fs_timestamps.py | 3 +- .../android/modules/bugreport/tombstones.py | 3 +- src/mvt/android/utils.py | 10 +- src/mvt/common/artifact.py | 26 +--- src/mvt/common/cmd_check_iocs.py | 2 +- src/mvt/common/command.py | 2 - src/mvt/common/indicators.py | 6 +- src/mvt/common/module.py | 25 +-- src/mvt/ios/modules/backup/backup_info.py | 3 +- .../modules/backup/configuration_profiles.py | 41 ++--- src/mvt/ios/modules/backup/manifest.py | 31 ++-- src/mvt/ios/modules/backup/profile_events.py | 43 +++--- src/mvt/ios/modules/base.py | 5 +- src/mvt/ios/modules/fs/analytics.py | 39 +++-- .../ios/modules/fs/analytics_ios_versions.py | 11 +- src/mvt/ios/modules/fs/cache_files.py | 26 ++-- src/mvt/ios/modules/fs/filesystem.py | 27 ++-- src/mvt/ios/modules/fs/net_netusage.py | 3 +- src/mvt/ios/modules/fs/safari_favicon.py | 23 +-- src/mvt/ios/modules/fs/shutdownlog.py | 34 +++-- src/mvt/ios/modules/fs/version_history.py | 11 +- src/mvt/ios/modules/fs/webkit_base.py | 9 +- src/mvt/ios/modules/fs/webkit_indexeddb.py | 11 +- src/mvt/ios/modules/fs/webkit_localstorage.py | 11 +- .../modules/fs/webkit_safariviewservice.py | 3 +- src/mvt/ios/modules/mixed/applications.py | 60 +++++--- src/mvt/ios/modules/mixed/calendar.py | 30 ++-- src/mvt/ios/modules/mixed/calls.py | 5 +- src/mvt/ios/modules/mixed/chrome_favicon.py | 24 +-- src/mvt/ios/modules/mixed/chrome_history.py | 19 ++- src/mvt/ios/modules/mixed/contacts.py | 3 +- src/mvt/ios/modules/mixed/firefox_favicon.py | 23 +-- src/mvt/ios/modules/mixed/firefox_history.py | 19 ++- .../ios/modules/mixed/global_preferences.py | 13 +- src/mvt/ios/modules/mixed/idstatuscache.py | 30 ++-- src/mvt/ios/modules/mixed/interactionc.py | 11 +- src/mvt/ios/modules/mixed/locationd.py | 91 ++++++----- src/mvt/ios/modules/mixed/net_datausage.py | 3 +- .../ios/modules/mixed/osanalytics_addaily.py | 19 ++- .../ios/modules/mixed/safari_browserstate.py | 31 ++-- src/mvt/ios/modules/mixed/safari_history.py | 27 ++-- src/mvt/ios/modules/mixed/shortcuts.py | 19 ++- src/mvt/ios/modules/mixed/sms.py | 26 ++-- src/mvt/ios/modules/mixed/sms_attachments.py | 30 ++-- src/mvt/ios/modules/mixed/tcc.py | 19 ++- .../mixed/webkit_resource_load_statistics.py | 21 ++- .../mixed/webkit_session_resource_log.py | 21 ++- src/mvt/ios/modules/mixed/whatsapp.py | 19 ++- src/mvt/ios/modules/net_base.py | 50 ++++-- .../test_artifact_dumpsys_accessibility.py | 4 +- tests/android/test_artifact_dumpsys_appops.py | 14 +- .../test_artifact_dumpsys_battery_daily.py | 4 +- .../test_artifact_dumpsys_battery_history.py | 4 +- tests/android/test_artifact_dumpsys_dbinfo.py | 4 +- ...est_artifact_dumpsys_package_activities.py | 4 +- .../android/test_artifact_dumpsys_packages.py | 4 +- .../test_artifact_dumpsys_platform_compat.py | 4 +- .../test_artifact_dumpsys_receivers.py | 4 +- tests/android/test_artifact_getprop.py | 4 +- tests/android/test_artifact_processes.py | 4 +- tests/android/test_backup_parser.py | 1 - tests/android_androidqf/test_files.py | 2 +- tests/android_androidqf/test_getprop.py | 8 +- tests/android_androidqf/test_packages.py | 61 ++++---- tests/android_androidqf/test_processes.py | 2 +- tests/android_androidqf/test_settings.py | 2 +- tests/android_androidqf/test_sms.py | 2 +- tests/android_androidqf/test_tcc.py | 36 +++++ tests/android_bugreport/test_bugreport.py | 8 +- tests/ios_backup/test_calendar.py | 4 +- tests/ios_backup/test_datausage.py | 10 +- tests/ios_backup/test_global_preferences.py | 8 +- tests/ios_backup/test_manifest.py | 4 +- tests/ios_backup/test_safari_browserstate.py | 4 +- tests/ios_backup/test_sms.py | 4 +- tests/ios_backup/test_tcc.py | 8 +- .../test_webkit_resource_load_statistics.py | 2 +- tests/ios_fs/test_filesystem.py | 4 +- 126 files changed, 1143 insertions(+), 772 deletions(-) create mode 100644 tests/android_androidqf/test_tcc.py diff --git a/src/mvt/android/artifacts/dumpsys_accessibility.py b/src/mvt/android/artifacts/dumpsys_accessibility.py index fca84dfff..66b1684a5 100644 --- a/src/mvt/android/artifacts/dumpsys_accessibility.py +++ b/src/mvt/android/artifacts/dumpsys_accessibility.py @@ -14,10 +14,10 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_app_id(result["package_name"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_app_id(result["package_name"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) continue def parse(self, content: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_appops.py b/src/mvt/android/artifacts/dumpsys_appops.py index 8323c8a12..36d61c8ad 100644 --- a/src/mvt/android/artifacts/dumpsys_appops.py +++ b/src/mvt/android/artifacts/dumpsys_appops.py @@ -4,9 +4,9 @@ # https://license.mvt.re/1.1/ from datetime import datetime -from typing import Any, Dict, List, Union from mvt.common.utils import convert_datetime_to_iso +from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult from .artifact import AndroidArtifact @@ -20,9 +20,9 @@ class DumpsysAppopsArtifact(AndroidArtifact): Parser for dumpsys app ops info """ - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, result: ModuleAtomicResult) -> ModuleSerializedResult: records = [] - for perm in record["permissions"]: + for perm in result["permissions"]: if "entries" not in perm: continue @@ -33,7 +33,7 @@ def serialize(self, record: dict) -> Union[dict, list]: "timestamp": entry["timestamp"], "module": self.__class__.__name__, "event": entry["access"], - "data": f"{record['package_name']} access to " + "data": f"{result['package_name']} access to " f"{perm['name']}: {entry['access']}", } ) @@ -43,48 +43,51 @@ def serialize(self, record: dict) -> Union[dict, list]: def check_indicators(self) -> None: for result in self.results: if self.indicators: - ioc = self.indicators.check_app_id(result.get("package_name")) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_app_id(result.get("package_name")) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", result + ) continue - detected_permissions = [] + # We use a placeholder entry to create a basic alert even without permission entries. + placeholder_entry = {"access": "Unknown", "timestamp": ""} + for perm in result["permissions"]: if ( perm["name"] in RISKY_PERMISSIONS # and perm["access"] == "allow" ): - detected_permissions.append(perm) - for entry in sorted(perm["entries"], key=lambda x: x["timestamp"]): - self.log.warning( - "Package '%s' had risky permission '%s' set to '%s' at %s", - result["package_name"], - perm["name"], - entry["access"], + for entry in sorted( + perm["entries"] or [placeholder_entry], + key=lambda x: x["timestamp"], + ): + cleaned_result = result.copy() + cleaned_result["permissions"] = [perm] + self.alertstore.medium( + self.get_slug(), + f"Package '{result['package_name']}' had risky permission '{perm['name']}' set to '{entry['access']}' at {entry['timestamp']}", entry["timestamp"], + cleaned_result, ) elif result["package_name"] in RISKY_PACKAGES: - detected_permissions.append(perm) - for entry in sorted(perm["entries"], key=lambda x: x["timestamp"]): - self.log.warning( - "Risky package '%s' had '%s' permission set to '%s' at %s", - result["package_name"], - perm["name"], - entry["access"], + for entry in sorted( + perm["entries"] or [placeholder_entry], + key=lambda x: x["timestamp"], + ): + cleaned_result = result.copy() + cleaned_result["permissions"] = [perm] + self.alertstore.medium( + self.get_slug(), + f"Risky package '{result['package_name']}' had '{perm['name']}' permission set to '{entry['access']}' at {entry['timestamp']}", entry["timestamp"], + cleaned_result, ) - if detected_permissions: - # We clean the result to only include the risky permission, otherwise the timeline - # will be polluted with all the other irrelevant permissions - cleaned_result = result.copy() - cleaned_result["permissions"] = detected_permissions - self.detected.append(cleaned_result) - def parse(self, output: str) -> None: - self.results: List[Dict[str, Any]] = [] + # self.results: List[Dict[str, Any]] = [] perm = {} package = {} entry = {} diff --git a/src/mvt/android/artifacts/dumpsys_battery_daily.py b/src/mvt/android/artifacts/dumpsys_battery_daily.py index 06b980bd3..03082f55d 100644 --- a/src/mvt/android/artifacts/dumpsys_battery_daily.py +++ b/src/mvt/android/artifacts/dumpsys_battery_daily.py @@ -3,9 +3,9 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ -from typing import Union from .artifact import AndroidArtifact +from mvt.common.module_types import ModuleSerializedResult, ModuleAtomicResult class DumpsysBatteryDailyArtifact(AndroidArtifact): @@ -13,7 +13,7 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact): Parser for dumpsys dattery daily updates. """ - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["from"], "module": self.__class__.__name__, @@ -27,10 +27,10 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_app_id(result["package_name"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_app_id(result["package_name"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) continue def parse(self, output: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_battery_history.py b/src/mvt/android/artifacts/dumpsys_battery_history.py index 35e41ec39..cd2d6a8cc 100644 --- a/src/mvt/android/artifacts/dumpsys_battery_history.py +++ b/src/mvt/android/artifacts/dumpsys_battery_history.py @@ -16,10 +16,10 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_app_id(result["package_name"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_app_id(result["package_name"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) continue def parse(self, data: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_dbinfo.py b/src/mvt/android/artifacts/dumpsys_dbinfo.py index 1064e49f7..c5f251660 100644 --- a/src/mvt/android/artifacts/dumpsys_dbinfo.py +++ b/src/mvt/android/artifacts/dumpsys_dbinfo.py @@ -20,10 +20,12 @@ def check_indicators(self) -> None: for result in self.results: path = result.get("path", "") for part in path.split("/"): - ioc = self.indicators.check_app_id(part) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_app_id(part) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", result + ) continue def parse(self, output: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_package_activities.py b/src/mvt/android/artifacts/dumpsys_package_activities.py index d8d284f6c..f3b5d2ed0 100644 --- a/src/mvt/android/artifacts/dumpsys_package_activities.py +++ b/src/mvt/android/artifacts/dumpsys_package_activities.py @@ -12,10 +12,12 @@ def check_indicators(self) -> None: return for activity in self.results: - ioc = self.indicators.check_app_id(activity["package_name"]) - if ioc: - activity["matched_indicator"] = ioc - self.detected.append(activity) + ioc_match = self.indicators.check_app_id(activity["package_name"]) + if ioc_match: + activity["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", activity + ) continue def parse(self, content: str): diff --git a/src/mvt/android/artifacts/dumpsys_packages.py b/src/mvt/android/artifacts/dumpsys_packages.py index 220418040..be59db832 100644 --- a/src/mvt/android/artifacts/dumpsys_packages.py +++ b/src/mvt/android/artifacts/dumpsys_packages.py @@ -4,35 +4,39 @@ # https://license.mvt.re/1.1/ import re -from typing import Any, Dict, List, Union +from typing import Any, Dict, List from mvt.android.utils import ROOT_PACKAGES from .artifact import AndroidArtifact +from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult class DumpsysPackagesArtifact(AndroidArtifact): def check_indicators(self) -> None: for result in self.results: + # XXX: De-duplication Package detections if result["package_name"] in ROOT_PACKAGES: - self.log.warning( - 'Found an installed package related to rooting/jailbreaking: "%s"', - result["package_name"], + self.alertstore.medium( + self.get_slug(), + f'Found an installed package related to rooting/jailbreaking: "{result["package_name"]}"', + "", + result, ) - self.detected.append(result) + self.alertstore.log_latest() continue if not self.indicators: continue - ioc = self.indicators.check_app_id(result.get("package_name", "")) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_app_id(result.get("package_name", "")) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.log_latest() - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: records = [] - timestamps = [ {"event": "package_install", "timestamp": record["timestamp"]}, { diff --git a/src/mvt/android/artifacts/dumpsys_platform_compat.py b/src/mvt/android/artifacts/dumpsys_platform_compat.py index e1037f01c..012a6e378 100644 --- a/src/mvt/android/artifacts/dumpsys_platform_compat.py +++ b/src/mvt/android/artifacts/dumpsys_platform_compat.py @@ -16,10 +16,10 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_app_id(result["package_name"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_app_id(result["package_name"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) continue def parse(self, data: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_receivers.py b/src/mvt/android/artifacts/dumpsys_receivers.py index 6ef1c08f0..75f5afe60 100644 --- a/src/mvt/android/artifacts/dumpsys_receivers.py +++ b/src/mvt/android/artifacts/dumpsys_receivers.py @@ -50,10 +50,12 @@ def check_indicators(self) -> None: if not self.indicators: continue - ioc = self.indicators.check_app_id(receiver["package_name"]) - if ioc: - receiver["matched_indicator"] = ioc - self.detected.append({intent: receiver}) + ioc_match = self.indicators.check_app_id(receiver["package_name"]) + if ioc_match: + receiver["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", {intent: receiver} + ) continue def parse(self, output: str) -> None: diff --git a/src/mvt/android/artifacts/file_timestamps.py b/src/mvt/android/artifacts/file_timestamps.py index aa2dc2591..98b878928 100644 --- a/src/mvt/android/artifacts/file_timestamps.py +++ b/src/mvt/android/artifacts/file_timestamps.py @@ -2,13 +2,13 @@ # 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 typing import Union from .artifact import AndroidArtifact +from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult class FileTimestampsArtifact(AndroidArtifact): - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: records = [] for ts in set( diff --git a/src/mvt/android/artifacts/getprop.py b/src/mvt/android/artifacts/getprop.py index 6c7030f6e..053b5fd31 100644 --- a/src/mvt/android/artifacts/getprop.py +++ b/src/mvt/android/artifacts/getprop.py @@ -59,13 +59,16 @@ def check_indicators(self) -> None: self.log.info("%s: %s", entry["name"], entry["value"]) if entry["name"] == "ro.build.version.security_patch": - warn_android_patch_level(entry["value"], self.log) + warning_message = warn_android_patch_level(entry["value"], self.log) + self.alertstore.medium(self.get_slug(), warning_message, "", entry) if not self.indicators: return for result in self.results: - ioc = self.indicators.check_android_property_name(result.get("name", "")) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_android_property_name( + result.get("name", "") + ) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) diff --git a/src/mvt/android/artifacts/processes.py b/src/mvt/android/artifacts/processes.py index 273ac109c..7b6f41f7b 100644 --- a/src/mvt/android/artifacts/processes.py +++ b/src/mvt/android/artifacts/processes.py @@ -58,13 +58,13 @@ def check_indicators(self) -> None: if result["proc_name"] == "gatekeeperd": continue - ioc = self.indicators.check_app_id(proc_name) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_app_id(proc_name) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) continue - ioc = self.indicators.check_process(proc_name) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_process(proc_name) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) diff --git a/src/mvt/android/artifacts/tombstone_crashes.py b/src/mvt/android/artifacts/tombstone_crashes.py index 38271547f..76d55066f 100644 --- a/src/mvt/android/artifacts/tombstone_crashes.py +++ b/src/mvt/android/artifacts/tombstone_crashes.py @@ -4,12 +4,13 @@ # https://license.mvt.re/1.1/ import datetime -from typing import List, Optional, Union +from typing import List, Optional import pydantic import betterproto from mvt.common.utils import convert_datetime_to_iso +from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult from mvt.android.parsers.proto.tombstone import Tombstone from .artifact import AndroidArtifact @@ -75,7 +76,7 @@ class TombstoneCrashArtifact(AndroidArtifact): This parser can parse both text and protobuf tombstone crash files. """ - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["timestamp"], "module": self.__class__.__name__, @@ -91,18 +92,20 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_process(result["process_name"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_process(result["process_name"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) continue if result.get("command_line", []): command_name = result.get("command_line")[0].split("/")[-1] - ioc = self.indicators.check_process(command_name) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_process(command_name) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", result + ) continue SUSPICIOUS_UIDS = [ @@ -111,11 +114,15 @@ def check_indicators(self) -> None: 2000, # shell ] if result["uid"] in SUSPICIOUS_UIDS: - self.log.warning( - f"Potentially suspicious crash in process '{result['process_name']}' " - f"running as UID '{result['uid']}' in tombstone '{result['file_name']}' at {result['timestamp']}" + self.alertstore.medium( + self.get_slug(), + ( + f"Potentially suspicious crash in process '{result['process_name']}' " + f"running as UID '{result['uid']}' in tombstone '{result['file_name']}' at {result['timestamp']}" + ), + "", + result, ) - self.detected.append(result) def parse_protobuf( self, file_name: str, file_timestamp: datetime.datetime, data: bytes diff --git a/src/mvt/android/cmd_check_backup.py b/src/mvt/android/cmd_check_backup.py index e366d2b5f..2c39bae2f 100644 --- a/src/mvt/android/cmd_check_backup.py +++ b/src/mvt/android/cmd_check_backup.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import List, Optional -from mvt.android.modules.backup.base import BackupExtraction +from mvt.android.modules.backup.base import BackupModule from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password from mvt.android.parsers.backup import ( AndroidBackupParsingError, @@ -113,7 +113,7 @@ def init(self) -> None: ) sys.exit(1) - def module_init(self, module: BackupExtraction) -> None: # type: ignore[override] + def module_init(self, module: BackupModule) -> None: # type: ignore[override] if self.backup_type == "folder": module.from_dir(self.target_path, self.backup_files) else: diff --git a/src/mvt/android/modules/adb/chrome_history.py b/src/mvt/android/modules/adb/chrome_history.py index 54be2a0ec..9568e4447 100644 --- a/src/mvt/android/modules/adb/chrome_history.py +++ b/src/mvt/android/modules/adb/chrome_history.py @@ -6,9 +6,14 @@ import logging import os import sqlite3 -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleSerializedResult, + ModuleResults, +) from .base import AndroidExtraction @@ -25,7 +30,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -37,7 +42,7 @@ def __init__( ) self.results = [] - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, @@ -51,9 +56,10 @@ def check_indicators(self) -> None: return for result in self.results: - if self.indicators.check_url(result["url"]): - self.detected.append(result) - continue + ioc_match = self.indicators.check_url(result["url"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) def _parse_db(self, db_path: str) -> None: """Parse a Chrome History database file. diff --git a/src/mvt/android/modules/adb/dumpsys_full.py b/src/mvt/android/modules/adb/dumpsys_full.py index 61033575f..fa1a6b3ca 100644 --- a/src/mvt/android/modules/adb/dumpsys_full.py +++ b/src/mvt/android/modules/adb/dumpsys_full.py @@ -8,6 +8,7 @@ from typing import Optional from .base import AndroidExtraction +from mvt.common.module_types import ModuleResults class DumpsysFull(AndroidExtraction): @@ -20,7 +21,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/adb/files.py b/src/mvt/android/modules/adb/files.py index a8a11a2d3..28b108871 100644 --- a/src/mvt/android/modules/adb/files.py +++ b/src/mvt/android/modules/adb/files.py @@ -9,6 +9,7 @@ from typing import Optional, Union from mvt.common.utils import convert_unix_to_iso +from mvt.common.module_types import ModuleResults from .base import AndroidExtraction @@ -32,7 +33,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/adb/getprop.py b/src/mvt/android/modules/adb/getprop.py index 71bface59..a9c77be07 100644 --- a/src/mvt/android/modules/adb/getprop.py +++ b/src/mvt/android/modules/adb/getprop.py @@ -9,6 +9,7 @@ from mvt.android.artifacts.getprop import GetProp as GetPropArtifact from .base import AndroidExtraction +from mvt.common.module_types import ModuleResults class Getprop(GetPropArtifact, AndroidExtraction): @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/adb/logcat.py b/src/mvt/android/modules/adb/logcat.py index bdc8c4887..41418a1b9 100644 --- a/src/mvt/android/modules/adb/logcat.py +++ b/src/mvt/android/modules/adb/logcat.py @@ -8,6 +8,7 @@ from typing import Optional from .base import AndroidExtraction +from mvt.common.module_types import ModuleResults class Logcat(AndroidExtraction): @@ -20,7 +21,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/adb/packages.py b/src/mvt/android/modules/adb/packages.py index 1d9c82134..04563ae46 100644 --- a/src/mvt/android/modules/adb/packages.py +++ b/src/mvt/android/modules/adb/packages.py @@ -4,12 +4,7 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union - -from rich.console import Console -from rich.progress import track -from rich.table import Table -from rich.text import Text +from typing import Optional from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact from mvt.android.utils import ( @@ -19,7 +14,11 @@ SECURITY_PACKAGES, SYSTEM_UPDATE_PACKAGES, ) -from mvt.common.virustotal import VTNoKey, VTQuotaExceeded, virustotal_lookup +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleResults, + ModuleSerializedResult, +) from .base import AndroidExtraction @@ -34,7 +33,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -46,7 +45,7 @@ def __init__( ) self._user_needed = False - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: records = [] timestamps = [ @@ -95,70 +94,71 @@ def check_indicators(self) -> None: if not self.indicators: continue - ioc = self.indicators.check_app_id(result.get("package_name")) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) - continue + ioc_match = self.indicators.check_app_id(result.get("package_name")) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) for package_file in result.get("files", []): - ioc = self.indicators.check_file_hash(package_file["sha256"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) - - @staticmethod - def check_virustotal(packages: list) -> None: - hashes = [] - for package in packages: - for file in package.get("files", []): - if file["sha256"] not in hashes: - hashes.append(file["sha256"]) - - total_hashes = len(hashes) - detections = {} - - progress_desc = f"Looking up {total_hashes} files..." - for i in track(range(total_hashes), description=progress_desc): - try: - results = virustotal_lookup(hashes[i]) - except VTNoKey: - return - except VTQuotaExceeded as exc: - print("Unable to continue: %s", exc) - break - - if not results: - continue - - positives = results["attributes"]["last_analysis_stats"]["malicious"] - total = len(results["attributes"]["last_analysis_results"]) - - detections[hashes[i]] = f"{positives}/{total}" - - table = Table(title="VirusTotal Packages Detections") - table.add_column("Package name") - table.add_column("File path") - table.add_column("Detections") - - for package in packages: - for file in package.get("files", []): - row = [package["package_name"], file["path"]] - - if file["sha256"] in detections: - detection = detections[file["sha256"]] - positives = detection.split("/")[0] - if int(positives) > 0: - row.append(Text(detection, "red bold")) - else: - row.append(detection) - else: - row.append("not found") - - table.add_row(*row) - - console = Console() - console.print(table) + ioc_match = self.indicators.check_file_hash(package_file["sha256"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", result + ) + + # @staticmethod + # def check_virustotal(packages: list) -> None: + # hashes = [] + # for package in packages: + # for file in package.get("files", []): + # if file["sha256"] not in hashes: + # hashes.append(file["sha256"]) + + # total_hashes = len(hashes) + # detections = {} + + # progress_desc = f"Looking up {total_hashes} files..." + # for i in track(range(total_hashes), description=progress_desc): + # try: + # results = virustotal_lookup(hashes[i]) + # except VTNoKey: + # return + # except VTQuotaExceeded as exc: + # print("Unable to continue: %s", exc) + # break + + # if not results: + # continue + + # positives = results["attributes"]["last_analysis_stats"]["malicious"] + # total = len(results["attributes"]["last_analysis_results"]) + + # detections[hashes[i]] = f"{positives}/{total}" + + # table = Table(title="VirusTotal Packages Detections") + # table.add_column("Package name") + # table.add_column("File path") + # table.add_column("Detections") + + # for package in packages: + # for file in package.get("files", []): + # row = [package["package_name"], file["path"]] + + # if file["sha256"] in detections: + # detection = detections[file["sha256"]] + # positives = detection.split("/")[0] + # if int(positives) > 0: + # row.append(Text(detection, "red bold")) + # else: + # row.append(detection) + # else: + # row.append("not found") + + # table.add_row(*row) + + # console = Console() + # console.print(table) @staticmethod def parse_package_for_details(output: str) -> dict: diff --git a/src/mvt/android/modules/adb/processes.py b/src/mvt/android/modules/adb/processes.py index 1a9f29f82..dcdb03640 100644 --- a/src/mvt/android/modules/adb/processes.py +++ b/src/mvt/android/modules/adb/processes.py @@ -9,6 +9,7 @@ from mvt.android.artifacts.processes import Processes as ProcessesArtifact from .base import AndroidExtraction +from mvt.common.module_types import ModuleResults class Processes(ProcessesArtifact, AndroidExtraction): @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/adb/root_binaries.py b/src/mvt/android/modules/adb/root_binaries.py index 6d8350c51..0315e230b 100644 --- a/src/mvt/android/modules/adb/root_binaries.py +++ b/src/mvt/android/modules/adb/root_binaries.py @@ -7,6 +7,7 @@ from typing import Optional from .base import AndroidExtraction +from mvt.common.module_types import ModuleResults class RootBinaries(AndroidExtraction): @@ -19,7 +20,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/adb/selinux_status.py b/src/mvt/android/modules/adb/selinux_status.py index a46e362fd..39925c756 100644 --- a/src/mvt/android/modules/adb/selinux_status.py +++ b/src/mvt/android/modules/adb/selinux_status.py @@ -7,6 +7,7 @@ from typing import Optional from .base import AndroidExtraction +from mvt.common.module_types import ModuleResults class SELinuxStatus(AndroidExtraction): @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/adb/settings.py b/src/mvt/android/modules/adb/settings.py index 416ef7f67..dcfc6e564 100644 --- a/src/mvt/android/modules/adb/settings.py +++ b/src/mvt/android/modules/adb/settings.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.android.artifacts.settings import Settings as SettingsArtifact +from mvt.common.module_types import ModuleResults from .base import AndroidExtraction @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/adb/sms.py b/src/mvt/android/modules/adb/sms.py index 673e56a5c..a69592cd4 100644 --- a/src/mvt/android/modules/adb/sms.py +++ b/src/mvt/android/modules/adb/sms.py @@ -6,11 +6,16 @@ import logging import os import sqlite3 -from typing import Optional, Union +from typing import Optional from mvt.android.parsers.backup import AndroidBackupParsingError, parse_tar_for_sms from mvt.common.module import InsufficientPrivileges from mvt.common.utils import check_for_links, convert_unix_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleResults, + ModuleSerializedResult, +) from .base import AndroidExtraction @@ -51,7 +56,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -64,7 +69,7 @@ def __init__( self.sms_db_type = 0 - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: body = record["body"].replace("\n", "\\n") return { "timestamp": record["isodate"], @@ -85,9 +90,12 @@ def check_indicators(self) -> None: if message_links == []: message_links = check_for_links(message["body"]) - if self.indicators.check_urls(message_links): - self.detected.append(message) - continue + ioc_match = self.indicators.check_urls(message_links) + if ioc_match: + message["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", message + ) def _parse_db(self, db_path: str) -> None: """Parse an Android bugle_db SMS database file. diff --git a/src/mvt/android/modules/adb/whatsapp.py b/src/mvt/android/modules/adb/whatsapp.py index 28ee1702c..40f8875ac 100644 --- a/src/mvt/android/modules/adb/whatsapp.py +++ b/src/mvt/android/modules/adb/whatsapp.py @@ -7,11 +7,16 @@ import logging import os import sqlite3 -from typing import Optional, Union +from typing import Optional from mvt.common.utils import check_for_links, convert_unix_to_iso from .base import AndroidExtraction +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleSerializedResult, + ModuleResults, +) WHATSAPP_PATH = "data/data/com.whatsapp/databases/msgstore.db" @@ -26,7 +31,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -37,7 +42,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: text = record["data"].replace("\n", "\\n") return { "timestamp": record["isodate"], diff --git a/src/mvt/android/modules/androidqf/aqf_files.py b/src/mvt/android/modules/androidqf/aqf_files.py index 90eb3b880..562b2d31e 100644 --- a/src/mvt/android/modules/androidqf/aqf_files.py +++ b/src/mvt/android/modules/androidqf/aqf_files.py @@ -10,10 +10,15 @@ try: import zoneinfo except ImportError: - from backports import zoneinfo -from typing import Optional, Union + from backports import zoneinfo # type: ignore +from typing import Optional from mvt.android.modules.androidqf.base import AndroidQFModule +from mvt.common.module_types import ( + ModuleResults, + ModuleAtomicResult, + ModuleSerializedResult, +) from mvt.common.utils import convert_datetime_to_iso SUSPICIOUS_PATHS = [ @@ -36,7 +41,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -47,7 +52,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: records = [] for ts in set( @@ -82,10 +87,11 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_file_path(result["path"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_file_path(result["path"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.log_latest() continue # NOTE: Update with final path used for Android collector. @@ -98,20 +104,17 @@ def check_indicators(self) -> None: if self.file_is_executable(result["mode"]): file_type = "executable " - self.log.warning( - 'Found %sfile at suspicious path "%s".', - file_type, - result["path"], - ) - self.detected.append(result) + msg = f'Found {file_type}file at suspicious path "{result["path"]}"' + self.alertstore.high(self.get_slug(), msg, "", result) + self.alertstore.log_latest() if result.get("sha256", "") == "": continue - ioc = self.indicators.check_file_hash(result["sha256"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_file_hash(result["sha256"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) # TODO: adds SHA1 and MD5 when available in MVT diff --git a/src/mvt/android/modules/androidqf/aqf_getprop.py b/src/mvt/android/modules/androidqf/aqf_getprop.py index 35514f836..68dca906d 100644 --- a/src/mvt/android/modules/androidqf/aqf_getprop.py +++ b/src/mvt/android/modules/androidqf/aqf_getprop.py @@ -9,6 +9,7 @@ from mvt.android.artifacts.getprop import GetProp as GetPropArtifact from .base import AndroidQFModule +from mvt.common.module_types import ModuleResults class AQFGetProp(GetPropArtifact, AndroidQFModule): @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/androidqf/aqf_log_timestamps.py b/src/mvt/android/modules/androidqf/aqf_log_timestamps.py index e5a141044..fc4680451 100644 --- a/src/mvt/android/modules/androidqf/aqf_log_timestamps.py +++ b/src/mvt/android/modules/androidqf/aqf_log_timestamps.py @@ -9,6 +9,7 @@ from typing import Optional from mvt.common.utils import convert_datetime_to_iso +from mvt.common.module_types import ModuleResults from .base import AndroidQFModule from mvt.android.artifacts.file_timestamps import FileTimestampsArtifact @@ -25,7 +26,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/androidqf/aqf_packages.py b/src/mvt/android/modules/androidqf/aqf_packages.py index 500b3d4ba..20b1399a6 100644 --- a/src/mvt/android/modules/androidqf/aqf_packages.py +++ b/src/mvt/android/modules/androidqf/aqf_packages.py @@ -17,6 +17,7 @@ ) from .base import AndroidQFModule +from mvt.common.module_types import ModuleResults class AQFPackages(AndroidQFModule): @@ -29,7 +30,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -43,79 +44,98 @@ def __init__( def check_indicators(self) -> None: for result in self.results: if result["name"] in ROOT_PACKAGES: - self.log.warning( - 'Found an installed package related to rooting/jailbreaking: "%s"', - result["name"], + self.alertstore.medium( + self.get_slug(), + f'Found an installed package related to rooting/jailbreaking: "{result["name"]}"', + "", + result, ) - self.detected.append(result) + self.alertstore.log_latest() continue - # Detections for apps installed via unusual methods + # Detections for apps installed via unusual methods. if result["installer"] in THIRD_PARTY_STORE_INSTALLERS: - self.log.warning( - 'Found a package installed via a third party store (installer="%s"): "%s"', - result["installer"], - result["name"], + self.alertstore.info( + self.get_slug(), + f'Found a package installed via a third party store (installer="{result["installer"]}"): "{result["name"]}"', + "", + result, ) + self.alertstore.log_latest() elif result["installer"] in BROWSER_INSTALLERS: - self.log.warning( - 'Found a package installed via a browser (installer="%s"): "%s"', - result["installer"], - result["name"], + self.alertstore.medium( + self.get_slug(), + f'Found a package installed via a browser (installer="{result["installer"]}"): "{result["name"]}"', + "", + result, ) - self.detected.append(result) + self.alertstore.log_latest() elif result["installer"] == "null" and result["system"] is False: - self.log.warning( - 'Found a non-system package installed via adb or another method: "%s"', - result["name"], + self.alertstore.high( + self.get_slug(), + f'Found a non-system package installed via adb or another method: "{result["name"]}"', + "", + result, ) - self.detected.append(result) + self.alertstore.log_latest() elif result["installer"] in PLAY_STORE_INSTALLERS: pass - # Check for disabled security or software update packages + # Check for disabled security or software update packages. package_disabled = result.get("disabled", None) if result["name"] in SECURITY_PACKAGES and package_disabled: - self.log.warning( - 'Security package "%s" disabled on the phone', result["name"] + self.alertstore.high( + self.get_slug(), + f'Security package "{result["name"]}" disabled on the phone', + "", + result, ) + self.alertstore.log_latest() if result["name"] in SYSTEM_UPDATE_PACKAGES and package_disabled: - self.log.warning( - 'System OTA update package "%s" disabled on the phone', - result["name"], + self.alertstore.high( + self.get_slug(), + f'System OTA update package "{result["name"]}" disabled on the phone', + "", + result, ) + self.alertstore.log_latest() if not self.indicators: continue - ioc = self.indicators.check_app_id(result.get("name")) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_app_id(result.get("name")) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.log_latest() for package_file in result.get("files", []): - ioc = self.indicators.check_file_hash(package_file["sha256"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_file_hash(package_file["sha256"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", result + ) + self.alertstore.log_latest() if "certificate" not in package_file: continue - # The keys generated by AndroidQF have a leading uppercase character + # The keys generated by AndroidQF have a leading uppercase character. for hash_type in ["Md5", "Sha1", "Sha256"]: certificate_hash = package_file["certificate"][hash_type] - ioc = self.indicators.check_app_certificate_hash(certificate_hash) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_app_certificate_hash( + certificate_hash + ) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", result + ) + self.alertstore.log_latest() break - # Deduplicate the detected packages - dedupe_detected_dict = {str(item): item for item in self.detected} - self.detected = list(dedupe_detected_dict.values()) - def run(self) -> None: packages = self._get_files_by_pattern("*/packages.json") if not packages: diff --git a/src/mvt/android/modules/androidqf/aqf_processes.py b/src/mvt/android/modules/androidqf/aqf_processes.py index 3faabb432..4c69ca02f 100644 --- a/src/mvt/android/modules/androidqf/aqf_processes.py +++ b/src/mvt/android/modules/androidqf/aqf_processes.py @@ -9,6 +9,7 @@ from mvt.android.artifacts.processes import Processes as ProcessesArtifact from .base import AndroidQFModule +from mvt.common.module_types import ModuleResults class AQFProcesses(ProcessesArtifact, AndroidQFModule): @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/androidqf/aqf_settings.py b/src/mvt/android/modules/androidqf/aqf_settings.py index 46a70fb23..1dc3b6eaf 100644 --- a/src/mvt/android/modules/androidqf/aqf_settings.py +++ b/src/mvt/android/modules/androidqf/aqf_settings.py @@ -9,6 +9,7 @@ from mvt.android.artifacts.settings import Settings as SettingsArtifact from .base import AndroidQFModule +from mvt.common.module_types import ModuleResults class AQFSettings(SettingsArtifact, AndroidQFModule): @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/androidqf/base.py b/src/mvt/android/modules/androidqf/base.py index 43e62108a..a784158f5 100644 --- a/src/mvt/android/modules/androidqf/base.py +++ b/src/mvt/android/modules/androidqf/base.py @@ -7,9 +7,10 @@ import logging import os import zipfile -from typing import Any, Dict, List, Optional, Union +from typing import List, Optional from mvt.common.module import MVTModule +from mvt.common.module_types import ModuleResults class AndroidQFModule(MVTModule): @@ -22,7 +23,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Union[List[Dict[str, Any]], Dict[str, Any], None] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/androidqf/sms.py b/src/mvt/android/modules/androidqf/sms.py index 893e5178b..bc1d36178 100644 --- a/src/mvt/android/modules/androidqf/sms.py +++ b/src/mvt/android/modules/androidqf/sms.py @@ -53,8 +53,12 @@ def check_indicators(self) -> None: if "body" not in message: continue - if self.indicators.check_domains(message.get("links", [])): - self.detected.append(message) + ioc_match = self.indicators.check_domains(message.get("links", [])) + if ioc_match: + message["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", message + ) def parse_backup(self, data): header = parse_ab_header(data) diff --git a/src/mvt/android/modules/backup/base.py b/src/mvt/android/modules/backup/base.py index 29238ba61..8da8a2001 100644 --- a/src/mvt/android/modules/backup/base.py +++ b/src/mvt/android/modules/backup/base.py @@ -9,10 +9,10 @@ from tarfile import TarFile from typing import List, Optional -from mvt.common.module import MVTModule +from mvt.common.module import MVTModule, ModuleResults -class BackupExtraction(MVTModule): +class BackupModule(MVTModule): """This class provides a base for all backup extractios modules""" def __init__( @@ -22,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/backup/sms.py b/src/mvt/android/modules/backup/sms.py index a194a1e2c..3fafcfc38 100644 --- a/src/mvt/android/modules/backup/sms.py +++ b/src/mvt/android/modules/backup/sms.py @@ -6,12 +6,13 @@ import logging from typing import Optional -from mvt.android.modules.backup.base import BackupExtraction +from mvt.android.modules.backup.base import BackupModule from mvt.android.parsers.backup import parse_sms_file from mvt.common.utils import check_for_links +from mvt.common.module_types import ModuleResults -class SMS(BackupExtraction): +class SMS(BackupModule): def __init__( self, file_path: Optional[str] = None, @@ -19,7 +20,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -43,8 +44,12 @@ def check_indicators(self) -> None: if message_links == []: message_links = check_for_links(message.get("text", "")) - if self.indicators.check_urls(message_links): - self.detected.append(message) + ioc_match = self.indicators.check_urls(message_links) + if ioc_match: + message["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", message + ) continue def run(self) -> None: diff --git a/src/mvt/android/modules/bugreport/base.py b/src/mvt/android/modules/bugreport/base.py index 158bc2882..025e2c162 100644 --- a/src/mvt/android/modules/bugreport/base.py +++ b/src/mvt/android/modules/bugreport/base.py @@ -10,7 +10,7 @@ from typing import List, Optional from zipfile import ZipFile -from mvt.common.module import MVTModule +from mvt.common.module import MVTModule, ModuleResults class BugReportModule(MVTModule): @@ -23,7 +23,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/dumpsys_accessibility.py b/src/mvt/android/modules/bugreport/dumpsys_accessibility.py index e141b2fe6..0c0f294a2 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_accessibility.py +++ b/src/mvt/android/modules/bugreport/dumpsys_accessibility.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArtifact +from mvt.common.module_types import ModuleResults from .base import BugReportModule @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/dumpsys_activities.py b/src/mvt/android/modules/bugreport/dumpsys_activities.py index a58c6f48c..bfceebfcf 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_activities.py +++ b/src/mvt/android/modules/bugreport/dumpsys_activities.py @@ -9,6 +9,7 @@ from mvt.android.artifacts.dumpsys_package_activities import ( DumpsysPackageActivitiesArtifact, ) +from mvt.common.module_types import ModuleResults from .base import BugReportModule @@ -23,7 +24,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/dumpsys_adb_state.py b/src/mvt/android/modules/bugreport/dumpsys_adb_state.py index ff74368a8..07d4694bc 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_adb_state.py +++ b/src/mvt/android/modules/bugreport/dumpsys_adb_state.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.android.artifacts.dumpsys_adb import DumpsysADBArtifact +from mvt.common.module_types import ModuleResults from .base import BugReportModule @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/dumpsys_appops.py b/src/mvt/android/modules/bugreport/dumpsys_appops.py index 96b479672..f3ab41c29 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_appops.py +++ b/src/mvt/android/modules/bugreport/dumpsys_appops.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.android.artifacts.dumpsys_appops import DumpsysAppopsArtifact +from mvt.common.module_types import ModuleResults from .base import BugReportModule @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/dumpsys_battery_daily.py b/src/mvt/android/modules/bugreport/dumpsys_battery_daily.py index 7fc832981..365d193fb 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_battery_daily.py +++ b/src/mvt/android/modules/bugreport/dumpsys_battery_daily.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtifact +from mvt.common.module_types import ModuleResults from .base import BugReportModule @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/dumpsys_battery_history.py b/src/mvt/android/modules/bugreport/dumpsys_battery_history.py index 729f801aa..2e0f468ac 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_battery_history.py +++ b/src/mvt/android/modules/bugreport/dumpsys_battery_history.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryArtifact +from mvt.common.module_types import ModuleResults from .base import BugReportModule @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/dumpsys_dbinfo.py b/src/mvt/android/modules/bugreport/dumpsys_dbinfo.py index 73902bb34..96b0bf352 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_dbinfo.py +++ b/src/mvt/android/modules/bugreport/dumpsys_dbinfo.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfoArtifact +from mvt.common.module_types import ModuleResults from .base import BugReportModule @@ -23,7 +24,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/dumpsys_getprop.py b/src/mvt/android/modules/bugreport/dumpsys_getprop.py index acec15ce0..2bb5cd662 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_getprop.py +++ b/src/mvt/android/modules/bugreport/dumpsys_getprop.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.android.artifacts.getprop import GetProp as GetPropArtifact +from mvt.common.module_types import ModuleResults from .base import BugReportModule @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/dumpsys_packages.py b/src/mvt/android/modules/bugreport/dumpsys_packages.py index fccf10210..0fb4713f1 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_packages.py +++ b/src/mvt/android/modules/bugreport/dumpsys_packages.py @@ -6,6 +6,7 @@ import logging from typing import Optional +from mvt.common.module_types import ModuleResults from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact from mvt.android.utils import DANGEROUS_PERMISSIONS, DANGEROUS_PERMISSIONS_THRESHOLD @@ -22,7 +23,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/dumpsys_platform_compat.py b/src/mvt/android/modules/bugreport/dumpsys_platform_compat.py index e9d10e66d..29e58f3ad 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_platform_compat.py +++ b/src/mvt/android/modules/bugreport/dumpsys_platform_compat.py @@ -9,6 +9,7 @@ from mvt.android.artifacts.dumpsys_platform_compat import DumpsysPlatformCompatArtifact from mvt.android.modules.bugreport.base import BugReportModule +from mvt.common.module_types import ModuleResults class DumpsysPlatformCompat(DumpsysPlatformCompatArtifact, BugReportModule): @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/dumpsys_receivers.py b/src/mvt/android/modules/bugreport/dumpsys_receivers.py index 591af2f7c..000d98ca5 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_receivers.py +++ b/src/mvt/android/modules/bugreport/dumpsys_receivers.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact +from mvt.common.module_types import ModuleResults from .base import BugReportModule @@ -21,7 +22,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/fs_timestamps.py b/src/mvt/android/modules/bugreport/fs_timestamps.py index 14e1cd1e3..000d07617 100644 --- a/src/mvt/android/modules/bugreport/fs_timestamps.py +++ b/src/mvt/android/modules/bugreport/fs_timestamps.py @@ -8,6 +8,7 @@ from mvt.common.utils import convert_datetime_to_iso from .base import BugReportModule +from mvt.common.module_types import ModuleResults from mvt.android.artifacts.file_timestamps import FileTimestampsArtifact @@ -23,7 +24,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/modules/bugreport/tombstones.py b/src/mvt/android/modules/bugreport/tombstones.py index 6447e6199..58ef254fd 100644 --- a/src/mvt/android/modules/bugreport/tombstones.py +++ b/src/mvt/android/modules/bugreport/tombstones.py @@ -7,6 +7,7 @@ from typing import Optional from mvt.android.artifacts.tombstone_crashes import TombstoneCrashArtifact +from mvt.common.module_types import ModuleResults from .base import BugReportModule @@ -22,7 +23,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/android/utils.py b/src/mvt/android/utils.py index 2455959de..689c048ab 100644 --- a/src/mvt/android/utils.py +++ b/src/mvt/android/utils.py @@ -6,16 +6,16 @@ from typing import List -def warn_android_patch_level(patch_level: str, log) -> bool: +def warn_android_patch_level(patch_level: str, log) -> str: """Alert if Android patch level out-of-date""" patch_date = datetime.strptime(patch_level, "%Y-%m-%d") if (datetime.now() - patch_date) > timedelta(days=6 * 31): - log.warning( - "This phone has not received security updates " - "for more than six months (last update: %s)", + warning_message = ( + f"This phone has not received security updates " + f"for more than six months (last update: {patch_level}).", patch_level, ) - return True + return warning_message return False diff --git a/src/mvt/common/artifact.py b/src/mvt/common/artifact.py index 7cc0682ab..af0ba98c4 100644 --- a/src/mvt/common/artifact.py +++ b/src/mvt/common/artifact.py @@ -2,27 +2,11 @@ # 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 .module import MVTModule -class Artifact: - """ - Main artifact class - """ - - def __init__(self, *args, **kwargs): - self.results = [] - self.detected = [] - self.indicators = None - super().__init__(*args, **kwargs) +class Artifact(MVTModule): + """Base class for artifacts. - def parse(self, entry: str): - """ - Parse the artifact, adds the parsed information to self.results - """ - raise NotImplementedError - - def check_indicators(self) -> None: - """Check the results of this module against a provided list of - indicators coming from self.indicators - """ - raise NotImplementedError + XXX: Inheriting from MVTModule to have the same signature as other modules. Not sure if this is a good idea. + """ diff --git a/src/mvt/common/cmd_check_iocs.py b/src/mvt/common/cmd_check_iocs.py index 1f8bde5d1..11e5658d5 100644 --- a/src/mvt/common/cmd_check_iocs.py +++ b/src/mvt/common/cmd_check_iocs.py @@ -78,7 +78,7 @@ def run(self) -> None: except NotImplementedError: continue else: - total_detections += len(m.detected) + total_detections += len(m.alertstore.alerts) if total_detections > 0: log.warning( diff --git a/src/mvt/common/command.py b/src/mvt/common/command.py index 920f3a053..00bf11450 100644 --- a/src/mvt/common/command.py +++ b/src/mvt/common/command.py @@ -58,11 +58,9 @@ def __init__( # This list will contain all executed modules. # We can use this to reference e.g. self.executed[0].results. self.executed = [] - self.detected_count = 0 self.hashes = hashes self.hash_values = [] self.timeline = [] - self.timeline_detected = [] # Load IOCs self._create_storage() diff --git a/src/mvt/common/indicators.py b/src/mvt/common/indicators.py index adb448373..877429eab 100644 --- a/src/mvt/common/indicators.py +++ b/src/mvt/common/indicators.py @@ -590,9 +590,9 @@ def check_file_path(self, file_path: str) -> Optional[IndicatorMatch]: if not file_path: return None - ioc = self.check_file_name(os.path.basename(file_path)) - if ioc: - return ioc + ioc_match = self.check_file_name(os.path.basename(file_path)) + if ioc_match: + return ioc_match for ioc in self.get_iocs("file_paths"): # Strip any trailing slash from indicator paths to match diff --git a/src/mvt/common/module.py b/src/mvt/common/module.py index e57a9fc75..775717dbb 100644 --- a/src/mvt/common/module.py +++ b/src/mvt/common/module.py @@ -76,7 +76,6 @@ def __init__( self.alertstore: AlertStore = AlertStore(log=log) self.results: ModuleResults = results if results else [] - self.detected: ModuleResults = [] self.timeline: ModuleTimeline = [] self.timeline_detected: ModuleTimeline = [] @@ -126,11 +125,13 @@ def save_to_json(self) -> None: exc, ) - if self.detected: + if self.alertstore.alerts: detected_file_name = f"{name}_detected.json" detected_json_path = os.path.join(self.results_path, detected_file_name) with open(detected_json_path, "w", encoding="utf-8") as handle: - json.dump(self.detected, handle, indent=4, cls=CustomJSONEncoder) + json.dump( + self.alertstore.alerts, handle, indent=4, cls=CustomJSONEncoder + ) def serialize(self, result: ModuleAtomicResult) -> ModuleSerializedResult: raise NotImplementedError @@ -165,17 +166,17 @@ def to_timeline(self) -> None: else: self.timeline.append(record) - for detected in self.detected: - record = self.serialize(detected) - if record: - if isinstance(record, list): - self.timeline_detected.extend(record) - else: - self.timeline_detected.append(record) + # for detected in self.alertstore.alerts: + # record = self.serialize(detected) + # if record: + # if isinstance(record, list): + # self.timeline_detected.extend(record) + # else: + # self.timeline_detected.append(record) # De-duplicate timeline entries. self.timeline = self._deduplicate_timeline(self.timeline) - self.timeline_detected = self._deduplicate_timeline(self.timeline_detected) + # self.timeline_detected = self._deduplicate_timeline(self.timeline_detected) def run(self) -> None: """Run the main module procedure.""" @@ -230,7 +231,7 @@ def run_module(module: MVTModule) -> None: ) else: - if module.indicators and not module.detected: + if module.indicators and not module.alertstore.alerts: module.log.info( "The %s module produced no detections!", module.__class__.__name__ ) diff --git a/src/mvt/ios/modules/backup/backup_info.py b/src/mvt/ios/modules/backup/backup_info.py index c8f55f605..07baa2f6a 100644 --- a/src/mvt/ios/modules/backup/backup_info.py +++ b/src/mvt/ios/modules/backup/backup_info.py @@ -9,6 +9,7 @@ from typing import Optional from mvt.common.module import DatabaseNotFoundError +from mvt.common.module_types import ModuleResults from mvt.ios.versions import get_device_desc_from_id, is_ios_version_outdated from ..base import IOSExtraction @@ -24,7 +25,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/ios/modules/backup/configuration_profiles.py b/src/mvt/ios/modules/backup/configuration_profiles.py index 3866971f1..306ea4f0a 100644 --- a/src/mvt/ios/modules/backup/configuration_profiles.py +++ b/src/mvt/ios/modules/backup/configuration_profiles.py @@ -7,9 +7,14 @@ import os import plistlib from base64 import b64encode -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_datetime_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleSerializedResult, + ModuleResults, +) from ..base import IOSExtraction @@ -28,7 +33,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -39,7 +44,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: if not record["install_date"]: return {} @@ -63,28 +68,28 @@ def check_indicators(self) -> None: # Alert on any known malicious configuration profiles in the # indicator list. - ioc = self.indicators.check_profile(result["plist"]["PayloadUUID"]) - if ioc: - self.log.warning( - "Found a known malicious configuration " - 'profile "%s" with UUID %s', - result["plist"]["PayloadDisplayName"], - result["plist"]["PayloadUUID"], + ioc_match = self.indicators.check_profile( + result["plist"]["PayloadUUID"] + ) + if ioc_match: + warning_message = ( + f'Found a known malicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with UUID "{result["plist"]["PayloadUUID"]}"', + ) + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), warning_message, "", result ) - result["matched_indicator"] = ioc - self.detected.append(result) + self.alertstore.log_latest() continue # Highlight suspicious configuration profiles which may be used # to hide notifications. if payload_content["PayloadType"] in ["com.apple.notificationsettings"]: - self.log.warning( - "Found a potentially suspicious configuration profile " - '"%s" with payload type %s', - result["plist"]["PayloadDisplayName"], - payload_content["PayloadType"], + warning_message = ( + f'Found a potentially suspicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with payload type {payload_content["PayloadType"]}', ) - self.detected.append(result) + self.alertstore.medum(self.get_slug(), warning_message, "", result) + self.alertstore.log_latest() continue def run(self) -> None: diff --git a/src/mvt/ios/modules/backup/manifest.py b/src/mvt/ios/modules/backup/manifest.py index ccbc459c9..099a682ad 100644 --- a/src/mvt/ios/modules/backup/manifest.py +++ b/src/mvt/ios/modules/backup/manifest.py @@ -13,6 +13,11 @@ from mvt.common.module import DatabaseNotFoundError from mvt.common.url import URL from mvt.common.utils import convert_datetime_to_iso, convert_unix_to_iso +from mvt.common.module_types import ( + ModuleResults, + ModuleAtomicResult, + ModuleSerializedResult, +) from ..base import IOSExtraction @@ -27,7 +32,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -60,7 +65,7 @@ def _convert_timestamp(timestamp_or_unix_time_int): return convert_unix_to_iso(timestamp_or_unix_time_int) - def serialize(self, record: dict) -> []: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: records = [] if "modified" not in record or "status_changed" not in record: return records @@ -95,8 +100,10 @@ def check_indicators(self) -> None: if not self.indicators: continue - if self.indicators.check_file_path("/" + result["relative_path"]): - self.detected.append(result) + ioc_match = self.indicators.check_file_path("/" + result["relative_path"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.high(self.get_slug(), ioc_match.message, "", result) continue rel_path = result["relative_path"].lower() @@ -107,15 +114,15 @@ def check_indicators(self) -> None: except Exception: continue - ioc = self.indicators.check_url(part) - if ioc: - self.log.warning( - 'Found mention of domain "%s" in a backup file with path: %s', - ioc["value"], - rel_path, + ioc_match = self.indicators.check_url(part) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.high( + self.get_slug(), + f'Found mention of domain "{ioc_match.ioc.value}" in a backup file with path: {rel_path}', + "", + result, ) - result["matched_indicator"] = ioc - self.detected.append(result) def run(self) -> None: manifest_db_path = os.path.join(self.target_path, "Manifest.db") diff --git a/src/mvt/ios/modules/backup/profile_events.py b/src/mvt/ios/modules/backup/profile_events.py index eeb91e28c..0648b9e43 100644 --- a/src/mvt/ios/modules/backup/profile_events.py +++ b/src/mvt/ios/modules/backup/profile_events.py @@ -5,9 +5,14 @@ import logging import plistlib -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_datetime_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleResults, + ModuleSerializedResult, +) from ..base import IOSExtraction @@ -29,7 +34,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -40,7 +45,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record.get("timestamp"), "module": self.__class__.__name__, @@ -51,20 +56,27 @@ def serialize(self, record: dict) -> Union[dict, list]: } def check_indicators(self) -> None: + for result in self.results: + message = f'On {result.get("timestamp")} process "{result.get("timestamp")}" started operation "{result.get("operation")}" of profile "{result.get("profile_id")}"' + self.alertstore.low( + self.get_slug(), message, result.get("timestamp"), result + ) + self.alertstore.log_latest() + if not self.indicators: return for result in self.results: - ioc = self.indicators.check_process(result.get("process")) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_process(result.get("process")) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) continue - ioc = self.indicators.check_profile(result.get("profile_id")) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_profile(result.get("profile_id")) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) @staticmethod def parse_profile_events(file_data: bytes) -> list: @@ -109,13 +121,4 @@ def run(self) -> None: with open(events_file_path, "rb") as handle: self.results.extend(self.parse_profile_events(handle.read())) - for result in self.results: - self.log.info( - 'On %s process "%s" started operation "%s" of profile "%s"', - result.get("timestamp"), - result.get("process"), - result.get("operation"), - result.get("profile_id"), - ) - self.log.info("Extracted %d profile events", len(self.results)) diff --git a/src/mvt/ios/modules/base.py b/src/mvt/ios/modules/base.py index f96d99aa1..1a4861bf0 100644 --- a/src/mvt/ios/modules/base.py +++ b/src/mvt/ios/modules/base.py @@ -11,7 +11,8 @@ import subprocess from typing import Iterator, Optional, Union -from mvt.common.module import DatabaseCorruptedError, DatabaseNotFoundError, MVTModule +from mvt.common.module import DatabaseCorruptedError, DatabaseNotFoundError +from mvt.common.module import MVTModule, ModuleResults class IOSExtraction(MVTModule): @@ -25,7 +26,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/ios/modules/fs/analytics.py b/src/mvt/ios/modules/fs/analytics.py index fecab5762..b5cba0d5a 100644 --- a/src/mvt/ios/modules/fs/analytics.py +++ b/src/mvt/ios/modules/fs/analytics.py @@ -7,9 +7,14 @@ import logging import plistlib import sqlite3 -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_mactime_to_iso +from mvt.common.module_types import ( + ModuleResults, + ModuleSerializedResult, + ModuleAtomicResult, +) from ..base import IOSExtraction @@ -29,7 +34,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -40,7 +45,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, @@ -57,24 +62,26 @@ def check_indicators(self) -> None: if not isinstance(value, str): continue - ioc = self.indicators.check_process(value) - if ioc: - self.log.warning( - 'Found mention of a malicious process "%s" in %s file at %s', - value, - result["artifact"], - result["isodate"], + ioc_match = self.indicators.check_process(value) + if ioc_match: + warning_message = ( + f'Found mention of a malicious process "{value}" in {result["artifact"]} file at {result["isodate"]}', ) new_result = copy.copy(result) - new_result["matched_indicator"] = ioc - self.detected.append(new_result) + new_result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), warning_message, "", new_result + ) + self.alertstore.log_latest() continue - ioc = self.indicators.check_url(value) - if ioc: + ioc_match = self.indicators.check_url(value) + if ioc_match: new_result = copy.copy(result) - new_result["matched_indicator"] = ioc - self.detected.append(new_result) + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", new_result + ) def _extract_analytics_data(self): artifact = self.file_path.split("/")[-1] diff --git a/src/mvt/ios/modules/fs/analytics_ios_versions.py b/src/mvt/ios/modules/fs/analytics_ios_versions.py index 16ac9fc85..5fb300ed7 100644 --- a/src/mvt/ios/modules/fs/analytics_ios_versions.py +++ b/src/mvt/ios/modules/fs/analytics_ios_versions.py @@ -5,9 +5,14 @@ import logging from datetime import datetime -from typing import Optional, Union +from typing import Optional from mvt.ios.versions import find_version_by_build +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleSerializedResult, + ModuleResults, +) from ..base import IOSExtraction from .analytics import Analytics @@ -25,7 +30,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -36,7 +41,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, diff --git a/src/mvt/ios/modules/fs/cache_files.py b/src/mvt/ios/modules/fs/cache_files.py index 120ed1d04..fa34b08f6 100644 --- a/src/mvt/ios/modules/fs/cache_files.py +++ b/src/mvt/ios/modules/fs/cache_files.py @@ -6,8 +6,13 @@ import logging import os import sqlite3 -from typing import Optional, Union +from typing import Optional +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleSerializedResult, + ModuleResults, +) from ..base import IOSExtraction @@ -19,7 +24,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -30,7 +35,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: records = [] for item in self.results[record]: records.append( @@ -48,18 +53,19 @@ def check_indicators(self) -> None: if not self.indicators: return - self.detected = {} + self.alertstore.alerts = {} for key, values in self.results.items(): for value in values: - ioc = self.indicators.check_url(value["url"]) - if ioc: - value["matched_indicator"] = ioc - if key not in self.detected: - self.detected[key] = [ + ioc_match = self.indicators.check_url(value["url"]) + if ioc_match: + value["matched_indicator"] = ioc_match.ioc + # XXX: Finish converting this method + if key not in self.alertstore.alerts: + self.alertstore.alerts[key] = [ value, ] else: - self.detected[key].append(value) + self.alertstore.alerts[key].append(value) def _process_cache_file(self, file_path): self.log.info("Processing cache file at path: %s", file_path) diff --git a/src/mvt/ios/modules/fs/filesystem.py b/src/mvt/ios/modules/fs/filesystem.py index 87c5a0b02..980857a3b 100644 --- a/src/mvt/ios/modules/fs/filesystem.py +++ b/src/mvt/ios/modules/fs/filesystem.py @@ -5,9 +5,14 @@ import logging import os -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_unix_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleSerializedResult, + ModuleResults, +) from ..base import IOSExtraction @@ -24,7 +29,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -35,7 +40,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["modified"], "module": self.__class__.__name__, @@ -51,19 +56,19 @@ def check_indicators(self) -> None: if "path" not in result: continue - ioc = self.indicators.check_file_path(result["path"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_file_path(result["path"]) + if ioc_match: + self.alertstore.high(self.get_slug(), ioc_match.message, "", result) + self.alertstore.log_latest() # If we are instructed to run fast, we skip the rest. if self.module_options.get("fast_mode", None): continue - ioc = self.indicators.check_file_path_process(result["path"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_file_path_process(result["path"]) + if ioc_match: + self.alertstore.high(self.get_slug(), ioc_match.message, "", result) + self.alertstore.log_latest() def run(self) -> None: for root, dirs, files in os.walk(self.target_path): diff --git a/src/mvt/ios/modules/fs/net_netusage.py b/src/mvt/ios/modules/fs/net_netusage.py index ac36a791d..23b97f3fb 100644 --- a/src/mvt/ios/modules/fs/net_netusage.py +++ b/src/mvt/ios/modules/fs/net_netusage.py @@ -7,6 +7,7 @@ import sqlite3 from typing import Optional +from mvt.common.module_types import ModuleResults from ..net_base import NetBase NETUSAGE_ROOT_PATHS = [ @@ -29,7 +30,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/ios/modules/fs/safari_favicon.py b/src/mvt/ios/modules/fs/safari_favicon.py index 72bcc9bd8..c7579fb13 100644 --- a/src/mvt/ios/modules/fs/safari_favicon.py +++ b/src/mvt/ios/modules/fs/safari_favicon.py @@ -4,9 +4,14 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_mactime_to_iso +from mvt.common.module_types import ( + ModuleResults, + ModuleSerializedResult, + ModuleAtomicResult, +) from ..base import IOSExtraction @@ -26,7 +31,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -37,7 +42,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, @@ -51,13 +56,13 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_url(result["url"]) - if not ioc: - ioc = self.indicators.check_url(result["icon_url"]) + ioc_match = self.indicators.check_url(result["url"]) + if not ioc_match: + ioc_match = self.indicators.check_url(result["icon_url"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + if ioc_match: + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.log_latest() def _process_favicon_db(self, file_path): conn = self._open_sqlite_db(file_path) diff --git a/src/mvt/ios/modules/fs/shutdownlog.py b/src/mvt/ios/modules/fs/shutdownlog.py index 3d2be78d2..ad8fcd967 100644 --- a/src/mvt/ios/modules/fs/shutdownlog.py +++ b/src/mvt/ios/modules/fs/shutdownlog.py @@ -4,9 +4,14 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_mactime_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleResults, + ModuleSerializedResult, +) from ..base import IOSExtraction @@ -25,7 +30,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -36,7 +41,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, @@ -50,22 +55,23 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_file_path(result["client"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_file_path(result["client"]) + if ioc_match: + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.log_latest() continue for ioc in self.indicators.get_iocs("processes"): parts = result["client"].split("/") - if ioc in parts: - self.log.warning( - 'Found mention of a known malicious process "%s" in ' - "shutdown.log", - ioc, - ) + if ioc.value in parts: result["matched_indicator"] = ioc - self.detected.append(result) + self.alertstore.critical( + self.get_slug(), + f'Found mention of a known malicious process "{ioc.value}" in shutdown.log', + "", + result, + ) + self.alertstore.log_latest() continue def process_shutdownlog(self, content): diff --git a/src/mvt/ios/modules/fs/version_history.py b/src/mvt/ios/modules/fs/version_history.py index 44b9b134e..8c0af0f46 100644 --- a/src/mvt/ios/modules/fs/version_history.py +++ b/src/mvt/ios/modules/fs/version_history.py @@ -6,9 +6,14 @@ import datetime import json import logging -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_datetime_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleResults, + ModuleSerializedResult, +) from ..base import IOSExtraction @@ -27,7 +32,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -38,7 +43,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, diff --git a/src/mvt/ios/modules/fs/webkit_base.py b/src/mvt/ios/modules/fs/webkit_base.py index 7e4b2451c..00d4c192d 100644 --- a/src/mvt/ios/modules/fs/webkit_base.py +++ b/src/mvt/ios/modules/fs/webkit_base.py @@ -18,10 +18,11 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_url(result["url"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_url(result["url"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + continue def _process_webkit_folder(self, root_paths): for found_path in self._get_fs_files_from_patterns(root_paths): diff --git a/src/mvt/ios/modules/fs/webkit_indexeddb.py b/src/mvt/ios/modules/fs/webkit_indexeddb.py index aba91c3de..58cea427d 100644 --- a/src/mvt/ios/modules/fs/webkit_indexeddb.py +++ b/src/mvt/ios/modules/fs/webkit_indexeddb.py @@ -4,8 +4,13 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleResults, + ModuleSerializedResult, +) from .webkit_base import WebkitBase WEBKIT_INDEXEDDB_ROOT_PATHS = [ @@ -29,7 +34,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -40,7 +45,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, diff --git a/src/mvt/ios/modules/fs/webkit_localstorage.py b/src/mvt/ios/modules/fs/webkit_localstorage.py index dfb117f72..2b94fd1f4 100644 --- a/src/mvt/ios/modules/fs/webkit_localstorage.py +++ b/src/mvt/ios/modules/fs/webkit_localstorage.py @@ -4,8 +4,13 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional +from mvt.common.module_types import ( + ModuleResults, + ModuleSerializedResult, + ModuleAtomicResult, +) from .webkit_base import WebkitBase WEBKIT_LOCALSTORAGE_ROOT_PATHS = [ @@ -27,7 +32,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -38,7 +43,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, diff --git a/src/mvt/ios/modules/fs/webkit_safariviewservice.py b/src/mvt/ios/modules/fs/webkit_safariviewservice.py index 9e18c93df..caa7eef14 100644 --- a/src/mvt/ios/modules/fs/webkit_safariviewservice.py +++ b/src/mvt/ios/modules/fs/webkit_safariviewservice.py @@ -6,6 +6,7 @@ import logging from typing import Optional +from mvt.common.module_types import ModuleResults from .webkit_base import WebkitBase WEBKIT_SAFARIVIEWSERVICE_ROOT_PATHS = [ @@ -27,7 +28,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/ios/modules/mixed/applications.py b/src/mvt/ios/modules/mixed/applications.py index 8c151301c..558430a21 100644 --- a/src/mvt/ios/modules/mixed/applications.py +++ b/src/mvt/ios/modules/mixed/applications.py @@ -8,11 +8,13 @@ import os import plistlib from datetime import datetime, timezone -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional from mvt.common.module import DatabaseNotFoundError from mvt.common.utils import convert_datetime_to_iso from mvt.ios.modules.base import IOSExtraction +from mvt.common.module import ModuleResults, ModuleAtomicResult, ModuleSerializedResult + APPLICATIONS_DB_PATH = [ "private/var/containers/Bundle/Application/*/iTunesMetadata.plist" @@ -35,7 +37,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -46,7 +48,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: if "isodate" in record: return { "timestamp": record["isodate"], @@ -60,41 +62,51 @@ def check_indicators(self) -> None: for result in self.results: if self.indicators: if "softwareVersionBundleId" not in result: - self.log.warning( - "Suspicious application identified without softwareVersionBundleId" + self.alertstore.high( + self.get_slug(), + "Suspicious application identified without softwareVersionBundleId", + "", + result, ) - self.detected.append(result) continue - ioc = self.indicators.check_process(result["softwareVersionBundleId"]) - if ioc: - self.log.warning( - "Malicious application %s identified", - result["softwareVersionBundleId"], + ioc_match = self.indicators.check_process( + result["softwareVersionBundleId"] + ) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), + f"Malicious application {result['softwareVersionBundleId']} identified", + "", + result, ) - result["matched_indicator"] = ioc - self.detected.append(result) continue - ioc = self.indicators.check_app_id(result["softwareVersionBundleId"]) - if ioc: - self.log.warning( - "Malicious application %s identified", - result["softwareVersionBundleId"], + ioc_match = self.indicators.check_app_id( + result["softwareVersionBundleId"] + ) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), + f"Malicious application {result['softwareVersionBundleId']} identified", + "", + result, ) - result["matched_indicator"] = ioc - self.detected.append(result) continue + # Some apps installed from apple store with sourceApp "com.apple.AppStore.ProductPageExtension" if ( result.get("sourceApp", "com.apple.AppStore") not in KNOWN_APP_INSTALLERS ): - self.log.warning( - "Suspicious app not installed from the App Store or MDM: %s", - result["softwareVersionBundleId"], + self.alertstore.medium( + self.get_slug(), + f"Suspicious app not installed from the App Store or MDM: {result['softwareVersionBundleId']}", + "", + result, ) - self.detected.append(result) def _parse_itunes_timestamp(self, entry: Dict[str, Any]) -> None: """ diff --git a/src/mvt/ios/modules/mixed/calendar.py b/src/mvt/ios/modules/mixed/calendar.py index bfd1fc79a..6c111862a 100644 --- a/src/mvt/ios/modules/mixed/calendar.py +++ b/src/mvt/ios/modules/mixed/calendar.py @@ -4,9 +4,14 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_mactime_to_iso +from mvt.common.module_types import ( + ModuleResults, + ModuleSerializedResult, + ModuleAtomicResult, +) from ..base import IOSExtraction @@ -26,7 +31,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -44,7 +49,7 @@ def __init__( "participant_last_modified", ] - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: records = [] for timestamp in self.timestamps: if timestamp not in record or not record[timestamp]: @@ -64,18 +69,23 @@ def serialize(self, record: dict) -> Union[dict, list]: def check_indicators(self) -> None: for result in self.results: if result["participant_email"] and self.indicators: - ioc = self.indicators.check_email(result["participant_email"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_email(result["participant_email"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", result + ) continue # Custom check for Quadream exploit if result["summary"] == "Meeting" and result["description"] == "Notes": - self.log.warning( - "Potential Quadream exploit event identified: %s", result["uuid"] + self.alertstore.high( + self.get_slug(), + f"Potential Quadream exploit event identified: {result['uuid']}", + "", + result, ) - self.detected.append(result) + self.alertstore.log_latest() def _parse_calendar_db(self): """ diff --git a/src/mvt/ios/modules/mixed/calls.py b/src/mvt/ios/modules/mixed/calls.py index e29be3552..82debbae9 100644 --- a/src/mvt/ios/modules/mixed/calls.py +++ b/src/mvt/ios/modules/mixed/calls.py @@ -4,9 +4,10 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_mactime_to_iso +from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult from ..base import IOSExtraction @@ -37,7 +38,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, diff --git a/src/mvt/ios/modules/mixed/chrome_favicon.py b/src/mvt/ios/modules/mixed/chrome_favicon.py index f50ee2929..00d0b6ceb 100644 --- a/src/mvt/ios/modules/mixed/chrome_favicon.py +++ b/src/mvt/ios/modules/mixed/chrome_favicon.py @@ -4,9 +4,14 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso +from mvt.common.module_types import ( + ModuleResults, + ModuleSerializedResult, + ModuleAtomicResult, +) from ..base import IOSExtraction @@ -27,7 +32,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -38,7 +43,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, @@ -51,12 +56,13 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_url(result["url"]) - if not ioc: - ioc = self.indicators.check_url(result["icon_url"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_url(result["url"]) + if not ioc_match: + ioc_match = self.indicators.check_url(result["icon_url"]) + + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) continue def run(self) -> None: diff --git a/src/mvt/ios/modules/mixed/chrome_history.py b/src/mvt/ios/modules/mixed/chrome_history.py index e59ea9fc6..012b703c2 100644 --- a/src/mvt/ios/modules/mixed/chrome_history.py +++ b/src/mvt/ios/modules/mixed/chrome_history.py @@ -4,9 +4,14 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso +from mvt.common.module_types import ( + ModuleResults, + ModuleAtomicResult, + ModuleSerializedResult, +) from ..base import IOSExtraction @@ -29,7 +34,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -40,7 +45,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, @@ -55,10 +60,10 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_url(result["url"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_url(result["url"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) def run(self) -> None: self._find_ios_database( diff --git a/src/mvt/ios/modules/mixed/contacts.py b/src/mvt/ios/modules/mixed/contacts.py index 5f842c646..2bd5bee29 100644 --- a/src/mvt/ios/modules/mixed/contacts.py +++ b/src/mvt/ios/modules/mixed/contacts.py @@ -7,6 +7,7 @@ import sqlite3 from typing import Optional +from mvt.common.module_types import ModuleResults from ..base import IOSExtraction CONTACTS_BACKUP_IDS = [ @@ -27,7 +28,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/ios/modules/mixed/firefox_favicon.py b/src/mvt/ios/modules/mixed/firefox_favicon.py index 8c88e4a3f..048911484 100644 --- a/src/mvt/ios/modules/mixed/firefox_favicon.py +++ b/src/mvt/ios/modules/mixed/firefox_favicon.py @@ -4,9 +4,14 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_unix_to_iso +from mvt.common.module_types import ( + ModuleResults, + ModuleSerializedResult, + ModuleAtomicResult, +) from ..base import IOSExtraction @@ -28,7 +33,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -39,7 +44,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, @@ -53,13 +58,13 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_url(result.get("url", "")) - if not ioc: - ioc = self.indicators.check_url(result.get("history_url", "")) + ioc_match = self.indicators.check_url(result.get("url", "")) + if not ioc_match: + ioc_match = self.indicators.check_url(result.get("history_url", "")) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) def run(self) -> None: self._find_ios_database( diff --git a/src/mvt/ios/modules/mixed/firefox_history.py b/src/mvt/ios/modules/mixed/firefox_history.py index 69bc03480..67adda5d4 100644 --- a/src/mvt/ios/modules/mixed/firefox_history.py +++ b/src/mvt/ios/modules/mixed/firefox_history.py @@ -4,9 +4,14 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_unix_to_iso +from mvt.common.module_types import ( + ModuleResults, + ModuleSerializedResult, + ModuleAtomicResult, +) from ..base import IOSExtraction @@ -32,7 +37,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -43,7 +48,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, @@ -56,10 +61,10 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_url(result["url"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_url(result["url"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) def run(self) -> None: self._find_ios_database( diff --git a/src/mvt/ios/modules/mixed/global_preferences.py b/src/mvt/ios/modules/mixed/global_preferences.py index 7b159b167..a1aa9c2a5 100644 --- a/src/mvt/ios/modules/mixed/global_preferences.py +++ b/src/mvt/ios/modules/mixed/global_preferences.py @@ -7,6 +7,7 @@ import plistlib from typing import Optional +from mvt.common.module_types import ModuleResults from ..base import IOSExtraction GLOBAL_PREFERENCES_BACKUP_IDS = ["0dc926a1810f7aee4e8f38793ed788701f93bf9d"] @@ -25,7 +26,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -40,9 +41,15 @@ def check_indicators(self) -> None: for entry in self.results: if entry["entry"] == "LDMGlobalEnabled": if entry["value"]: - self.log.warning("Lockdown mode enabled") + self.alertstore.info( + self.get_slug(), "Lockdown mode enabled", "", None + ) else: - self.log.warning("Lockdown mode disabled") + self.alertstore.low( + self.get_slug(), "Lockdown mode disabled", "", None + ) + self.alertstore.log_latest() + continue def process_file(self, file_path: str) -> None: with open(file_path, "rb") as handle: diff --git a/src/mvt/ios/modules/mixed/idstatuscache.py b/src/mvt/ios/modules/mixed/idstatuscache.py index e8f41570a..1d836fc48 100644 --- a/src/mvt/ios/modules/mixed/idstatuscache.py +++ b/src/mvt/ios/modules/mixed/idstatuscache.py @@ -6,9 +6,14 @@ import collections import logging import plistlib -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_mactime_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleResults, + ModuleSerializedResult, +) from ..base import IOSExtraction @@ -31,7 +36,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -42,7 +47,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, @@ -58,18 +63,21 @@ def check_indicators(self) -> None: for result in self.results: if result.get("user", "").startswith("mailto:"): email = result["user"][7:].strip("'") - ioc = self.indicators.check_email(email) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_email(email) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", result + ) continue if "\\x00\\x00" in result.get("user", ""): - self.log.warning( - "Found an ID Status Cache entry with suspicious patterns: %s", - result.get("user"), + self.alertstore.high( + self.get_slug(), + f"Found an ID Status Cache entry with suspicious patterns: {result.get('user')}", + "", + result, ) - self.detected.append(result) def _extract_idstatuscache_entries(self, file_path): with open(file_path, "rb") as handle: diff --git a/src/mvt/ios/modules/mixed/interactionc.py b/src/mvt/ios/modules/mixed/interactionc.py index 744decddb..8eadda48d 100644 --- a/src/mvt/ios/modules/mixed/interactionc.py +++ b/src/mvt/ios/modules/mixed/interactionc.py @@ -5,9 +5,14 @@ import logging import sqlite3 -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_mactime_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleSerializedResult, + ModuleResults, +) from ..base import IOSExtraction @@ -223,7 +228,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -247,7 +252,7 @@ def __init__( "last_outgoing_recipient_date", ] - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: records = [] processed = [] for timestamp in self.timestamps: diff --git a/src/mvt/ios/modules/mixed/locationd.py b/src/mvt/ios/modules/mixed/locationd.py index c1905890e..35a1c492e 100644 --- a/src/mvt/ios/modules/mixed/locationd.py +++ b/src/mvt/ios/modules/mixed/locationd.py @@ -6,9 +6,14 @@ import base64 import logging import plistlib -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_mactime_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleSerializedResult, + ModuleResults, +) from ..base import IOSExtraction @@ -31,7 +36,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -54,7 +59,7 @@ def __init__( "BeaconRegionTimeStopped", ] - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: records = [] for timestamp in self.timestamps: if timestamp in record.keys(): @@ -77,59 +82,71 @@ def check_indicators(self) -> None: parts = result["package"].split("/") proc_name = parts[len(parts) - 1] - ioc = self.indicators.check_process(proc_name) - if ioc: - self.log.warning( - "Found a suspicious process name in LocationD entry %s", - result["package"], + ioc_match = self.indicators.check_process(proc_name) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.high( + self.get_slug(), + f"Found a suspicious process name in LocationD entry {result['package']}", + "", + result, ) - result["matched_indicator"] = ioc - self.detected.append(result) + self.alertstore.log_latest() continue if "BundleId" in result: - ioc = self.indicators.check_process(result["BundleId"]) - if ioc: - self.log.warning( - "Found a suspicious process name in LocationD entry %s", - result["package"], + ioc_match = self.indicators.check_process(result["BundleId"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.high( + self.get_slug(), + f"Found a suspicious process name in LocationD entry {result['package']}", + "", + result, ) - result["matched_indicator"] = ioc + self.alertstore.log_latest() if "BundlePath" in result: - ioc = self.indicators.check_file_path(result["BundlePath"]) - if ioc: - self.log.warning( - "Found a suspicious file path in Location D: %s", - result["BundlePath"], + ioc_match = self.indicators.check_file_path(result["BundlePath"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.high( + self.get_slug(), + f"Found a suspicious file path in LocationD entry {result['BundlePath']}", + "", + result, ) - result["matched_indicator"] = ioc - self.detected.append(result) + self.alertstore.log_latest() continue if "Executable" in result: - ioc = self.indicators.check_file_path(result["Executable"]) - if ioc: - self.log.warning( - "Found a suspicious file path in Location D: %s", - result["Executable"], + ioc_match = self.indicators.check_file_path(result["Executable"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.high( + self.get_slug(), + f"Found a suspicious file path in LocationD entry {result['Executable']}", + "", + result, ) - result["matched_indicator"] = ioc - self.detected.append(result) + self.alertstore.log_latest() continue if "Registered" in result: # Sometimes registered is a bool if isinstance(result["Registered"], bool): continue - ioc = self.indicators.check_file_path(result["Registered"]) - if ioc: - self.log.warning( - "Found a suspicious file path in Location D: %s", - result["Registered"], + + ioc_match = self.indicators.check_file_path(result["Registered"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.high( + self.get_slug(), + f"Found a suspicious file path in LocationD entry {result['Registered']}", + "", + result, ) - result["matched_indicator"] = ioc - self.detected.append(result) + self.alertstore.log_latest() continue def _extract_locationd_entries(self, file_path): diff --git a/src/mvt/ios/modules/mixed/net_datausage.py b/src/mvt/ios/modules/mixed/net_datausage.py index ce52179ce..713a7f9d6 100644 --- a/src/mvt/ios/modules/mixed/net_datausage.py +++ b/src/mvt/ios/modules/mixed/net_datausage.py @@ -6,6 +6,7 @@ import logging from typing import Optional +from mvt.common.module_types import ModuleResults from ..net_base import NetBase DATAUSAGE_BACKUP_IDS = [ @@ -30,7 +31,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, diff --git a/src/mvt/ios/modules/mixed/osanalytics_addaily.py b/src/mvt/ios/modules/mixed/osanalytics_addaily.py index aea5dbfd1..1238d3c2b 100644 --- a/src/mvt/ios/modules/mixed/osanalytics_addaily.py +++ b/src/mvt/ios/modules/mixed/osanalytics_addaily.py @@ -5,9 +5,14 @@ import logging import plistlib -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_datetime_to_iso +from mvt.common.module_types import ( + ModuleResults, + ModuleAtomicResult, + ModuleSerializedResult, +) from ..base import IOSExtraction @@ -30,7 +35,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -41,7 +46,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["ts"], "module": self.__class__.__name__, @@ -57,10 +62,10 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_process(result["package"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_process(result["package"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) def run(self) -> None: self._find_ios_database( diff --git a/src/mvt/ios/modules/mixed/safari_browserstate.py b/src/mvt/ios/modules/mixed/safari_browserstate.py index 616ea2004..2e9f834c0 100644 --- a/src/mvt/ios/modules/mixed/safari_browserstate.py +++ b/src/mvt/ios/modules/mixed/safari_browserstate.py @@ -8,9 +8,14 @@ import os import plistlib import sqlite3 -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_mactime_to_iso, keys_bytes_to_string +from mvt.common.module_types import ( + ModuleResults, + ModuleSerializedResult, + ModuleAtomicResult, +) from ..base import IOSExtraction @@ -31,7 +36,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -44,7 +49,7 @@ def __init__( self._session_history_count = 0 - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["last_viewed_timestamp"], "module": self.__class__.__name__, @@ -58,10 +63,12 @@ def check_indicators(self) -> None: for result in self.results: if "tab_url" in result: - ioc = self.indicators.check_url(result["tab_url"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_url(result["tab_url"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", result + ) continue if "session_data" not in result: @@ -69,10 +76,12 @@ def check_indicators(self) -> None: for session_entry in result["session_data"]: if "entry_url" in session_entry: - ioc = self.indicators.check_url(session_entry["entry_url"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_url(session_entry["entry_url"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", result + ) def _process_browser_state_db(self, db_path): self._recover_sqlite_db_if_needed(db_path) diff --git a/src/mvt/ios/modules/mixed/safari_history.py b/src/mvt/ios/modules/mixed/safari_history.py index 56bc9d03e..213cab675 100644 --- a/src/mvt/ios/modules/mixed/safari_history.py +++ b/src/mvt/ios/modules/mixed/safari_history.py @@ -5,10 +5,15 @@ import logging import os -from typing import Optional, Union +from typing import Optional from mvt.common.url import URL from mvt.common.utils import convert_mactime_to_datetime, convert_mactime_to_iso +from mvt.common.module_types import ( + ModuleResults, + ModuleAtomicResult, + ModuleSerializedResult, +) from ..base import IOSExtraction @@ -33,7 +38,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -44,7 +49,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, @@ -95,9 +100,11 @@ def _find_injections(self): elapsed_ms = elapsed_time.microseconds / 1000 if elapsed_time.seconds == 0: - self.log.warning( - "Redirect took less than a second! (%d milliseconds)", - elapsed_ms, + self.alertstore.medium( + self.get_slug(), + f"Redirect took less than a second! ({elapsed_ms} milliseconds)", + result["timestamp"], + result, ) def check_indicators(self) -> None: @@ -107,10 +114,10 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_url(result["url"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_url(result["url"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) def _process_history_db(self, history_path): self._recover_sqlite_db_if_needed(history_path) diff --git a/src/mvt/ios/modules/mixed/shortcuts.py b/src/mvt/ios/modules/mixed/shortcuts.py index f61168bcb..38735a0aa 100644 --- a/src/mvt/ios/modules/mixed/shortcuts.py +++ b/src/mvt/ios/modules/mixed/shortcuts.py @@ -8,9 +8,14 @@ import logging import plistlib import sqlite3 -from typing import Optional, Union +from typing import Optional from mvt.common.utils import check_for_links, convert_mactime_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleResults, + ModuleSerializedResult, +) from ..base import IOSExtraction @@ -32,7 +37,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -43,7 +48,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: found_urls = "" if record["action_urls"]: found_urls = f"- URLs in actions: {', '.join(record['action_urls'])}" @@ -72,10 +77,10 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_urls(result["action_urls"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_urls(result["action_urls"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) def run(self) -> None: self._find_ios_database( diff --git a/src/mvt/ios/modules/mixed/sms.py b/src/mvt/ios/modules/mixed/sms.py index 34c064b00..5f64941bd 100644 --- a/src/mvt/ios/modules/mixed/sms.py +++ b/src/mvt/ios/modules/mixed/sms.py @@ -6,9 +6,14 @@ import logging import sqlite3 from base64 import b64encode -from typing import Optional, Union +from typing import Optional from mvt.common.utils import check_for_links, convert_mactime_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleResults, + ModuleSerializedResult, +) from ..base import IOSExtraction @@ -30,7 +35,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -41,7 +46,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: text = record["text"].replace("\n", "\\n") sms_data = f'{record["service"]}: {record["guid"]} "{text}" from {record["phone_number"]} ({record["account"]})' records = [ @@ -71,10 +76,13 @@ def check_indicators(self) -> None: if message.get("text", "").startswith(alert_old) or message.get( "text", "" ).startswith(alert_new): - self.log.warning( - "Apple warning about state-sponsored attack received on the %s", + self.alertstore.high( + self.get_slug(), + f"Apple warning about state-sponsored attack received on the {message['isodate']}", message["isodate"], + message, ) + self.alertstore.log_latest() if not self.indicators: return @@ -84,10 +92,10 @@ def check_indicators(self) -> None: # Making sure not link was ignored if message_links == []: message_links = check_for_links(result.get("text", "")) - ioc = self.indicators.check_urls(message_links) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_urls(message_links) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) def run(self) -> None: self._find_ios_database(backup_ids=SMS_BACKUP_IDS, root_paths=SMS_ROOT_PATHS) diff --git a/src/mvt/ios/modules/mixed/sms_attachments.py b/src/mvt/ios/modules/mixed/sms_attachments.py index ea9b47767..0b9a6e342 100644 --- a/src/mvt/ios/modules/mixed/sms_attachments.py +++ b/src/mvt/ios/modules/mixed/sms_attachments.py @@ -5,9 +5,14 @@ import logging from base64 import b64encode -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_mactime_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleSerializedResult, + ModuleResults, +) from ..base import IOSExtraction @@ -29,7 +34,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -40,7 +45,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { "timestamp": record["isodate"], "module": self.__class__.__name__, @@ -56,22 +61,25 @@ def serialize(self, record: dict) -> Union[dict, list]: def check_indicators(self) -> None: for attachment in self.results: # Check for known malicious filenames. - if self.indicators and self.indicators.check_file_path( - attachment["filename"] - ): - self.detected.append(attachment) + if self.indicators: + ioc_match = self.indicators.check_file_path(attachment["filename"]) + if ioc_match: + attachment["matched_indicator"] = ioc_match.ioc + self.alertstore.high( + self.get_slug(), ioc_match.message, "", attachment + ) if ( attachment["filename"].startswith("/var/tmp/") and attachment["filename"].endswith("-1") and attachment["direction"] == "received" ): - self.log.warning( - "Suspicious iMessage attachment %s on %s", - attachment["filename"], + self.alertstore.medium( + self.get_slug(), + f"Suspicious iMessage attachment {attachment['filename']} on {attachment['isodate']}", attachment["isodate"], + attachment, ) - self.detected.append(attachment) def run(self) -> None: self._find_ios_database(backup_ids=SMS_BACKUP_IDS, root_paths=SMS_ROOT_PATHS) diff --git a/src/mvt/ios/modules/mixed/tcc.py b/src/mvt/ios/modules/mixed/tcc.py index 461e5b310..9be706e91 100644 --- a/src/mvt/ios/modules/mixed/tcc.py +++ b/src/mvt/ios/modules/mixed/tcc.py @@ -5,9 +5,14 @@ import logging import sqlite3 -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_unix_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleSerializedResult, + ModuleResults, +) from ..base import IOSExtraction @@ -51,7 +56,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -62,7 +67,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: if "last_modified" in record: if "allowed_value" in record: msg = ( @@ -89,10 +94,10 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_process(result["client"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_process(result["client"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) def process_db(self, file_path): conn = self._open_sqlite_db(file_path) diff --git a/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py b/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py index e0c2833b3..4de6df6f7 100644 --- a/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py +++ b/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py @@ -6,9 +6,14 @@ import logging import os import sqlite3 -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_unix_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleSerializedResult, + ModuleResults, +) from ..base import IOSExtraction @@ -32,7 +37,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -45,7 +50,7 @@ def __init__( self.results = [] if not results else results - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: msg = f"Webkit resource loaded from {record['registrable_domain']}" if record["domain"] != "": msg += f" by app in domain {record['domain']}" @@ -60,12 +65,12 @@ def check_indicators(self) -> None: if not self.indicators: return - self.detected = [] for result in self.results: - ioc = self.indicators.check_url(result["registrable_domain"]) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_url(result["registrable_domain"]) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.log_latest() def _process_observations_db(self, db_path: str, domain: str, path: str) -> None: self.log.info( diff --git a/src/mvt/ios/modules/mixed/webkit_session_resource_log.py b/src/mvt/ios/modules/mixed/webkit_session_resource_log.py index 0ae254508..be59a8021 100644 --- a/src/mvt/ios/modules/mixed/webkit_session_resource_log.py +++ b/src/mvt/ios/modules/mixed/webkit_session_resource_log.py @@ -9,6 +9,7 @@ from typing import Optional from mvt.common.utils import convert_datetime_to_iso +from mvt.common.module_types import ModuleResults from ..base import IOSExtraction @@ -38,7 +39,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -86,10 +87,12 @@ def check_indicators(self) -> None: [entry["origin"]] + source_domains + destination_domains ) - ioc = self.indicators.check_urls(all_origins) - if ioc: - entry["matched_indicator"] = ioc - self.detected.append(entry) + ioc_match = self.indicators.check_urls(all_origins) + if ioc_match: + entry["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, "", entry + ) redirect_path = "" if len(source_domains) > 0: @@ -110,9 +113,11 @@ def check_indicators(self) -> None: redirect_path += ", ".join(destination_domains) - self.log.warning( - "Found HTTP redirect between suspicious domains: %s", - redirect_path, + self.alertstore.high( + self.get_slug(), + f"Found HTTP redirect between suspicious domains: {redirect_path}", + "", + entry, ) def _extract_browsing_stats(self, log_path): diff --git a/src/mvt/ios/modules/mixed/whatsapp.py b/src/mvt/ios/modules/mixed/whatsapp.py index 4e5d8db6b..4fdeba34c 100644 --- a/src/mvt/ios/modules/mixed/whatsapp.py +++ b/src/mvt/ios/modules/mixed/whatsapp.py @@ -4,9 +4,14 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional, Union +from typing import Optional from mvt.common.utils import check_for_links, convert_mactime_to_iso +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleResults, + ModuleSerializedResult, +) from ..base import IOSExtraction @@ -28,7 +33,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -39,7 +44,7 @@ def __init__( results=results, ) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: text = record.get("ZTEXT", "").replace("\n", "\\n") links_text = "" if record.get("links"): @@ -57,10 +62,10 @@ def check_indicators(self) -> None: return for result in self.results: - ioc = self.indicators.check_urls(result.get("links", [])) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_urls(result.get("links", [])) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) def run(self) -> None: self._find_ios_database( diff --git a/src/mvt/ios/modules/net_base.py b/src/mvt/ios/modules/net_base.py index 1773e292f..6de5bd16f 100644 --- a/src/mvt/ios/modules/net_base.py +++ b/src/mvt/ios/modules/net_base.py @@ -7,11 +7,16 @@ import operator import sqlite3 from pathlib import Path -from typing import Optional, Union +from typing import Optional from mvt.common.utils import convert_mactime_to_iso from .base import IOSExtraction +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleSerializedResult, + ModuleResults, +) class NetBase(IOSExtraction): @@ -25,7 +30,7 @@ def __init__( results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), - results: Optional[list] = None, + results: ModuleResults = [], ) -> None: super().__init__( file_path=file_path, @@ -129,7 +134,7 @@ def _extract_net_data(self): self.log.info("Extracted information on %d processes", len(self.results)) - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: record_data = ( f"{record['proc_name']} (Bundle ID: {record['bundle_id']}," f" ID: {record['proc_id']})" @@ -232,7 +237,10 @@ def _find_suspicious_processes(self): "been truncated in the database)" ) - self.log.warning(msg) + self.alertstore.medium( + self.get_slug(), msg, proc["live_isodate"], proc + ) + if not proc["live_proc_id"]: self.log.info( "Found process entry in ZPROCESS but not in ZLIVEUSAGE: %s at %s", @@ -251,16 +259,23 @@ def check_manipulated(self): # Avoid duplicate warnings for same process. if result["live_proc_id"] not in missing_process_cache: missing_process_cache.add(result["live_proc_id"]) - self.log.warning( - "Found manipulated process entry %s. Entry on %s", - result["live_proc_id"], + self.alertstore.high( + self.get_slug(), + f"Found manipulated process entry {result['live_proc_id']}. Entry on {result['live_isodate']}", result["live_isodate"], + result, ) + self.alertstore.log_latest() # Set manipulated proc timestamp so it appears in timeline. result["first_isodate"] = result["isodate"] = result["live_isodate"] result["proc_name"] = "MANIPULATED [process record deleted]" - self.detected.append(result) + self.alertstore.high( + self.get_slug(), + f"Found manipulated process entry {result['live_proc_id']}/", + result["first_isodate"], + result, + ) def find_deleted(self): """Identify process which may have been deleted from the DataUsage @@ -278,12 +293,13 @@ def find_deleted(self): for proc_id in range(min(all_proc_id), max(all_proc_id)): if proc_id not in all_proc_id: previous_proc = results_by_proc[last_proc_id] - self.log.info( - 'Missing process %d. Previous process at "%s" (%s)', - proc_id, + self.alertstore.low( + self.get_slug(), + f'Missing process {proc_id}. Previous process at "{previous_proc["first_isodate"]}" ({previous_proc["proc_name"]})', previous_proc["first_isodate"], - previous_proc["proc_name"], + previous_proc, ) + self.alertstore.log_latest() missing_procs[proc_id] = { "proc_id": proc_id, @@ -333,7 +349,9 @@ def check_indicators(self) -> None: if not result["proc_id"]: continue - ioc = self.indicators.check_process(proc_name) - if ioc: - result["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_process(proc_name) + if ioc_match: + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), ioc_match.message, result["first_isodate"], result + ) diff --git a/tests/android/test_artifact_dumpsys_accessibility.py b/tests/android/test_artifact_dumpsys_accessibility.py index 2eca8fabe..0c9d8d1e4 100644 --- a/tests/android/test_artifact_dumpsys_accessibility.py +++ b/tests/android/test_artifact_dumpsys_accessibility.py @@ -49,6 +49,6 @@ def test_ioc_check(self, indicator_file): ind.parse_stix2(indicator_file) ind.ioc_collections[0]["app_ids"].append("com.sec.android.app.camera") da.indicators = ind - assert len(da.detected) == 0 + assert len(da.alertstore.alerts) == 0 da.check_indicators() - assert len(da.detected) == 1 + assert len(da.alertstore.alerts) == 1 diff --git a/tests/android/test_artifact_dumpsys_appops.py b/tests/android/test_artifact_dumpsys_appops.py index 7c2edc273..8d593653f 100644 --- a/tests/android/test_artifact_dumpsys_appops.py +++ b/tests/android/test_artifact_dumpsys_appops.py @@ -42,22 +42,24 @@ def test_ioc_check(self, indicator_file): ind.parse_stix2(indicator_file) ind.ioc_collections[0]["app_ids"].append("com.facebook.katana") da.indicators = ind - assert len(da.detected) == 0 + assert len(da.alertstore.alerts) == 0 da.check_indicators() detected_by_ioc = [ - detected for detected in da.detected if detected.get("matched_indicator") + alert + for alert in da.alertstore.alerts + if "matched_indicator" in alert.event ] detected_by_permission_heuristic = [ - detected - for detected in da.detected + alert + for alert in da.alertstore.alerts if all( [ perm["name"] == "REQUEST_INSTALL_PACKAGES" - for perm in detected["permissions"] + for perm in alert.event["permissions"] ] ) ] - assert len(da.detected) == 3 + assert len(da.alertstore.alerts) == 3 assert len(detected_by_ioc) == 1 assert len(detected_by_permission_heuristic) == 2 diff --git a/tests/android/test_artifact_dumpsys_battery_daily.py b/tests/android/test_artifact_dumpsys_battery_daily.py index e93b05062..26917f968 100644 --- a/tests/android/test_artifact_dumpsys_battery_daily.py +++ b/tests/android/test_artifact_dumpsys_battery_daily.py @@ -32,6 +32,6 @@ def test_ioc_check(self, indicator_file): ind.parse_stix2(indicator_file) ind.ioc_collections[0]["app_ids"].append("com.facebook.system") dba.indicators = ind - assert len(dba.detected) == 0 + assert len(dba.alertstore.alerts) == 0 dba.check_indicators() - assert len(dba.detected) == 1 + assert len(dba.alertstore.alerts) == 1 diff --git a/tests/android/test_artifact_dumpsys_battery_history.py b/tests/android/test_artifact_dumpsys_battery_history.py index 9a09e88f8..03d7d2ef8 100644 --- a/tests/android/test_artifact_dumpsys_battery_history.py +++ b/tests/android/test_artifact_dumpsys_battery_history.py @@ -39,6 +39,6 @@ def test_ioc_check(self, indicator_file): ind.parse_stix2(indicator_file) ind.ioc_collections[0]["app_ids"].append("com.samsung.android.app.reminder") dba.indicators = ind - assert len(dba.detected) == 0 + assert len(dba.alertstore.alerts) == 0 dba.check_indicators() - assert len(dba.detected) == 2 + assert len(dba.alertstore.alerts) == 2 diff --git a/tests/android/test_artifact_dumpsys_dbinfo.py b/tests/android/test_artifact_dumpsys_dbinfo.py index 23df5f224..2becf6500 100644 --- a/tests/android/test_artifact_dumpsys_dbinfo.py +++ b/tests/android/test_artifact_dumpsys_dbinfo.py @@ -37,6 +37,6 @@ def test_ioc_check(self, indicator_file): ind.parse_stix2(indicator_file) ind.ioc_collections[0]["app_ids"].append("com.wssyncmldm") dbi.indicators = ind - assert len(dbi.detected) == 0 + assert len(dbi.alertstore.alerts) == 0 dbi.check_indicators() - assert len(dbi.detected) == 5 + assert len(dbi.alertstore.alerts) == 5 diff --git a/tests/android/test_artifact_dumpsys_package_activities.py b/tests/android/test_artifact_dumpsys_package_activities.py index da7c0ab15..5eab63d64 100644 --- a/tests/android/test_artifact_dumpsys_package_activities.py +++ b/tests/android/test_artifact_dumpsys_package_activities.py @@ -39,6 +39,6 @@ def test_ioc_check(self, indicator_file): ind.parse_stix2(indicator_file) ind.ioc_collections[0]["app_ids"].append("com.google.android.gms") dpa.indicators = ind - assert len(dpa.detected) == 0 + assert len(dpa.alertstore.alerts) == 0 dpa.check_indicators() - assert len(dpa.detected) == 1 + assert len(dpa.alertstore.alerts) == 1 diff --git a/tests/android/test_artifact_dumpsys_packages.py b/tests/android/test_artifact_dumpsys_packages.py index 6300f17b9..7b2ec0f10 100644 --- a/tests/android/test_artifact_dumpsys_packages.py +++ b/tests/android/test_artifact_dumpsys_packages.py @@ -37,6 +37,6 @@ def test_ioc_check(self, indicator_file): ind.parse_stix2(indicator_file) ind.ioc_collections[0]["app_ids"].append("com.sec.android.app.DataCreate") dpa.indicators = ind - assert len(dpa.detected) == 0 + assert len(dpa.alertstore.alerts) == 0 dpa.check_indicators() - assert len(dpa.detected) == 1 + assert len(dpa.alertstore.alerts) == 1 diff --git a/tests/android/test_artifact_dumpsys_platform_compat.py b/tests/android/test_artifact_dumpsys_platform_compat.py index e2321a4a3..c8c59b3da 100644 --- a/tests/android/test_artifact_dumpsys_platform_compat.py +++ b/tests/android/test_artifact_dumpsys_platform_compat.py @@ -35,6 +35,6 @@ def test_ioc_check(self, indicator_file): ind.ioc_collections[0]["app_ids"].append("org.torproject.torbrowser") ind.ioc_collections[0]["app_ids"].append("org.article19.circulo.next") dbi.indicators = ind - assert len(dbi.detected) == 0 + assert len(dbi.alertstore.alerts) == 0 dbi.check_indicators() - assert len(dbi.detected) == 2 + assert len(dbi.alertstore.alerts) == 2 diff --git a/tests/android/test_artifact_dumpsys_receivers.py b/tests/android/test_artifact_dumpsys_receivers.py index f236aa94d..e4bed62c5 100644 --- a/tests/android/test_artifact_dumpsys_receivers.py +++ b/tests/android/test_artifact_dumpsys_receivers.py @@ -42,6 +42,6 @@ def test_ioc_check(self, indicator_file): ind.parse_stix2(indicator_file) ind.ioc_collections[0]["app_ids"].append("com.android.storagemanager") dr.indicators = ind - assert len(dr.detected) == 0 + assert len(dr.alertstore.alerts) == 0 dr.check_indicators() - assert len(dr.detected) == 1 + assert len(dr.alertstore.alerts) == 1 diff --git a/tests/android/test_artifact_getprop.py b/tests/android/test_artifact_getprop.py index 4ae9036ea..d9fec6b7f 100644 --- a/tests/android/test_artifact_getprop.py +++ b/tests/android/test_artifact_getprop.py @@ -36,6 +36,6 @@ def test_ioc_check(self, indicator_file): "dalvik.vm.appimageformat" ) gp.indicators = ind - assert len(gp.detected) == 0 + assert len(gp.alertstore.alerts) == 0 gp.check_indicators() - assert len(gp.detected) == 1 + assert len(gp.alertstore.alerts) == 1 diff --git a/tests/android/test_artifact_processes.py b/tests/android/test_artifact_processes.py index 54bc36c34..2806472d6 100644 --- a/tests/android/test_artifact_processes.py +++ b/tests/android/test_artifact_processes.py @@ -33,6 +33,6 @@ def test_ioc_check(self, indicator_file): ind.parse_stix2(indicator_file) ind.ioc_collections[0]["processes"].append("lru-add-drain") p.indicators = ind - assert len(p.detected) == 0 + assert len(p.alertstore.alerts) == 0 p.check_indicators() - assert len(p.detected) == 1 + assert len(p.alertstore.alerts) == 1 diff --git a/tests/android/test_backup_parser.py b/tests/android/test_backup_parser.py index 4da90243d..5bd5b99ab 100644 --- a/tests/android/test_backup_parser.py +++ b/tests/android/test_backup_parser.py @@ -60,7 +60,6 @@ def test_parsing_compression(self): == "33e73df2ede9798dcb3a85c06200ee41c8f52dd2f2e50ffafcceb0407bc13e3a" ) sms = parse_tar_for_sms(ddata) - print(sms) assert isinstance(sms, list) assert len(sms) == 1 assert len(sms[0]["links"]) == 1 diff --git a/tests/android_androidqf/test_files.py b/tests/android_androidqf/test_files.py index c0d45b51c..c854473fb 100644 --- a/tests/android_androidqf/test_files.py +++ b/tests/android_androidqf/test_files.py @@ -22,4 +22,4 @@ def test_androidqf_files(self): run_module(m) assert len(m.results) == 3 assert len(m.timeline) == 6 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 diff --git a/tests/android_androidqf/test_getprop.py b/tests/android_androidqf/test_getprop.py index 3947acd4f..9a938cc44 100644 --- a/tests/android_androidqf/test_getprop.py +++ b/tests/android_androidqf/test_getprop.py @@ -26,7 +26,7 @@ def test_androidqf_getprop(self): assert m.results[0]["name"] == "dalvik.vm.appimageformat" assert m.results[0]["value"] == "lz4" assert len(m.timeline) == 0 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 def test_getprop_parsing_zip(self): fpath = get_artifact("androidqf.zip") @@ -38,7 +38,7 @@ def test_getprop_parsing_zip(self): assert m.results[0]["name"] == "dalvik.vm.appimageformat" assert m.results[0]["value"] == "lz4" assert len(m.timeline) == 0 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 def test_androidqf_getprop_detection(self, indicator_file): data_path = get_android_androidqf() @@ -52,5 +52,5 @@ def test_androidqf_getprop_detection(self, indicator_file): m.indicators = ind run_module(m) assert len(m.results) == 10 - assert len(m.detected) == 1 - assert m.detected[0]["name"] == "dalvik.vm.heapmaxfree" + assert len(m.alertstore.alerts) == 1 + assert m.alertstore.alerts[0].event["name"] == "dalvik.vm.heapmaxfree" diff --git a/tests/android_androidqf/test_packages.py b/tests/android_androidqf/test_packages.py index fe6332a13..129cb353b 100644 --- a/tests/android_androidqf/test_packages.py +++ b/tests/android_androidqf/test_packages.py @@ -47,38 +47,35 @@ def test_packages_list(self, module): def test_non_appstore_warnings(self, caplog, module): run_module(module) - assert len(module.detected) == 4 + assert len(module.alertstore.alerts) == 5 # Not a super test to be searching logs for this but heuristic detections not yet formalised - assert ( - 'Found a non-system package installed via adb or another method: "com.whatsapp"' - in caplog.text - ) + adb_message = "Found a non-system package installed via adb or another method:" whatsapp_detected = [ - pkg for pkg in module.detected if pkg["name"] == "com.whatsapp" + alert + for alert in module.alertstore.alerts + if alert.event["name"] == "com.whatsapp" ] assert len(whatsapp_detected) == 1 + assert adb_message in whatsapp_detected[0].message - assert ( - 'Found a package installed via a browser (installer="com.google.android.packageinstaller"): ' - '"app.revanced.manager.flutter"' in caplog.text - ) + browser_message = 'Found a package installed via a browser (installer="com.google.android.packageinstaller"): ' revanced_detected = [ - pkg - for pkg in module.detected - if pkg["name"] == "app.revanced.manager.flutter" + alert + for alert in module.alertstore.alerts + if alert.event["name"] == "app.revanced.manager.flutter" ] assert len(revanced_detected) == 1 + assert browser_message in revanced_detected[0].message - assert ( - 'Found a package installed via a third party store (installer="org.fdroid.fdroid"): "org.nuclearfog.apollo"' - in caplog.text - ) - # We do not currently flag a third party store as a detection, we only flag the app in the logs. + third_party_message = 'Found a package installed via a third party store (installer="org.fdroid.fdroid")' appollo_detected = [ - pkg for pkg in module.detected if pkg["name"] == "org.nuclearfog.apollo" + alert + for alert in module.alertstore.alerts + if alert.event["name"] == "org.nuclearfog.apollo" ] - assert len(appollo_detected) == 0 + assert len(appollo_detected) == 1 + assert third_party_message in appollo_detected[0].message def test_packages_ioc_package_names(self, module, indicators_factory): module.indicators = indicators_factory(app_ids=["com.malware.blah"]) @@ -86,12 +83,14 @@ def test_packages_ioc_package_names(self, module, indicators_factory): run_module(module) possible_detected_app = [ - pkg for pkg in module.detected if pkg["name"] == "com.malware.blah" + alert + for alert in module.alertstore.alerts + if alert.event["name"] == "com.malware.blah" ] assert len(possible_detected_app) == 1 - assert possible_detected_app[0]["name"] == "com.malware.blah" + assert possible_detected_app[0].event["name"] == "com.malware.blah" assert ( - possible_detected_app[0]["matched_indicator"].ioc.value + possible_detected_app[0].event["matched_indicator"].value == "com.malware.blah" ) @@ -105,12 +104,14 @@ def test_packages_ioc_sha256(self, module, indicators_factory): run_module(module) possible_detected_app = [ - pkg for pkg in module.detected if pkg["name"] == "com.malware.muahaha" + alert + for alert in module.alertstore.alerts + if alert.event["name"] == "com.malware.muahaha" ] assert len(possible_detected_app) == 1 - assert possible_detected_app[0]["name"] == "com.malware.muahaha" + assert possible_detected_app[0].event["name"] == "com.malware.muahaha" assert ( - possible_detected_app[0]["matched_indicator"].ioc.value + possible_detected_app[0].event["matched_indicator"].value == "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa" ) @@ -124,11 +125,13 @@ def test_packages_certificate_hash_ioc(self, module, indicators_factory): run_module(module) possible_detected_app = [ - pkg for pkg in module.detected if pkg["name"] == "com.malware.muahaha" + alert + for alert in module.alertstore.alerts + if alert.event["name"] == "com.malware.muahaha" ] assert len(possible_detected_app) == 1 - assert possible_detected_app[0]["name"] == "com.malware.muahaha" + assert possible_detected_app[0].event["name"] == "com.malware.muahaha" assert ( - possible_detected_app[0]["matched_indicator"].ioc.value + possible_detected_app[0].event["matched_indicator"].value == "c7e56178748be1441370416d4c10e34817ea0c961eb636c8e9d98e0fd79bf730" ) diff --git a/tests/android_androidqf/test_processes.py b/tests/android_androidqf/test_processes.py index bcd401339..da75fa586 100644 --- a/tests/android_androidqf/test_processes.py +++ b/tests/android_androidqf/test_processes.py @@ -22,4 +22,4 @@ def test_androidqf_processes(self): run_module(m) assert len(m.results) == 15 assert len(m.timeline) == 0 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 diff --git a/tests/android_androidqf/test_settings.py b/tests/android_androidqf/test_settings.py index 75527a791..ef7386a71 100644 --- a/tests/android_androidqf/test_settings.py +++ b/tests/android_androidqf/test_settings.py @@ -21,4 +21,4 @@ def test_parsing(self): run_module(m) assert len(m.results) == 1 assert "random" in m.results.keys() - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 diff --git a/tests/android_androidqf/test_sms.py b/tests/android_androidqf/test_sms.py index d7433cff3..116d5b2c6 100644 --- a/tests/android_androidqf/test_sms.py +++ b/tests/android_androidqf/test_sms.py @@ -25,7 +25,7 @@ def test_androidqf_sms(self): run_module(m) assert len(m.results) == 2 assert len(m.timeline) == 0 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 def test_androidqf_sms_encrypted_password_valid(self): data_path = os.path.join(get_artifact_folder(), "androidqf_encrypted") diff --git a/tests/android_androidqf/test_tcc.py b/tests/android_androidqf/test_tcc.py new file mode 100644 index 000000000..d1bf073b8 --- /dev/null +++ b/tests/android_androidqf/test_tcc.py @@ -0,0 +1,36 @@ +# 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 mvt.common.indicators import Indicators +from mvt.common.module import run_module +from mvt.ios.modules.mixed.tcc import TCC + +from ..utils import get_ios_backup_folder + + +class TestTCCModule: + def test_tcc(self): + m = TCC(target_path=get_ios_backup_folder()) + run_module(m) + assert len(m.results) == 11 + assert len(m.timeline) == 11 + assert len(m.alertstore.alerts) == 0 + assert m.results[0]["service"] == "kTCCServiceUbiquity" + assert m.results[0]["client"] == "com.apple.Preferences" + assert m.results[0]["auth_value"] == "allowed" + + def test_tcc_detection(self, indicator_file): + m = TCC(target_path=get_ios_backup_folder()) + ind = Indicators(log=logging.getLogger()) + ind.parse_stix2(indicator_file) + m.indicators = ind + run_module(m) + assert len(m.results) == 11 + assert len(m.timeline) == 11 + assert len(m.alertstore.alerts) == 1 + assert m.alertstore.alerts[0].event["service"] == "kTCCServiceLiverpool" + assert m.alertstore.alerts[0].event["client"] == "Launch" diff --git a/tests/android_bugreport/test_bugreport.py b/tests/android_bugreport/test_bugreport.py index 8abc8961f..cb156e570 100644 --- a/tests/android_bugreport/test_bugreport.py +++ b/tests/android_bugreport/test_bugreport.py @@ -35,9 +35,13 @@ def test_appops_module(self): assert len(m.timeline) == 16 detected_by_ioc = [ - detected for detected in m.detected if detected.get("matched_indicator") + detected + for detected in m.alertstore.alerts + if detected.event.get("matched_indicator") ] - assert len(m.detected) == 1 # Hueristic detection for suspicious permissions + assert ( + len(m.alertstore.alerts) == 1 + ) # Hueristic detection for suspicious permissions assert len(detected_by_ioc) == 0 def test_packages_module(self): diff --git a/tests/ios_backup/test_calendar.py b/tests/ios_backup/test_calendar.py index de3f32c4e..bf931f44d 100644 --- a/tests/ios_backup/test_calendar.py +++ b/tests/ios_backup/test_calendar.py @@ -18,7 +18,7 @@ def test_calendar(self): run_module(m) assert len(m.results) == 1 assert len(m.timeline) == 4 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 assert m.results[0]["summary"] == "Super interesting meeting" def test_calendar_detection(self, indicator_file): @@ -30,4 +30,4 @@ def test_calendar_detection(self, indicator_file): run_module(m) assert len(m.results) == 1 assert len(m.timeline) == 4 - assert len(m.detected) == 1 + assert len(m.alertstore.alerts) == 1 diff --git a/tests/ios_backup/test_datausage.py b/tests/ios_backup/test_datausage.py index 5e12727af..8ab25f812 100644 --- a/tests/ios_backup/test_datausage.py +++ b/tests/ios_backup/test_datausage.py @@ -7,6 +7,7 @@ from mvt.common.indicators import Indicators from mvt.common.module import run_module +from mvt.common.alerts import AlertLevel from mvt.ios.modules.mixed.net_datausage import Datausage from ..utils import get_ios_backup_folder @@ -19,7 +20,9 @@ def test_datausage(self): assert m.results[0]["isodate"][0:19] == "2019-08-27 15:08:09" assert len(m.results) == 42 assert len(m.timeline) == 60 - assert len(m.detected) == 0 + assert ( + len(m.alertstore.alerts) == 1 + ) # We now have a detection for missing processes. def test_detection(self, indicator_file): m = Datausage(target_path=get_ios_backup_folder()) @@ -29,4 +32,7 @@ def test_detection(self, indicator_file): ind.ioc_collections[0]["processes"].append("CumulativeUsageTracker") m.indicators = ind run_module(m) - assert len(m.detected) == 2 + critical_alerts = [ + alert for alert in m.alertstore.alerts if alert.level == AlertLevel.CRITICAL + ] + assert len(critical_alerts) == 2 diff --git a/tests/ios_backup/test_global_preferences.py b/tests/ios_backup/test_global_preferences.py index 705ca8ddb..7c2f76650 100644 --- a/tests/ios_backup/test_global_preferences.py +++ b/tests/ios_backup/test_global_preferences.py @@ -4,6 +4,7 @@ # https://license.mvt.re/1.1/ from mvt.common.module import run_module +from mvt.common.alerts import AlertLevel from mvt.ios.modules.mixed.global_preferences import GlobalPreferences from ..utils import get_ios_backup_folder @@ -15,6 +16,11 @@ def test_global_preferences(self): run_module(m) assert len(m.results) == 16 assert len(m.timeline) == 0 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 1 + + lockdown_mode_alert = m.alertstore.alerts[0] + assert lockdown_mode_alert.message == "Lockdown mode enabled" + assert lockdown_mode_alert.level == AlertLevel.INFORMATIONAL + assert m.results[0]["entry"] == "WebKitShowLinkPreviews" assert m.results[0]["value"] is False diff --git a/tests/ios_backup/test_manifest.py b/tests/ios_backup/test_manifest.py index 9b9882fe5..44df42dec 100644 --- a/tests/ios_backup/test_manifest.py +++ b/tests/ios_backup/test_manifest.py @@ -18,7 +18,7 @@ def test_manifest(self): run_module(m) assert len(m.results) == 3721 assert len(m.timeline) == 5881 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 def test_detection(self, indicator_file): m = Manifest(target_path=get_ios_backup_folder()) @@ -27,4 +27,4 @@ def test_detection(self, indicator_file): ind.ioc_collections[0]["file_names"].append("com.apple.CoreBrightness.plist") m.indicators = ind run_module(m) - assert len(m.detected) == 1 + assert len(m.alertstore.alerts) == 1 diff --git a/tests/ios_backup/test_safari_browserstate.py b/tests/ios_backup/test_safari_browserstate.py index e877ded92..fd7e62ea3 100644 --- a/tests/ios_backup/test_safari_browserstate.py +++ b/tests/ios_backup/test_safari_browserstate.py @@ -19,7 +19,7 @@ def test_parsing(self): run_module(m) assert len(m.results) == 1 assert len(m.timeline) == 1 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 def test_detection(self, indicator_file): m = SafariBrowserState(target_path=get_ios_backup_folder()) @@ -30,6 +30,6 @@ def test_detection(self, indicator_file): ind.ioc_collections[0]["domains"].append("en.wikipedia.org") m.indicators = ind run_module(m) - assert len(m.detected) == 1 + assert len(m.alertstore.alerts) == 1 assert len(m.results) == 1 assert m.results[0]["tab_url"] == "https://en.wikipedia.org/wiki/NSO_Group" diff --git a/tests/ios_backup/test_sms.py b/tests/ios_backup/test_sms.py index 243f6306c..03e4606b3 100644 --- a/tests/ios_backup/test_sms.py +++ b/tests/ios_backup/test_sms.py @@ -18,7 +18,7 @@ def test_sms(self): run_module(m) assert len(m.results) == 1 assert len(m.timeline) == 2 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 def test_detection(self, indicator_file): m = SMS(target_path=get_ios_backup_folder()) @@ -28,4 +28,4 @@ def test_detection(self, indicator_file): ind.ioc_collections[0]["domains"].append("badbadbad.example.org") m.indicators = ind run_module(m) - assert len(m.detected) == 1 + assert len(m.alertstore.alerts) == 1 diff --git a/tests/ios_backup/test_tcc.py b/tests/ios_backup/test_tcc.py index 23a1ab7e5..d1bf073b8 100644 --- a/tests/ios_backup/test_tcc.py +++ b/tests/ios_backup/test_tcc.py @@ -18,7 +18,7 @@ def test_tcc(self): run_module(m) assert len(m.results) == 11 assert len(m.timeline) == 11 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 assert m.results[0]["service"] == "kTCCServiceUbiquity" assert m.results[0]["client"] == "com.apple.Preferences" assert m.results[0]["auth_value"] == "allowed" @@ -31,6 +31,6 @@ def test_tcc_detection(self, indicator_file): run_module(m) assert len(m.results) == 11 assert len(m.timeline) == 11 - assert len(m.detected) == 1 - assert m.detected[0]["service"] == "kTCCServiceLiverpool" - assert m.detected[0]["client"] == "Launch" + assert len(m.alertstore.alerts) == 1 + assert m.alertstore.alerts[0].event["service"] == "kTCCServiceLiverpool" + assert m.alertstore.alerts[0].event["client"] == "Launch" diff --git a/tests/ios_backup/test_webkit_resource_load_statistics.py b/tests/ios_backup/test_webkit_resource_load_statistics.py index 0e59ebbe2..f0be231ac 100644 --- a/tests/ios_backup/test_webkit_resource_load_statistics.py +++ b/tests/ios_backup/test_webkit_resource_load_statistics.py @@ -18,4 +18,4 @@ def test_webkit(self): run_module(m) assert len(m.results) == 2 assert len(m.timeline) == 2 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 diff --git a/tests/ios_fs/test_filesystem.py b/tests/ios_fs/test_filesystem.py index 062713fdb..9fa664f44 100644 --- a/tests/ios_fs/test_filesystem.py +++ b/tests/ios_fs/test_filesystem.py @@ -17,7 +17,7 @@ def test_filesystem(self): run_module(m) assert len(m.results) == 15 assert len(m.timeline) == 15 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 def test_detection(self, indicator_file): m = Filesystem(target_path=get_ios_backup_folder()) @@ -31,4 +31,4 @@ def test_detection(self, indicator_file): run_module(m) assert len(m.results) == 15 assert len(m.timeline) == 15 - assert len(m.detected) == 1 + assert len(m.alertstore.alerts) == 1 From 70d646af78fae208be9495f4a60ee3e9276bf375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Mon, 6 Oct 2025 09:50:24 +0200 Subject: [PATCH 09/37] Quote STIX path in log line --- src/mvt/common/indicators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mvt/common/indicators.py b/src/mvt/common/indicators.py index 877429eab..c34418d4f 100644 --- a/src/mvt/common/indicators.py +++ b/src/mvt/common/indicators.py @@ -73,7 +73,7 @@ def _check_stix2_env_variable(self) -> None: self.parse_stix2(file) else: self.log.error( - "Path specified with env MVT_STIX2 is not a valid path: %s", path + "Path specified with env MVT_STIX2 is not a valid path: '%s'", path ) def _new_collection( @@ -212,7 +212,7 @@ def parse_stix2(self, file_path: str) -> None: :type file_path: str """ - self.log.info("Parsing STIX2 indicators file at path %s", file_path) + self.log.info("Parsing STIX2 indicators file at path '%s'", file_path) with open(file_path, "r", encoding="utf-8") as handle: try: From 05ad7d274cfc8dba8aa8f4cdf52384c78cc19561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Mon, 6 Oct 2025 09:50:43 +0200 Subject: [PATCH 10/37] Fix profile events log line --- src/mvt/ios/modules/backup/profile_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mvt/ios/modules/backup/profile_events.py b/src/mvt/ios/modules/backup/profile_events.py index 0648b9e43..98c87b160 100644 --- a/src/mvt/ios/modules/backup/profile_events.py +++ b/src/mvt/ios/modules/backup/profile_events.py @@ -57,7 +57,7 @@ def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: def check_indicators(self) -> None: for result in self.results: - message = f'On {result.get("timestamp")} process "{result.get("timestamp")}" started operation "{result.get("operation")}" of profile "{result.get("profile_id")}"' + message = f'On {result.get("timestamp")} process "{result.get("process")}" started operation "{result.get("operation")}" of profile "{result.get("profile_id")}"' self.alertstore.low( self.get_slug(), message, result.get("timestamp"), result ) From e9e621640b1797169b5e62229740fd9055ab6cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Mon, 6 Oct 2025 10:07:08 +0200 Subject: [PATCH 11/37] Close open archive (zip/tar) file handles --- src/mvt/android/cmd_check_androidqf.py | 7 ++++++ src/mvt/android/cmd_check_backup.py | 30 +++++++++++++++----------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/mvt/android/cmd_check_androidqf.py b/src/mvt/android/cmd_check_androidqf.py index e0f49aba5..013c62243 100644 --- a/src/mvt/android/cmd_check_androidqf.py +++ b/src/mvt/android/cmd_check_androidqf.py @@ -159,6 +159,9 @@ def run_bugreport_cmd(self) -> bool: self.timeline.extend(cmd.timeline) self.alertstore.extend(cmd.alertstore.alerts) + finally: + if bugreport: + bugreport.close() def run_backup_cmd(self) -> bool: try: @@ -183,6 +186,10 @@ def run_backup_cmd(self) -> bool: self.timeline.extend(cmd.timeline) self.alertstore.extend(cmd.alertstore.alerts) + finally: + if backup: + backup.close() + def finish(self) -> None: """ diff --git a/src/mvt/android/cmd_check_backup.py b/src/mvt/android/cmd_check_backup.py index 2c39bae2f..15711f322 100644 --- a/src/mvt/android/cmd_check_backup.py +++ b/src/mvt/android/cmd_check_backup.py @@ -56,12 +56,12 @@ def __init__( self.name = "check-backup" self.modules = BACKUP_MODULES - self.backup_type: str = "" - self.backup_archive: Optional[tarfile.TarFile] = None - self.backup_files: List[str] = [] + self.__type: str = "" + self.__tar: Optional[tarfile.TarFile] = None + self.__files: List[str] = [] def from_ab(self, ab_file_bytes: bytes) -> None: - self.backup_type = "ab" + self.__type = "ab" header = parse_ab_header(ab_file_bytes) if not header["backup"]: log.critical("Invalid backup format, file should be in .ab format") @@ -84,26 +84,26 @@ def from_ab(self, ab_file_bytes: bytes) -> None: sys.exit(1) dbytes = io.BytesIO(tardata) - self.backup_archive = tarfile.open(fileobj=dbytes) - for member in self.backup_archive: - self.backup_files.append(member.name) + self.__tar = tarfile.open(fileobj=dbytes) + for member in self.__tar: + self.__files.append(member.name) def init(self) -> None: if not self.target_path: return if os.path.isfile(self.target_path): - self.backup_type = "ab" + self.__type = "ab" with open(self.target_path, "rb") as handle: ab_file_bytes = handle.read() self.from_ab(ab_file_bytes) elif os.path.isdir(self.target_path): - self.backup_type = "folder" + self.__type = "folder" self.target_path = Path(self.target_path).absolute().as_posix() for root, subdirs, subfiles in os.walk(os.path.abspath(self.target_path)): for fname in subfiles: - self.backup_files.append( + self.__files.append( os.path.relpath(os.path.join(root, fname), self.target_path) ) else: @@ -114,7 +114,11 @@ def init(self) -> None: sys.exit(1) def module_init(self, module: BackupModule) -> None: # type: ignore[override] - if self.backup_type == "folder": - module.from_dir(self.target_path, self.backup_files) + if self.__type == "folder": + module.from_dir(self.target_path, self.__files) else: - module.from_ab(self.target_path, self.backup_archive, self.backup_files) + module.from_ab(self.target_path, self.__tar, self.__files) + + def finish(self) -> None: + if self.__tar: + self.__tar.close() From af8c56675b24bd456a0773390ba09f7dde64400e Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Fri, 7 Nov 2025 16:42:09 +0100 Subject: [PATCH 12/37] Fix root_binaries and mounts modules to use alertstore --- src/mvt/android/artifacts/mounts.py | 38 +++++++++++++------ .../modules/androidqf/root_binaries.py | 14 +++---- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/mvt/android/artifacts/mounts.py b/src/mvt/android/artifacts/mounts.py index 6e7b0b637..7483cf504 100644 --- a/src/mvt/android/artifacts/mounts.py +++ b/src/mvt/android/artifacts/mounts.py @@ -133,13 +133,18 @@ def check_indicators(self) -> None: if mount["is_system_partition"] and mount["is_read_write"]: system_rw_mounts.append(mount) if mount_point == "/system": - self.log.warning( - "Root detected /system partition is mounted as read-write (rw). " + self.alertstore.warning( + self.get_slug(), + "Root detected /system partition is mounted as read-write (rw)", + "", + mount, ) else: - self.log.warning( - "System partition %s is mounted as read-write (rw). This may indicate system modifications.", - mount_point, + self.alertstore.warning( + self.get_slug(), + f"System partition {mount_point} is mounted as read-write (rw). This may indicate system modifications.", + "", + mount, ) # Check for other suspicious mount options @@ -151,10 +156,11 @@ def check_indicators(self) -> None: ): continue suspicious_mounts.append(mount) - self.log.warning( - "Suspicious mount options found for %s: %s", - mount_point, - ", ".join(suspicious_opts), + self.alertstore.warning( + self.get_slug(), + f"Suspicious mount options found for {mount_point}: {', '.join(suspicious_opts)}", + "", + mount, ) # Log interesting mount information @@ -177,10 +183,20 @@ def check_indicators(self) -> None: ioc = self.indicators.check_file_path(mount.get("mount_point", "")) if ioc: mount["matched_indicator"] = ioc - self.detected.append(mount) + self.alertstore.critical( + self.get_slug(), + f"Mount point matches indicator: {mount.get('mount_point', '')}", + "", + mount, + ) # Check device paths for indicators ioc = self.indicators.check_file_path(mount.get("device", "")) if ioc: mount["matched_indicator"] = ioc - self.detected.append(mount) + self.alertstore.critical( + self.get_slug(), + f"Device path matches indicator: {mount.get('device', '')}", + "", + mount, + ) diff --git a/src/mvt/android/modules/androidqf/root_binaries.py b/src/mvt/android/modules/androidqf/root_binaries.py index c5df729ac..762f4cb8b 100644 --- a/src/mvt/android/modules/androidqf/root_binaries.py +++ b/src/mvt/android/modules/androidqf/root_binaries.py @@ -46,17 +46,17 @@ def check_indicators(self) -> None: # All found root binaries are considered indicators of rooting for result in self.results: - self.log.warning( - 'Found root binary "%s" at path "%s"', - result["binary_name"], - result["path"], + self.alertstore.warning( + self.get_slug(), + f'Found root binary "{result["binary_name"]}" at path "{result["path"]}"', + "", + result, ) - self.detected.append(result) - if self.detected: + if self.results: self.log.warning( "Device shows signs of rooting with %d root binaries found", - len(self.detected), + len(self.results), ) def run(self) -> None: From 301582d7dd882cf2bf0769f4e33fa45ec9335af6 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Fri, 7 Nov 2025 16:46:20 +0100 Subject: [PATCH 13/37] Update tests to use alertstore instead of detected attribute --- tests/android_androidqf/test_mounts.py | 4 +++- tests/android_androidqf/test_root_binaries.py | 4 ++-- tests/android_bugreport/test_bugreport.py | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/android_androidqf/test_mounts.py b/tests/android_androidqf/test_mounts.py index 95e8060db..d36511acc 100644 --- a/tests/android_androidqf/test_mounts.py +++ b/tests/android_androidqf/test_mounts.py @@ -94,4 +94,6 @@ def test_androidqf_module_no_mounts_file(self): assert len(m.results) == 0, ( f"Expected no results when mounts.json is absent, got: {m.results}" ) - assert len(m.detected) == 0, f"Expected no detections, got: {m.detected}" + assert len(m.alertstore.alerts) == 0, ( + f"Expected no detections, got: {m.alertstore.alerts}" + ) diff --git a/tests/android_androidqf/test_root_binaries.py b/tests/android_androidqf/test_root_binaries.py index 5d6b7701e..b014f580b 100644 --- a/tests/android_androidqf/test_root_binaries.py +++ b/tests/android_androidqf/test_root_binaries.py @@ -42,7 +42,7 @@ def test_root_binaries_detection(self, module): # Should find 4 root binaries from the test file assert len(module.results) == 4 - assert len(module.detected) == 4 + assert len(module.alertstore.alerts) == 4 # Check that all results are detected as indicators binary_paths = [result["path"] for result in module.results] @@ -113,4 +113,4 @@ def test_no_root_binaries_file(self, parent_data_path): run_module(m) assert len(m.results) == 0 - assert len(m.detected) == 0 + assert len(m.alertstore.alerts) == 0 diff --git a/tests/android_bugreport/test_bugreport.py b/tests/android_bugreport/test_bugreport.py index bdff1d1dd..a2c5e372a 100644 --- a/tests/android_bugreport/test_bugreport.py +++ b/tests/android_bugreport/test_bugreport.py @@ -9,6 +9,7 @@ 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 from ..utils import get_artifact_folder From 5b1f4df7a4bfe49cb1dbe34c9e199af61c4bf03a Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Fri, 7 Nov 2025 16:49:05 +0100 Subject: [PATCH 14/37] Fix alertstore method calls - use high() instead of warning() --- src/mvt/android/artifacts/mounts.py | 6 +++--- src/mvt/android/modules/androidqf/root_binaries.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mvt/android/artifacts/mounts.py b/src/mvt/android/artifacts/mounts.py index 7483cf504..2e1685d53 100644 --- a/src/mvt/android/artifacts/mounts.py +++ b/src/mvt/android/artifacts/mounts.py @@ -133,14 +133,14 @@ def check_indicators(self) -> None: if mount["is_system_partition"] and mount["is_read_write"]: system_rw_mounts.append(mount) if mount_point == "/system": - self.alertstore.warning( + self.alertstore.high( self.get_slug(), "Root detected /system partition is mounted as read-write (rw)", "", mount, ) else: - self.alertstore.warning( + self.alertstore.high( self.get_slug(), f"System partition {mount_point} is mounted as read-write (rw). This may indicate system modifications.", "", @@ -156,7 +156,7 @@ def check_indicators(self) -> None: ): continue suspicious_mounts.append(mount) - self.alertstore.warning( + self.alertstore.high( self.get_slug(), f"Suspicious mount options found for {mount_point}: {', '.join(suspicious_opts)}", "", diff --git a/src/mvt/android/modules/androidqf/root_binaries.py b/src/mvt/android/modules/androidqf/root_binaries.py index 762f4cb8b..c3c702992 100644 --- a/src/mvt/android/modules/androidqf/root_binaries.py +++ b/src/mvt/android/modules/androidqf/root_binaries.py @@ -46,7 +46,7 @@ def check_indicators(self) -> None: # All found root binaries are considered indicators of rooting for result in self.results: - self.alertstore.warning( + self.alertstore.high( self.get_slug(), f'Found root binary "{result["binary_name"]}" at path "{result["path"]}"', "", From 4b6a101cc76e454104279200ddf5ad4f089687d1 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Fri, 7 Nov 2025 17:14:47 +0100 Subject: [PATCH 15/37] Fix remaining test errors - Add log_latest() call in root_binaries to log each alert - Fix UnboundLocalError in cmd_check_androidqf by initializing bugreport variable - Remove incorrect backup.close() call since load_backup() returns bytes - Remove duplicate from_ab method in cmd_check_backup that was using old attributes --- src/mvt/android/cmd_check_androidqf.py | 33 +++++++++---------- src/mvt/android/cmd_check_backup.py | 28 ---------------- .../modules/androidqf/root_binaries.py | 1 + 3 files changed, 17 insertions(+), 45 deletions(-) diff --git a/src/mvt/android/cmd_check_androidqf.py b/src/mvt/android/cmd_check_androidqf.py index 8d57a1999..0427b1d58 100644 --- a/src/mvt/android/cmd_check_androidqf.py +++ b/src/mvt/android/cmd_check_androidqf.py @@ -140,6 +140,7 @@ def load_backup(self) -> bytes: raise NoAndroidQFBackup def run_bugreport_cmd(self) -> bool: + bugreport = None try: bugreport = self.load_bugreport() except NoAndroidQFBugReport: @@ -174,24 +175,22 @@ def run_backup_cmd(self) -> bool: "Skipping backup modules as no backup.ab found in AndroidQF data." ) return False - else: - cmd = CmdAndroidCheckBackup( - target_path=None, - results_path=self.results_path, - ioc_files=self.ioc_files, - iocs=self.iocs, - module_options=self.module_options, - hashes=self.hashes, - sub_command=True, - ) - cmd.from_ab(backup) - cmd.run() - self.timeline.extend(cmd.timeline) - self.alertstore.extend(cmd.alertstore.alerts) - finally: - if backup: - backup.close() + cmd = CmdAndroidCheckBackup( + target_path=None, + results_path=self.results_path, + ioc_files=self.ioc_files, + iocs=self.iocs, + module_options=self.module_options, + hashes=self.hashes, + sub_command=True, + ) + cmd.from_ab(backup) + cmd.run() + + self.timeline.extend(cmd.timeline) + self.alertstore.extend(cmd.alertstore.alerts) + return True def finish(self) -> None: """ diff --git a/src/mvt/android/cmd_check_backup.py b/src/mvt/android/cmd_check_backup.py index 027f99e03..8fc4d7161 100644 --- a/src/mvt/android/cmd_check_backup.py +++ b/src/mvt/android/cmd_check_backup.py @@ -92,34 +92,6 @@ def from_ab(self, ab_file_bytes: bytes) -> None: for member in self.__tar: self.__files.append(member.name) - def from_ab(self, ab_file_bytes: bytes) -> None: - self.backup_type = "ab" - header = parse_ab_header(ab_file_bytes) - if not header["backup"]: - log.critical("Invalid backup format, file should be in .ab format") - sys.exit(1) - - password = None - if header["encryption"] != "none": - password = prompt_or_load_android_backup_password(log, self.module_options) - if not password: - log.critical("No backup password provided.") - sys.exit(1) - try: - tardata = parse_backup_file(ab_file_bytes, password=password) - except InvalidBackupPassword: - log.critical("Invalid backup password") - sys.exit(1) - except AndroidBackupParsingError as exc: - log.critical("Impossible to parse this backup file: %s", exc) - log.critical("Please use Android Backup Extractor (ABE) instead") - sys.exit(1) - - dbytes = io.BytesIO(tardata) - self.backup_archive = tarfile.open(fileobj=dbytes) - for member in self.backup_archive: - self.backup_files.append(member.name) - def init(self) -> None: if not self.target_path: return diff --git a/src/mvt/android/modules/androidqf/root_binaries.py b/src/mvt/android/modules/androidqf/root_binaries.py index c3c702992..7bd56f6ac 100644 --- a/src/mvt/android/modules/androidqf/root_binaries.py +++ b/src/mvt/android/modules/androidqf/root_binaries.py @@ -52,6 +52,7 @@ def check_indicators(self) -> None: "", result, ) + self.alertstore.log_latest() if self.results: self.log.warning( From d4b970c7c0c6107af849fc5f727d4acd9854db69 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Fri, 7 Nov 2025 18:07:41 +0100 Subject: [PATCH 16/37] Log alerts on add --- src/mvt/common/alerts.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/mvt/common/alerts.py b/src/mvt/common/alerts.py index 635520dcd..e3831abb1 100644 --- a/src/mvt/common/alerts.py +++ b/src/mvt/common/alerts.py @@ -5,11 +5,11 @@ import csv import logging +from dataclasses import asdict, dataclass from enum import Enum -from dataclasses import dataclass, asdict -from typing import List, Dict, Any, Optional +from typing import Any, Dict, List, Optional -from .log import INFO_ALERT, LOW_ALERT, HIGH_ALERT, CRITICAL_ALERT, MEDIUM_ALERT +from .log import CRITICAL_ALERT, HIGH_ALERT, INFO_ALERT, LOW_ALERT, MEDIUM_ALERT from .module_types import ModuleAtomicResult @@ -41,9 +41,11 @@ def alerts(self) -> List[Alert]: def add(self, alert: Alert) -> None: self.__alerts.append(alert) + self.log(alert) def extend(self, alerts: List[Alert]) -> None: - self.__alerts.extend(alerts) + for alert in alerts: + self.add(alert) def info( self, module: str, message: str, event_time: str, event: ModuleAtomicResult From d259ab4810474ff5f1dac67e9ab9d95c2567d316 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Fri, 7 Nov 2025 18:20:36 +0100 Subject: [PATCH 17/37] Remove slug from alertstore calls --- .../artifacts/dumpsys_accessibility.py | 2 +- src/mvt/android/artifacts/dumpsys_appops.py | 9 ++------- .../artifacts/dumpsys_battery_daily.py | 5 +++-- .../artifacts/dumpsys_battery_history.py | 2 +- src/mvt/android/artifacts/dumpsys_dbinfo.py | 4 +--- .../artifacts/dumpsys_package_activities.py | 4 +--- src/mvt/android/artifacts/dumpsys_packages.py | 5 ++--- .../artifacts/dumpsys_platform_compat.py | 2 +- .../android/artifacts/dumpsys_receivers.py | 4 +--- src/mvt/android/artifacts/getprop.py | 4 ++-- src/mvt/android/artifacts/mounts.py | 5 ----- src/mvt/android/artifacts/processes.py | 4 ++-- .../android/artifacts/tombstone_crashes.py | 15 ++++++-------- src/mvt/android/modules/adb/chrome_history.py | 6 +++--- src/mvt/android/modules/adb/packages.py | 8 +++----- src/mvt/android/modules/adb/sms.py | 6 ++---- .../android/modules/androidqf/aqf_files.py | 10 +++++----- .../android/modules/androidqf/aqf_packages.py | 20 +++++-------------- .../modules/androidqf/root_binaries.py | 1 - src/mvt/android/modules/androidqf/sms.py | 4 +--- src/mvt/android/modules/backup/sms.py | 6 ++---- 21 files changed, 44 insertions(+), 82 deletions(-) diff --git a/src/mvt/android/artifacts/dumpsys_accessibility.py b/src/mvt/android/artifacts/dumpsys_accessibility.py index 66b1684a5..33fe8fa89 100644 --- a/src/mvt/android/artifacts/dumpsys_accessibility.py +++ b/src/mvt/android/artifacts/dumpsys_accessibility.py @@ -17,7 +17,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(result["package_name"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) continue def parse(self, content: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_appops.py b/src/mvt/android/artifacts/dumpsys_appops.py index 36d61c8ad..52ec7d740 100644 --- a/src/mvt/android/artifacts/dumpsys_appops.py +++ b/src/mvt/android/artifacts/dumpsys_appops.py @@ -5,12 +5,11 @@ from datetime import datetime -from mvt.common.utils import convert_datetime_to_iso from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult +from mvt.common.utils import convert_datetime_to_iso from .artifact import AndroidArtifact - RISKY_PERMISSIONS = ["REQUEST_INSTALL_PACKAGES"] RISKY_PACKAGES = ["com.android.shell"] @@ -46,9 +45,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(result.get("package_name")) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical( - self.get_slug(), ioc_match.message, "", result - ) + self.alertstore.critical(ioc_match.message, "", result) continue # We use a placeholder entry to create a basic alert even without permission entries. @@ -66,7 +63,6 @@ def check_indicators(self) -> None: cleaned_result = result.copy() cleaned_result["permissions"] = [perm] self.alertstore.medium( - self.get_slug(), f"Package '{result['package_name']}' had risky permission '{perm['name']}' set to '{entry['access']}' at {entry['timestamp']}", entry["timestamp"], cleaned_result, @@ -80,7 +76,6 @@ def check_indicators(self) -> None: cleaned_result = result.copy() cleaned_result["permissions"] = [perm] self.alertstore.medium( - self.get_slug(), f"Risky package '{result['package_name']}' had '{perm['name']}' permission set to '{entry['access']}' at {entry['timestamp']}", entry["timestamp"], cleaned_result, diff --git a/src/mvt/android/artifacts/dumpsys_battery_daily.py b/src/mvt/android/artifacts/dumpsys_battery_daily.py index 03082f55d..32d765aac 100644 --- a/src/mvt/android/artifacts/dumpsys_battery_daily.py +++ b/src/mvt/android/artifacts/dumpsys_battery_daily.py @@ -4,8 +4,9 @@ # https://license.mvt.re/1.1/ +from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult + from .artifact import AndroidArtifact -from mvt.common.module_types import ModuleSerializedResult, ModuleAtomicResult class DumpsysBatteryDailyArtifact(AndroidArtifact): @@ -30,7 +31,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(result["package_name"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) continue def parse(self, output: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_battery_history.py b/src/mvt/android/artifacts/dumpsys_battery_history.py index cd2d6a8cc..a6cec3ebe 100644 --- a/src/mvt/android/artifacts/dumpsys_battery_history.py +++ b/src/mvt/android/artifacts/dumpsys_battery_history.py @@ -19,7 +19,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(result["package_name"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) continue def parse(self, data: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_dbinfo.py b/src/mvt/android/artifacts/dumpsys_dbinfo.py index c5f251660..6dc9c3248 100644 --- a/src/mvt/android/artifacts/dumpsys_dbinfo.py +++ b/src/mvt/android/artifacts/dumpsys_dbinfo.py @@ -23,9 +23,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(part) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical( - self.get_slug(), ioc_match.message, "", result - ) + self.alertstore.critical(ioc_match.message, "", result) continue def parse(self, output: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_package_activities.py b/src/mvt/android/artifacts/dumpsys_package_activities.py index f3b5d2ed0..ef074921e 100644 --- a/src/mvt/android/artifacts/dumpsys_package_activities.py +++ b/src/mvt/android/artifacts/dumpsys_package_activities.py @@ -15,9 +15,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(activity["package_name"]) if ioc_match: activity["matched_indicator"] = ioc_match.ioc - self.alertstore.critical( - self.get_slug(), ioc_match.message, "", activity - ) + self.alertstore.critical(ioc_match.message, "", activity) continue def parse(self, content: str): diff --git a/src/mvt/android/artifacts/dumpsys_packages.py b/src/mvt/android/artifacts/dumpsys_packages.py index be59db832..e5b382861 100644 --- a/src/mvt/android/artifacts/dumpsys_packages.py +++ b/src/mvt/android/artifacts/dumpsys_packages.py @@ -7,9 +7,9 @@ from typing import Any, Dict, List from mvt.android.utils import ROOT_PACKAGES +from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult from .artifact import AndroidArtifact -from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult class DumpsysPackagesArtifact(AndroidArtifact): @@ -18,7 +18,6 @@ def check_indicators(self) -> None: # XXX: De-duplication Package detections if result["package_name"] in ROOT_PACKAGES: self.alertstore.medium( - self.get_slug(), f'Found an installed package related to rooting/jailbreaking: "{result["package_name"]}"', "", result, @@ -32,7 +31,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(result.get("package_name", "")) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) self.alertstore.log_latest() def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: diff --git a/src/mvt/android/artifacts/dumpsys_platform_compat.py b/src/mvt/android/artifacts/dumpsys_platform_compat.py index 012a6e378..45573e3ae 100644 --- a/src/mvt/android/artifacts/dumpsys_platform_compat.py +++ b/src/mvt/android/artifacts/dumpsys_platform_compat.py @@ -19,7 +19,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(result["package_name"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) continue def parse(self, data: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_receivers.py b/src/mvt/android/artifacts/dumpsys_receivers.py index 75f5afe60..98b0e0e94 100644 --- a/src/mvt/android/artifacts/dumpsys_receivers.py +++ b/src/mvt/android/artifacts/dumpsys_receivers.py @@ -53,9 +53,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(receiver["package_name"]) if ioc_match: receiver["matched_indicator"] = ioc_match.ioc - self.alertstore.critical( - self.get_slug(), ioc_match.message, "", {intent: receiver} - ) + self.alertstore.critical(ioc_match.message, "", {intent: receiver}) continue def parse(self, output: str) -> None: diff --git a/src/mvt/android/artifacts/getprop.py b/src/mvt/android/artifacts/getprop.py index 053b5fd31..ba827f919 100644 --- a/src/mvt/android/artifacts/getprop.py +++ b/src/mvt/android/artifacts/getprop.py @@ -60,7 +60,7 @@ def check_indicators(self) -> None: if entry["name"] == "ro.build.version.security_patch": warning_message = warn_android_patch_level(entry["value"], self.log) - self.alertstore.medium(self.get_slug(), warning_message, "", entry) + self.alertstore.medium(warning_message, "", entry) if not self.indicators: return @@ -71,4 +71,4 @@ def check_indicators(self) -> None: ) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) diff --git a/src/mvt/android/artifacts/mounts.py b/src/mvt/android/artifacts/mounts.py index 2e1685d53..f6a42586a 100644 --- a/src/mvt/android/artifacts/mounts.py +++ b/src/mvt/android/artifacts/mounts.py @@ -134,14 +134,12 @@ def check_indicators(self) -> None: system_rw_mounts.append(mount) if mount_point == "/system": self.alertstore.high( - self.get_slug(), "Root detected /system partition is mounted as read-write (rw)", "", mount, ) else: self.alertstore.high( - self.get_slug(), f"System partition {mount_point} is mounted as read-write (rw). This may indicate system modifications.", "", mount, @@ -157,7 +155,6 @@ def check_indicators(self) -> None: continue suspicious_mounts.append(mount) self.alertstore.high( - self.get_slug(), f"Suspicious mount options found for {mount_point}: {', '.join(suspicious_opts)}", "", mount, @@ -184,7 +181,6 @@ def check_indicators(self) -> None: if ioc: mount["matched_indicator"] = ioc self.alertstore.critical( - self.get_slug(), f"Mount point matches indicator: {mount.get('mount_point', '')}", "", mount, @@ -195,7 +191,6 @@ def check_indicators(self) -> None: if ioc: mount["matched_indicator"] = ioc self.alertstore.critical( - self.get_slug(), f"Device path matches indicator: {mount.get('device', '')}", "", mount, diff --git a/src/mvt/android/artifacts/processes.py b/src/mvt/android/artifacts/processes.py index 7b6f41f7b..2f1dcac64 100644 --- a/src/mvt/android/artifacts/processes.py +++ b/src/mvt/android/artifacts/processes.py @@ -61,10 +61,10 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(proc_name) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) continue ioc_match = self.indicators.check_process(proc_name) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) diff --git a/src/mvt/android/artifacts/tombstone_crashes.py b/src/mvt/android/artifacts/tombstone_crashes.py index 68b24bdbf..a73b80010 100644 --- a/src/mvt/android/artifacts/tombstone_crashes.py +++ b/src/mvt/android/artifacts/tombstone_crashes.py @@ -6,15 +6,15 @@ import datetime from typing import List, Optional -import pydantic import betterproto +import pydantic from dateutil import parser -from mvt.common.utils import convert_datetime_to_iso -from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult from mvt.android.parsers.proto.tombstone import Tombstone -from .artifact import AndroidArtifact +from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult +from mvt.common.utils import convert_datetime_to_iso +from .artifact import AndroidArtifact TOMBSTONE_DELIMITER = "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***" @@ -96,7 +96,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_process(result["process_name"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) continue if result.get("command_line", []): @@ -104,9 +104,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_process(command_name) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical( - self.get_slug(), ioc_match.message, "", result - ) + self.alertstore.critical(ioc_match.message, "", result) continue SUSPICIOUS_UIDS = [ @@ -116,7 +114,6 @@ def check_indicators(self) -> None: ] if result["uid"] in SUSPICIOUS_UIDS: self.alertstore.medium( - self.get_slug(), ( f"Potentially suspicious crash in process '{result['process_name']}' " f"running as UID '{result['uid']}' in tombstone '{result['file_name']}' at {result['timestamp']}" diff --git a/src/mvt/android/modules/adb/chrome_history.py b/src/mvt/android/modules/adb/chrome_history.py index 9568e4447..c00735d04 100644 --- a/src/mvt/android/modules/adb/chrome_history.py +++ b/src/mvt/android/modules/adb/chrome_history.py @@ -8,12 +8,12 @@ import sqlite3 from typing import Optional -from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso from mvt.common.module_types import ( ModuleAtomicResult, - ModuleSerializedResult, ModuleResults, + ModuleSerializedResult, ) +from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso from .base import AndroidExtraction @@ -59,7 +59,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_url(result["url"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) def _parse_db(self, db_path: str) -> None: """Parse a Chrome History database file. diff --git a/src/mvt/android/modules/adb/packages.py b/src/mvt/android/modules/adb/packages.py index 04563ae46..a73caf6d4 100644 --- a/src/mvt/android/modules/adb/packages.py +++ b/src/mvt/android/modules/adb/packages.py @@ -94,18 +94,16 @@ def check_indicators(self) -> None: if not self.indicators: continue - ioc_match = self.indicators.check_app_id(result.get("package_name")) + ioc_match = self.indicators.check_app_id(result["package_name"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) for package_file in result.get("files", []): ioc_match = self.indicators.check_file_hash(package_file["sha256"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical( - self.get_slug(), ioc_match.message, "", result - ) + self.alertstore.critical(ioc_match.message, "", result) # @staticmethod # def check_virustotal(packages: list) -> None: diff --git a/src/mvt/android/modules/adb/sms.py b/src/mvt/android/modules/adb/sms.py index a69592cd4..47f0a2096 100644 --- a/src/mvt/android/modules/adb/sms.py +++ b/src/mvt/android/modules/adb/sms.py @@ -10,12 +10,12 @@ from mvt.android.parsers.backup import AndroidBackupParsingError, parse_tar_for_sms from mvt.common.module import InsufficientPrivileges -from mvt.common.utils import check_for_links, convert_unix_to_iso from mvt.common.module_types import ( ModuleAtomicResult, ModuleResults, ModuleSerializedResult, ) +from mvt.common.utils import check_for_links, convert_unix_to_iso from .base import AndroidExtraction @@ -93,9 +93,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_urls(message_links) if ioc_match: message["matched_indicator"] = ioc_match.ioc - self.alertstore.critical( - self.get_slug(), ioc_match.message, "", message - ) + self.alertstore.critical(ioc_match.message, "", message) def _parse_db(self, db_path: str) -> None: """Parse an Android bugle_db SMS database file. diff --git a/src/mvt/android/modules/androidqf/aqf_files.py b/src/mvt/android/modules/androidqf/aqf_files.py index 562b2d31e..d355a9d73 100644 --- a/src/mvt/android/modules/androidqf/aqf_files.py +++ b/src/mvt/android/modules/androidqf/aqf_files.py @@ -15,8 +15,8 @@ from mvt.android.modules.androidqf.base import AndroidQFModule from mvt.common.module_types import ( - ModuleResults, ModuleAtomicResult, + ModuleResults, ModuleSerializedResult, ) from mvt.common.utils import convert_datetime_to_iso @@ -90,7 +90,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_file_path(result["path"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) self.alertstore.log_latest() continue @@ -105,16 +105,16 @@ def check_indicators(self) -> None: file_type = "executable " msg = f'Found {file_type}file at suspicious path "{result["path"]}"' - self.alertstore.high(self.get_slug(), msg, "", result) + self.alertstore.high(msg, "", result) self.alertstore.log_latest() if result.get("sha256", "") == "": continue - ioc_match = self.indicators.check_file_hash(result["sha256"]) + ioc_match = self.indicators.check_file_hash(result.get("sha256")) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) # TODO: adds SHA1 and MD5 when available in MVT diff --git a/src/mvt/android/modules/androidqf/aqf_packages.py b/src/mvt/android/modules/androidqf/aqf_packages.py index 20b1399a6..19cc0d248 100644 --- a/src/mvt/android/modules/androidqf/aqf_packages.py +++ b/src/mvt/android/modules/androidqf/aqf_packages.py @@ -11,13 +11,13 @@ BROWSER_INSTALLERS, PLAY_STORE_INSTALLERS, ROOT_PACKAGES, - THIRD_PARTY_STORE_INSTALLERS, SECURITY_PACKAGES, SYSTEM_UPDATE_PACKAGES, + THIRD_PARTY_STORE_INSTALLERS, ) +from mvt.common.module_types import ModuleResults from .base import AndroidQFModule -from mvt.common.module_types import ModuleResults class AQFPackages(AndroidQFModule): @@ -45,7 +45,6 @@ def check_indicators(self) -> None: for result in self.results: if result["name"] in ROOT_PACKAGES: self.alertstore.medium( - self.get_slug(), f'Found an installed package related to rooting/jailbreaking: "{result["name"]}"', "", result, @@ -56,7 +55,6 @@ def check_indicators(self) -> None: # Detections for apps installed via unusual methods. if result["installer"] in THIRD_PARTY_STORE_INSTALLERS: self.alertstore.info( - self.get_slug(), f'Found a package installed via a third party store (installer="{result["installer"]}"): "{result["name"]}"', "", result, @@ -64,7 +62,6 @@ def check_indicators(self) -> None: self.alertstore.log_latest() elif result["installer"] in BROWSER_INSTALLERS: self.alertstore.medium( - self.get_slug(), f'Found a package installed via a browser (installer="{result["installer"]}"): "{result["name"]}"', "", result, @@ -72,7 +69,6 @@ def check_indicators(self) -> None: self.alertstore.log_latest() elif result["installer"] == "null" and result["system"] is False: self.alertstore.high( - self.get_slug(), f'Found a non-system package installed via adb or another method: "{result["name"]}"', "", result, @@ -85,7 +81,6 @@ def check_indicators(self) -> None: package_disabled = result.get("disabled", None) if result["name"] in SECURITY_PACKAGES and package_disabled: self.alertstore.high( - self.get_slug(), f'Security package "{result["name"]}" disabled on the phone', "", result, @@ -94,7 +89,6 @@ def check_indicators(self) -> None: if result["name"] in SYSTEM_UPDATE_PACKAGES and package_disabled: self.alertstore.high( - self.get_slug(), f'System OTA update package "{result["name"]}" disabled on the phone', "", result, @@ -107,16 +101,14 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(result.get("name")) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical(ioc_match.message, "", result) self.alertstore.log_latest() for package_file in result.get("files", []): ioc_match = self.indicators.check_file_hash(package_file["sha256"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical( - self.get_slug(), ioc_match.message, "", result - ) + self.alertstore.critical(ioc_match.message, "", result) self.alertstore.log_latest() if "certificate" not in package_file: @@ -130,9 +122,7 @@ def check_indicators(self) -> None: ) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical( - self.get_slug(), ioc_match.message, "", result - ) + self.alertstore.critical(ioc_match.message, "", result) self.alertstore.log_latest() break diff --git a/src/mvt/android/modules/androidqf/root_binaries.py b/src/mvt/android/modules/androidqf/root_binaries.py index 7bd56f6ac..7a2cb3406 100644 --- a/src/mvt/android/modules/androidqf/root_binaries.py +++ b/src/mvt/android/modules/androidqf/root_binaries.py @@ -47,7 +47,6 @@ def check_indicators(self) -> None: # All found root binaries are considered indicators of rooting for result in self.results: self.alertstore.high( - self.get_slug(), f'Found root binary "{result["binary_name"]}" at path "{result["path"]}"', "", result, diff --git a/src/mvt/android/modules/androidqf/sms.py b/src/mvt/android/modules/androidqf/sms.py index bc1d36178..687f402c4 100644 --- a/src/mvt/android/modules/androidqf/sms.py +++ b/src/mvt/android/modules/androidqf/sms.py @@ -56,9 +56,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_domains(message.get("links", [])) if ioc_match: message["matched_indicator"] = ioc_match.ioc - self.alertstore.critical( - self.get_slug(), ioc_match.message, "", message - ) + self.alertstore.critical(ioc_match.message, "", message) def parse_backup(self, data): header = parse_ab_header(data) diff --git a/src/mvt/android/modules/backup/sms.py b/src/mvt/android/modules/backup/sms.py index 3fafcfc38..fe760494b 100644 --- a/src/mvt/android/modules/backup/sms.py +++ b/src/mvt/android/modules/backup/sms.py @@ -8,8 +8,8 @@ from mvt.android.modules.backup.base import BackupModule from mvt.android.parsers.backup import parse_sms_file -from mvt.common.utils import check_for_links from mvt.common.module_types import ModuleResults +from mvt.common.utils import check_for_links class SMS(BackupModule): @@ -47,9 +47,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_urls(message_links) if ioc_match: message["matched_indicator"] = ioc_match.ioc - self.alertstore.critical( - self.get_slug(), ioc_match.message, "", message - ) + self.alertstore.critical(ioc_match.message, "", message) continue def run(self) -> None: From b1f0a2de06c987babeddf2cb6d6fbb84a677fc9b Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Fri, 7 Nov 2025 18:22:08 +0100 Subject: [PATCH 18/37] update alerts.py --- src/mvt/common/alerts.py | 64 +++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/src/mvt/common/alerts.py b/src/mvt/common/alerts.py index e3831abb1..0c596cfea 100644 --- a/src/mvt/common/alerts.py +++ b/src/mvt/common/alerts.py @@ -4,6 +4,7 @@ # https://license.mvt.re/1.1/ import csv +import inspect import logging from dataclasses import asdict, dataclass from enum import Enum @@ -35,6 +36,39 @@ def __init__(self, log: Optional[logging.Logger] = None) -> None: self.__alerts: List[Alert] = [] self.__log = log + def _get_calling_module(self) -> str: + """ + Automatically detect the calling MVT module and return its slug. + + Walks up the call stack to find the first frame that belongs to an MVT module + (artifact or extraction module) and extracts its slug. + + :return: Module slug string + """ + frame = inspect.currentframe() + try: + # Walk up the call stack + while frame is not None: + frame = frame.f_back + if frame is None: + break + + # Get the 'self' object from the frame's local variables + frame_locals = frame.f_locals + if "self" in frame_locals: + obj = frame_locals["self"] + # Check if it has a get_slug method (MVT modules have this) + if hasattr(obj, "get_slug") and callable(obj.get_slug): + try: + return obj.get_slug() + except: + pass + + # Fallback: return "unknown" if we can't find the module + return "unknown" + finally: + del frame + @property def alerts(self) -> List[Alert]: return self.__alerts @@ -47,65 +81,55 @@ def extend(self, alerts: List[Alert]) -> None: for alert in alerts: self.add(alert) - def info( - self, module: str, message: str, event_time: str, event: ModuleAtomicResult - ): + def info(self, message: str, event_time: str, event: ModuleAtomicResult): self.add( Alert( level=AlertLevel.INFORMATIONAL, - module=module, + module=self._get_calling_module(), message=message, event_time=event_time, event=event, ) ) - def low( - self, module: str, message: str, event_time: str, event: ModuleAtomicResult - ): + def low(self, message: str, event_time: str, event: ModuleAtomicResult): self.add( Alert( level=AlertLevel.LOW, - module=module, + module=self._get_calling_module(), message=message, event_time=event_time, event=event, ) ) - def medium( - self, module: str, message: str, event_time: str, event: ModuleAtomicResult - ): + def medium(self, message: str, event_time: str, event: ModuleAtomicResult): self.add( Alert( level=AlertLevel.MEDIUM, - module=module, + module=self._get_calling_module(), message=message, event_time=event_time, event=event, ) ) - def high( - self, module: str, message: str, event_time: str, event: ModuleAtomicResult - ): + def high(self, message: str, event_time: str, event: ModuleAtomicResult): self.add( Alert( level=AlertLevel.HIGH, - module=module, + module=self._get_calling_module(), message=message, event_time=event_time, event=event, ) ) - def critical( - self, module: str, message: str, event_time: str, event: ModuleAtomicResult - ): + def critical(self, message: str, event_time: str, event: ModuleAtomicResult): self.add( Alert( level=AlertLevel.CRITICAL, - module=module, + module=self._get_calling_module(), message=message, event_time=event_time, event=event, From c6837a455a381a55804eb4ac27bf6f5c5dc61709 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Fri, 7 Nov 2025 18:25:20 +0100 Subject: [PATCH 19/37] update alerts.py --- src/mvt/common/alerts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mvt/common/alerts.py b/src/mvt/common/alerts.py index 0c596cfea..5ea73e654 100644 --- a/src/mvt/common/alerts.py +++ b/src/mvt/common/alerts.py @@ -61,7 +61,7 @@ def _get_calling_module(self) -> str: if hasattr(obj, "get_slug") and callable(obj.get_slug): try: return obj.get_slug() - except: + except Exception: pass # Fallback: return "unknown" if we can't find the module From cc7781e255481cf652479cab21ebcf3199038fd6 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Fri, 7 Nov 2025 18:50:35 +0100 Subject: [PATCH 20/37] move indicator_match to alert object --- src/mvt/android/utils.py | 5 ++--- src/mvt/common/command.py | 12 ++++++------ src/mvt/ios/modules/mixed/global_preferences.py | 9 ++------- src/mvt/ios/modules/mixed/tcc.py | 9 +++++---- tests/android/test_artifact_dumpsys_appops.py | 3 ++- tests/android_androidqf/test_packages.py | 9 +++------ 6 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/mvt/android/utils.py b/src/mvt/android/utils.py index 689c048ab..5032ccff2 100644 --- a/src/mvt/android/utils.py +++ b/src/mvt/android/utils.py @@ -6,14 +6,13 @@ from typing import List -def warn_android_patch_level(patch_level: str, log) -> str: +def warn_android_patch_level(patch_level: str, log) -> str | bool: """Alert if Android patch level out-of-date""" patch_date = datetime.strptime(patch_level, "%Y-%m-%d") if (datetime.now() - patch_date) > timedelta(days=6 * 31): warning_message = ( f"This phone has not received security updates " - f"for more than six months (last update: {patch_level}).", - patch_level, + f"for more than six months (last update: {patch_level})." ) return warning_message diff --git a/src/mvt/common/command.py b/src/mvt/common/command.py index 7423157c6..e79639184 100644 --- a/src/mvt/common/command.py +++ b/src/mvt/common/command.py @@ -8,7 +8,7 @@ import os import sys from datetime import datetime -from typing import Optional +from typing import Any, Optional from rich.console import Console from rich.panel import Panel @@ -43,7 +43,7 @@ def __init__( disable_indicator_check: bool = False, ) -> None: self.name = "" - self.modules = [] + self.modules: list[Any] = [] self.target_path = target_path self.results_path = results_path @@ -62,10 +62,10 @@ def __init__( # This list will contain all executed modules. # We can use this to reference e.g. self.executed[0].results. - self.executed = [] + self.executed: list[Any] = [] self.hashes = hashes - self.hash_values = [] - self.timeline = [] + self.hash_values: list[dict[str, Any]] = [] + self.timeline: list[dict[str, Any]] = [] # Load IOCs self._create_storage() @@ -158,7 +158,7 @@ def _store_info(self) -> None: if self.target_path: target_path = os.path.abspath(self.target_path) - info = { + info: dict[str, Any] = { "target_path": target_path, "mvt_version": MVT_VERSION, "date": convert_datetime_to_iso(datetime.now()), diff --git a/src/mvt/ios/modules/mixed/global_preferences.py b/src/mvt/ios/modules/mixed/global_preferences.py index a2d70aad2..9f11db970 100644 --- a/src/mvt/ios/modules/mixed/global_preferences.py +++ b/src/mvt/ios/modules/mixed/global_preferences.py @@ -42,14 +42,9 @@ def check_indicators(self) -> None: for entry in self.results: if entry["entry"] == "LDMGlobalEnabled": if entry["value"]: - self.alertstore.info( - self.get_slug(), "Lockdown mode enabled", "", None - ) + self.alertstore.info("Lockdown mode enabled", "", None) else: - self.alertstore.low( - self.get_slug(), "Lockdown mode disabled", "", None - ) - self.alertstore.log_latest() + self.alertstore.low("Lockdown mode disabled", "", None) continue def process_file(self, file_path: str) -> None: diff --git a/src/mvt/ios/modules/mixed/tcc.py b/src/mvt/ios/modules/mixed/tcc.py index b991e3067..b0c54d4f4 100644 --- a/src/mvt/ios/modules/mixed/tcc.py +++ b/src/mvt/ios/modules/mixed/tcc.py @@ -7,12 +7,12 @@ import sqlite3 from typing import Optional -from mvt.common.utils import convert_unix_to_iso from mvt.common.module_types import ( ModuleAtomicResult, - ModuleSerializedResult, ModuleResults, + ModuleSerializedResult, ) +from mvt.common.utils import convert_unix_to_iso from ..base import IOSExtraction @@ -96,8 +96,9 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_process(result["client"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) def process_db(self, file_path): conn = self._open_sqlite_db(file_path) diff --git a/tests/android/test_artifact_dumpsys_appops.py b/tests/android/test_artifact_dumpsys_appops.py index 8d593653f..862b87212 100644 --- a/tests/android/test_artifact_dumpsys_appops.py +++ b/tests/android/test_artifact_dumpsys_appops.py @@ -48,7 +48,7 @@ def test_ioc_check(self, indicator_file): detected_by_ioc = [ alert for alert in da.alertstore.alerts - if "matched_indicator" in alert.event + if alert.matched_indicator is not None ] detected_by_permission_heuristic = [ alert @@ -62,4 +62,5 @@ def test_ioc_check(self, indicator_file): ] assert len(da.alertstore.alerts) == 3 assert len(detected_by_ioc) == 1 + assert detected_by_ioc[0].matched_indicator is not None assert len(detected_by_permission_heuristic) == 2 diff --git a/tests/android_androidqf/test_packages.py b/tests/android_androidqf/test_packages.py index 129cb353b..ae0ae6278 100644 --- a/tests/android_androidqf/test_packages.py +++ b/tests/android_androidqf/test_packages.py @@ -89,10 +89,7 @@ def test_packages_ioc_package_names(self, module, indicators_factory): ] assert len(possible_detected_app) == 1 assert possible_detected_app[0].event["name"] == "com.malware.blah" - assert ( - possible_detected_app[0].event["matched_indicator"].value - == "com.malware.blah" - ) + assert possible_detected_app[0].matched_indicator.value == "com.malware.blah" def test_packages_ioc_sha256(self, module, indicators_factory): module.indicators = indicators_factory( @@ -111,7 +108,7 @@ def test_packages_ioc_sha256(self, module, indicators_factory): assert len(possible_detected_app) == 1 assert possible_detected_app[0].event["name"] == "com.malware.muahaha" assert ( - possible_detected_app[0].event["matched_indicator"].value + possible_detected_app[0].matched_indicator.value == "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa" ) @@ -132,6 +129,6 @@ def test_packages_certificate_hash_ioc(self, module, indicators_factory): assert len(possible_detected_app) == 1 assert possible_detected_app[0].event["name"] == "com.malware.muahaha" assert ( - possible_detected_app[0].event["matched_indicator"].value + possible_detected_app[0].matched_indicator.value == "c7e56178748be1441370416d4c10e34817ea0c961eb636c8e9d98e0fd79bf730" ) From 6d1d499c4eadaf0e632d307284b02d0e2cebf316 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Fri, 7 Nov 2025 18:52:31 +0100 Subject: [PATCH 21/37] . --- .../artifacts/dumpsys_accessibility.py | 5 +- src/mvt/android/artifacts/dumpsys_appops.py | 12 +++-- .../artifacts/dumpsys_battery_daily.py | 8 ++-- .../artifacts/dumpsys_battery_history.py | 5 +- src/mvt/android/artifacts/dumpsys_dbinfo.py | 5 +- .../artifacts/dumpsys_package_activities.py | 5 +- src/mvt/android/artifacts/dumpsys_packages.py | 13 ++--- .../artifacts/dumpsys_platform_compat.py | 5 +- .../android/artifacts/dumpsys_receivers.py | 10 ++-- src/mvt/android/artifacts/getprop.py | 11 +++-- src/mvt/android/artifacts/mounts.py | 4 +- src/mvt/android/artifacts/processes.py | 10 ++-- .../android/artifacts/tombstone_crashes.py | 11 +++-- src/mvt/android/modules/adb/chrome_history.py | 5 +- src/mvt/android/modules/adb/packages.py | 10 ++-- src/mvt/android/modules/adb/sms.py | 5 +- .../android/modules/androidqf/aqf_files.py | 10 ++-- .../android/modules/androidqf/aqf_packages.py | 18 ++++--- src/mvt/android/modules/androidqf/sms.py | 5 +- src/mvt/android/modules/backup/sms.py | 9 ++-- src/mvt/common/alerts.py | 48 ++++++++++++++++--- 21 files changed, 142 insertions(+), 72 deletions(-) diff --git a/src/mvt/android/artifacts/dumpsys_accessibility.py b/src/mvt/android/artifacts/dumpsys_accessibility.py index 33fe8fa89..2a17df6d1 100644 --- a/src/mvt/android/artifacts/dumpsys_accessibility.py +++ b/src/mvt/android/artifacts/dumpsys_accessibility.py @@ -16,8 +16,9 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_app_id(result["package_name"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue def parse(self, content: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_appops.py b/src/mvt/android/artifacts/dumpsys_appops.py index 52ec7d740..492eac7a5 100644 --- a/src/mvt/android/artifacts/dumpsys_appops.py +++ b/src/mvt/android/artifacts/dumpsys_appops.py @@ -4,6 +4,7 @@ # https://license.mvt.re/1.1/ from datetime import datetime +from typing import Any from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult from mvt.common.utils import convert_datetime_to_iso @@ -44,8 +45,9 @@ def check_indicators(self) -> None: if self.indicators: ioc_match = self.indicators.check_app_id(result.get("package_name")) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue # We use a placeholder entry to create a basic alert even without permission entries. @@ -83,9 +85,9 @@ def check_indicators(self) -> None: def parse(self, output: str) -> None: # self.results: List[Dict[str, Any]] = [] - perm = {} - package = {} - entry = {} + perm: dict[str, Any] = {} + package: dict[str, Any] = {} + entry: dict[str, Any] = {} uid = None in_packages = False diff --git a/src/mvt/android/artifacts/dumpsys_battery_daily.py b/src/mvt/android/artifacts/dumpsys_battery_daily.py index 32d765aac..4eb2033b8 100644 --- a/src/mvt/android/artifacts/dumpsys_battery_daily.py +++ b/src/mvt/android/artifacts/dumpsys_battery_daily.py @@ -3,6 +3,7 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ +from typing import Any from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult @@ -30,13 +31,14 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_app_id(result["package_name"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue def parse(self, output: str) -> None: daily = None - daily_updates = [] + daily_updates: list[dict[str, Any]] = [] for line in output.splitlines(): if line.startswith(" Daily from "): if len(daily_updates) > 0: diff --git a/src/mvt/android/artifacts/dumpsys_battery_history.py b/src/mvt/android/artifacts/dumpsys_battery_history.py index a6cec3ebe..fb429030e 100644 --- a/src/mvt/android/artifacts/dumpsys_battery_history.py +++ b/src/mvt/android/artifacts/dumpsys_battery_history.py @@ -18,8 +18,9 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_app_id(result["package_name"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue def parse(self, data: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_dbinfo.py b/src/mvt/android/artifacts/dumpsys_dbinfo.py index 6dc9c3248..a3a4eb1fc 100644 --- a/src/mvt/android/artifacts/dumpsys_dbinfo.py +++ b/src/mvt/android/artifacts/dumpsys_dbinfo.py @@ -22,8 +22,9 @@ def check_indicators(self) -> None: for part in path.split("/"): ioc_match = self.indicators.check_app_id(part) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue def parse(self, output: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_package_activities.py b/src/mvt/android/artifacts/dumpsys_package_activities.py index ef074921e..31c6bbdb9 100644 --- a/src/mvt/android/artifacts/dumpsys_package_activities.py +++ b/src/mvt/android/artifacts/dumpsys_package_activities.py @@ -14,8 +14,9 @@ def check_indicators(self) -> None: for activity in self.results: ioc_match = self.indicators.check_app_id(activity["package_name"]) if ioc_match: - activity["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", activity) + self.alertstore.critical( + ioc_match.message, "", activity, matched_indicator=ioc_match.ioc + ) continue def parse(self, content: str): diff --git a/src/mvt/android/artifacts/dumpsys_packages.py b/src/mvt/android/artifacts/dumpsys_packages.py index e5b382861..41891e0fa 100644 --- a/src/mvt/android/artifacts/dumpsys_packages.py +++ b/src/mvt/android/artifacts/dumpsys_packages.py @@ -30,8 +30,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(result.get("package_name", "")) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) self.alertstore.log_latest() def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: @@ -62,15 +63,15 @@ def parse_dumpsys_package_for_details(output: str) -> Dict[str, Any]: """ Parse one entry of a dumpsys package information """ - details = { + details: Dict[str, Any] = { "uid": "", "version_name": "", "version_code": "", "timestamp": "", "first_install_time": "", "last_update_time": "", - "permissions": [], - "requested_permissions": [], + "permissions": list(), + "requested_permissions": list(), } in_install_permissions = False in_runtime_permissions = False @@ -148,7 +149,7 @@ def parse_dumpsys_packages(self, output: str) -> List[Dict[str, Any]]: results = [] package_name = None package = {} - lines = [] + lines: list[str] = [] for line in output.splitlines(): if line.startswith(" Package ["): if len(lines) > 0: diff --git a/src/mvt/android/artifacts/dumpsys_platform_compat.py b/src/mvt/android/artifacts/dumpsys_platform_compat.py index 45573e3ae..c01fbfd85 100644 --- a/src/mvt/android/artifacts/dumpsys_platform_compat.py +++ b/src/mvt/android/artifacts/dumpsys_platform_compat.py @@ -18,8 +18,9 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_app_id(result["package_name"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue def parse(self, data: str) -> None: diff --git a/src/mvt/android/artifacts/dumpsys_receivers.py b/src/mvt/android/artifacts/dumpsys_receivers.py index 98b0e0e94..c7130ed24 100644 --- a/src/mvt/android/artifacts/dumpsys_receivers.py +++ b/src/mvt/android/artifacts/dumpsys_receivers.py @@ -52,12 +52,16 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(receiver["package_name"]) if ioc_match: - receiver["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", {intent: receiver}) + self.alertstore.critical( + ioc_match.message, + "", + {intent: receiver}, + matched_indicator=ioc_match.ioc, + ) continue def parse(self, output: str) -> None: - self.results = {} + self.results: dict[str, list[dict[str, str]]] = {} in_receiver_resolver_table = False in_non_data_actions = False diff --git a/src/mvt/android/artifacts/getprop.py b/src/mvt/android/artifacts/getprop.py index ba827f919..71e4993fe 100644 --- a/src/mvt/android/artifacts/getprop.py +++ b/src/mvt/android/artifacts/getprop.py @@ -39,10 +39,10 @@ def parse(self, entry: str) -> None: if not matches or len(matches[0]) != 2: continue - entry = {"name": matches[0][0], "value": matches[0][1]} - self.results.append(entry) + prop_entry = {"name": matches[0][0], "value": matches[0][1]} + self.results.append(prop_entry) - def get_device_timezone(self) -> str: + def get_device_timezone(self) -> str | None: """ Get the device timezone from the getprop results @@ -70,5 +70,6 @@ def check_indicators(self) -> None: result.get("name", "") ) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) diff --git a/src/mvt/android/artifacts/mounts.py b/src/mvt/android/artifacts/mounts.py index f6a42586a..d4370ca64 100644 --- a/src/mvt/android/artifacts/mounts.py +++ b/src/mvt/android/artifacts/mounts.py @@ -179,19 +179,19 @@ def check_indicators(self) -> None: # Check if any mount points match indicators ioc = self.indicators.check_file_path(mount.get("mount_point", "")) if ioc: - mount["matched_indicator"] = ioc self.alertstore.critical( f"Mount point matches indicator: {mount.get('mount_point', '')}", "", mount, + matched_indicator=ioc, ) # Check device paths for indicators ioc = self.indicators.check_file_path(mount.get("device", "")) if ioc: - mount["matched_indicator"] = ioc self.alertstore.critical( f"Device path matches indicator: {mount.get('device', '')}", "", mount, + matched_indicator=ioc, ) diff --git a/src/mvt/android/artifacts/processes.py b/src/mvt/android/artifacts/processes.py index 2f1dcac64..7cf962e9b 100644 --- a/src/mvt/android/artifacts/processes.py +++ b/src/mvt/android/artifacts/processes.py @@ -60,11 +60,13 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(proc_name) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue ioc_match = self.indicators.check_process(proc_name) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) diff --git a/src/mvt/android/artifacts/tombstone_crashes.py b/src/mvt/android/artifacts/tombstone_crashes.py index a73b80010..63e7c53d9 100644 --- a/src/mvt/android/artifacts/tombstone_crashes.py +++ b/src/mvt/android/artifacts/tombstone_crashes.py @@ -95,16 +95,19 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_process(result["process_name"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue if result.get("command_line", []): command_name = result.get("command_line")[0].split("/")[-1] + command_name = result["command_line"][0] ioc_match = self.indicators.check_process(command_name) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue SUSPICIOUS_UIDS = [ diff --git a/src/mvt/android/modules/adb/chrome_history.py b/src/mvt/android/modules/adb/chrome_history.py index c00735d04..2be1d330a 100644 --- a/src/mvt/android/modules/adb/chrome_history.py +++ b/src/mvt/android/modules/adb/chrome_history.py @@ -58,8 +58,9 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_url(result["url"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) def _parse_db(self, db_path: str) -> None: """Parse a Chrome History database file. diff --git a/src/mvt/android/modules/adb/packages.py b/src/mvt/android/modules/adb/packages.py index a73caf6d4..74e3afc3b 100644 --- a/src/mvt/android/modules/adb/packages.py +++ b/src/mvt/android/modules/adb/packages.py @@ -96,14 +96,16 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(result["package_name"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) for package_file in result.get("files", []): ioc_match = self.indicators.check_file_hash(package_file["sha256"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) # @staticmethod # def check_virustotal(packages: list) -> None: diff --git a/src/mvt/android/modules/adb/sms.py b/src/mvt/android/modules/adb/sms.py index 47f0a2096..0b26131d1 100644 --- a/src/mvt/android/modules/adb/sms.py +++ b/src/mvt/android/modules/adb/sms.py @@ -92,8 +92,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_urls(message_links) if ioc_match: - message["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", message) + self.alertstore.critical( + ioc_match.message, "", message, matched_indicator=ioc_match.ioc + ) def _parse_db(self, db_path: str) -> None: """Parse an Android bugle_db SMS database file. diff --git a/src/mvt/android/modules/androidqf/aqf_files.py b/src/mvt/android/modules/androidqf/aqf_files.py index d355a9d73..35a848dae 100644 --- a/src/mvt/android/modules/androidqf/aqf_files.py +++ b/src/mvt/android/modules/androidqf/aqf_files.py @@ -89,8 +89,9 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_file_path(result["path"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) self.alertstore.log_latest() continue @@ -113,8 +114,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_file_hash(result.get("sha256")) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) # TODO: adds SHA1 and MD5 when available in MVT diff --git a/src/mvt/android/modules/androidqf/aqf_packages.py b/src/mvt/android/modules/androidqf/aqf_packages.py index 19cc0d248..34ed0cfa8 100644 --- a/src/mvt/android/modules/androidqf/aqf_packages.py +++ b/src/mvt/android/modules/androidqf/aqf_packages.py @@ -100,15 +100,17 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_app_id(result.get("name")) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) self.alertstore.log_latest() for package_file in result.get("files", []): ioc_match = self.indicators.check_file_hash(package_file["sha256"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) self.alertstore.log_latest() if "certificate" not in package_file: @@ -121,8 +123,12 @@ def check_indicators(self) -> None: certificate_hash ) if ioc_match: - result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, + "", + result, + matched_indicator=ioc_match.ioc, + ) self.alertstore.log_latest() break diff --git a/src/mvt/android/modules/androidqf/sms.py b/src/mvt/android/modules/androidqf/sms.py index 687f402c4..46cc3f6ba 100644 --- a/src/mvt/android/modules/androidqf/sms.py +++ b/src/mvt/android/modules/androidqf/sms.py @@ -55,8 +55,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_domains(message.get("links", [])) if ioc_match: - message["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", message) + self.alertstore.critical( + ioc_match.message, "", message, matched_indicator=ioc_match.ioc + ) def parse_backup(self, data): header = parse_ab_header(data) diff --git a/src/mvt/android/modules/backup/sms.py b/src/mvt/android/modules/backup/sms.py index fe760494b..1c75587f2 100644 --- a/src/mvt/android/modules/backup/sms.py +++ b/src/mvt/android/modules/backup/sms.py @@ -4,7 +4,7 @@ # https://license.mvt.re/1.1/ import logging -from typing import Optional +from typing import Any, Optional from mvt.android.modules.backup.base import BackupModule from mvt.android.parsers.backup import parse_sms_file @@ -30,7 +30,7 @@ def __init__( log=log, results=results, ) - self.results = [] + self.results: list[dict[str, Any]] = [] def check_indicators(self) -> None: if not self.indicators: @@ -46,8 +46,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_urls(message_links) if ioc_match: - message["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(ioc_match.message, "", message) + self.alertstore.critical( + ioc_match.message, "", message, matched_indicator=ioc_match.ioc + ) continue def run(self) -> None: diff --git a/src/mvt/common/alerts.py b/src/mvt/common/alerts.py index 5ea73e654..004d4227e 100644 --- a/src/mvt/common/alerts.py +++ b/src/mvt/common/alerts.py @@ -29,6 +29,7 @@ class Alert: message: str event_time: str event: ModuleAtomicResult + matched_indicator: Optional[Any] = None class AlertStore: @@ -60,7 +61,7 @@ def _get_calling_module(self) -> str: # Check if it has a get_slug method (MVT modules have this) if hasattr(obj, "get_slug") and callable(obj.get_slug): try: - return obj.get_slug() + return str(obj.get_slug()) except Exception: pass @@ -81,7 +82,13 @@ def extend(self, alerts: List[Alert]) -> None: for alert in alerts: self.add(alert) - def info(self, message: str, event_time: str, event: ModuleAtomicResult): + def info( + self, + message: str, + event_time: str, + event: ModuleAtomicResult, + matched_indicator: Optional[Any] = None, + ): self.add( Alert( level=AlertLevel.INFORMATIONAL, @@ -89,10 +96,17 @@ def info(self, message: str, event_time: str, event: ModuleAtomicResult): message=message, event_time=event_time, event=event, + matched_indicator=matched_indicator, ) ) - def low(self, message: str, event_time: str, event: ModuleAtomicResult): + def low( + self, + message: str, + event_time: str, + event: ModuleAtomicResult, + matched_indicator: Optional[Any] = None, + ): self.add( Alert( level=AlertLevel.LOW, @@ -100,10 +114,17 @@ def low(self, message: str, event_time: str, event: ModuleAtomicResult): message=message, event_time=event_time, event=event, + matched_indicator=matched_indicator, ) ) - def medium(self, message: str, event_time: str, event: ModuleAtomicResult): + def medium( + self, + message: str, + event_time: str, + event: ModuleAtomicResult, + matched_indicator: Optional[Any] = None, + ): self.add( Alert( level=AlertLevel.MEDIUM, @@ -111,10 +132,17 @@ def medium(self, message: str, event_time: str, event: ModuleAtomicResult): message=message, event_time=event_time, event=event, + matched_indicator=matched_indicator, ) ) - def high(self, message: str, event_time: str, event: ModuleAtomicResult): + def high( + self, + message: str, + event_time: str, + event: ModuleAtomicResult, + matched_indicator: Optional[Any] = None, + ): self.add( Alert( level=AlertLevel.HIGH, @@ -122,10 +150,17 @@ def high(self, message: str, event_time: str, event: ModuleAtomicResult): message=message, event_time=event_time, event=event, + matched_indicator=matched_indicator, ) ) - def critical(self, message: str, event_time: str, event: ModuleAtomicResult): + def critical( + self, + message: str, + event_time: str, + event: ModuleAtomicResult, + matched_indicator: Optional[Any] = None, + ): self.add( Alert( level=AlertLevel.CRITICAL, @@ -133,6 +168,7 @@ def critical(self, message: str, event_time: str, event: ModuleAtomicResult): message=message, event_time=event_time, event=event, + matched_indicator=matched_indicator, ) ) From 801c464492826d30ce4f1369b8943bbd3b327c52 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Fri, 7 Nov 2025 19:05:39 +0100 Subject: [PATCH 22/37] - Remove timeline_detected and route to alertstore --- src/mvt/android/modules/adb/files.py | 16 ++++++++++------ src/mvt/android/modules/adb/root_binaries.py | 10 +++++++--- src/mvt/android/modules/adb/whatsapp.py | 15 +++++++++------ src/mvt/common/module.py | 10 ---------- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/mvt/android/modules/adb/files.py b/src/mvt/android/modules/adb/files.py index 28b108871..134d9d281 100644 --- a/src/mvt/android/modules/adb/files.py +++ b/src/mvt/android/modules/adb/files.py @@ -8,8 +8,8 @@ import stat from typing import Optional, Union -from mvt.common.utils import convert_unix_to_iso from mvt.common.module_types import ModuleResults +from mvt.common.utils import convert_unix_to_iso from .base import AndroidExtraction @@ -64,11 +64,15 @@ def check_indicators(self) -> None: result["path"], ) - if self.indicators and self.indicators.check_file_path(result["path"]): - self.log.warning( - 'Found a known suspicous file at path: "%s"', result["path"] - ) - self.detected.append(result) + if self.indicators: + ioc_match = self.indicators.check_file_path(result["path"]) + if ioc_match: + self.alertstore.critical( + f'Found a known suspicious file at path: "{result["path"]}"', + "", + result, + matched_indicator=ioc_match, + ) def backup_file(self, file_path: str) -> None: if not self.results_path: diff --git a/src/mvt/android/modules/adb/root_binaries.py b/src/mvt/android/modules/adb/root_binaries.py index 0315e230b..d9d726973 100644 --- a/src/mvt/android/modules/adb/root_binaries.py +++ b/src/mvt/android/modules/adb/root_binaries.py @@ -6,9 +6,10 @@ import logging from typing import Optional -from .base import AndroidExtraction from mvt.common.module_types import ModuleResults +from .base import AndroidExtraction + class RootBinaries(AndroidExtraction): """This module extracts the list of installed packages.""" @@ -33,8 +34,11 @@ def __init__( def check_indicators(self) -> None: for root_binary in self.results: - self.detected.append(root_binary) - self.log.warning('Found root binary "%s"', root_binary) + self.alertstore.high( + f'Found root binary "{root_binary}"', + "", + root_binary, + ) def run(self) -> None: root_binaries = [ diff --git a/src/mvt/android/modules/adb/whatsapp.py b/src/mvt/android/modules/adb/whatsapp.py index 40f8875ac..76c130556 100644 --- a/src/mvt/android/modules/adb/whatsapp.py +++ b/src/mvt/android/modules/adb/whatsapp.py @@ -9,14 +9,14 @@ import sqlite3 from typing import Optional -from mvt.common.utils import check_for_links, convert_unix_to_iso - -from .base import AndroidExtraction from mvt.common.module_types import ( ModuleAtomicResult, - ModuleSerializedResult, ModuleResults, + ModuleSerializedResult, ) +from mvt.common.utils import check_for_links, convert_unix_to_iso + +from .base import AndroidExtraction WHATSAPP_PATH = "data/data/com.whatsapp/databases/msgstore.db" @@ -60,8 +60,11 @@ def check_indicators(self) -> None: continue message_links = check_for_links(message["data"]) - if self.indicators.check_urls(message_links): - self.detected.append(message) + ioc_match = self.indicators.check_urls(message_links) + if ioc_match: + self.alertstore.critical( + ioc_match.message, "", message, matched_indicator=ioc_match.ioc + ) continue def _parse_db(self, db_path: str) -> None: diff --git a/src/mvt/common/module.py b/src/mvt/common/module.py index 3ce10bfba..d5a76288d 100644 --- a/src/mvt/common/module.py +++ b/src/mvt/common/module.py @@ -77,7 +77,6 @@ def __init__( self.results: ModuleResults = results if results else [] self.timeline: ModuleTimeline = [] - self.timeline_detected: ModuleTimeline = [] @classmethod def from_json(cls, json_path: str, log: logging.Logger): @@ -166,17 +165,8 @@ def to_timeline(self) -> None: else: self.timeline.append(record) - # for detected in self.alertstore.alerts: - # record = self.serialize(detected) - # if record: - # if isinstance(record, list): - # self.timeline_detected.extend(record) - # else: - # self.timeline_detected.append(record) - # De-duplicate timeline entries. self.timeline = self._deduplicate_timeline(self.timeline) - # self.timeline_detected = self._deduplicate_timeline(self.timeline_detected) def run(self) -> None: """Run the main module procedure.""" From c779009550865f03ce8b75a6ac71a045e30e6c05 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Sat, 20 Dec 2025 09:50:55 +0100 Subject: [PATCH 23/37] fix typing for mypy --- src/mvt/android/artifacts/artifact.py | 42 +++++++++++++------ src/mvt/android/artifacts/dumpsys_adb.py | 2 +- src/mvt/android/artifacts/getprop.py | 3 +- src/mvt/android/cmd_check_androidqf.py | 2 + src/mvt/android/cmd_check_backup.py | 22 ++++++---- src/mvt/android/cmd_check_bugreport.py | 2 + src/mvt/android/modules/adb/chrome_history.py | 2 +- src/mvt/android/modules/adb/selinux_status.py | 5 ++- .../android/modules/androidqf/aqf_files.py | 2 +- .../android/modules/androidqf/aqf_getprop.py | 4 +- .../modules/androidqf/aqf_log_timestamps.py | 11 +++-- .../android/modules/androidqf/aqf_packages.py | 6 ++- .../android/modules/androidqf/aqf_settings.py | 4 +- src/mvt/android/modules/androidqf/base.py | 4 +- src/mvt/android/modules/androidqf/mounts.py | 4 +- src/mvt/android/modules/backup/base.py | 17 ++++---- src/mvt/android/modules/bugreport/base.py | 11 +++-- .../modules/bugreport/dumpsys_packages.py | 7 ++-- src/mvt/common/command.py | 2 +- src/mvt/common/config.py | 42 +++++++++---------- src/mvt/common/indicators.py | 12 +++--- src/mvt/common/module.py | 9 ++-- src/mvt/common/module_types.py | 20 +++++---- src/mvt/common/updates.py | 4 +- src/mvt/common/url.py | 18 +++++--- src/mvt/ios/decrypt.py | 6 ++- src/mvt/ios/modules/backup/backup_info.py | 4 +- .../modules/backup/configuration_profiles.py | 16 +++---- src/mvt/ios/modules/backup/manifest.py | 16 ++++--- src/mvt/ios/modules/backup/profile_events.py | 20 +++++---- src/mvt/ios/modules/base.py | 20 +++++++-- src/mvt/ios/modules/fs/analytics.py | 16 +++---- src/mvt/ios/modules/fs/cache_files.py | 5 ++- src/mvt/ios/modules/fs/safari_favicon.py | 9 ++-- src/mvt/ios/modules/fs/shutdownlog.py | 11 +++-- src/mvt/ios/modules/fs/version_history.py | 3 +- src/mvt/ios/modules/fs/webkit_base.py | 4 +- src/mvt/ios/modules/mixed/applications.py | 16 ++++--- src/mvt/ios/modules/mixed/calendar.py | 7 ++-- src/mvt/ios/modules/mixed/calls.py | 10 +++-- src/mvt/ios/modules/mixed/chrome_favicon.py | 10 +++-- src/mvt/ios/modules/mixed/chrome_history.py | 10 +++-- src/mvt/ios/modules/mixed/contacts.py | 3 ++ src/mvt/ios/modules/mixed/firefox_favicon.py | 10 +++-- src/mvt/ios/modules/mixed/firefox_history.py | 10 +++-- .../ios/modules/mixed/global_preferences.py | 6 ++- src/mvt/ios/modules/mixed/idstatuscache.py | 5 +-- src/mvt/ios/modules/mixed/interactionc.py | 6 ++- src/mvt/ios/modules/mixed/locationd.py | 11 ++--- .../ios/modules/mixed/osanalytics_addaily.py | 10 +++-- .../ios/modules/mixed/safari_browserstate.py | 11 +++-- src/mvt/ios/modules/mixed/safari_history.py | 10 +++-- src/mvt/ios/modules/mixed/shortcuts.py | 8 +++- src/mvt/ios/modules/mixed/sms.py | 10 ++++- src/mvt/ios/modules/mixed/sms_attachments.py | 10 ++--- .../mixed/webkit_resource_load_statistics.py | 8 ++-- .../mixed/webkit_session_resource_log.py | 13 +++--- src/mvt/ios/modules/mixed/whatsapp.py | 12 ++++-- src/mvt/ios/versions.py | 6 ++- 59 files changed, 365 insertions(+), 224 deletions(-) diff --git a/src/mvt/android/artifacts/artifact.py b/src/mvt/android/artifacts/artifact.py index a5df7b708..94ddddeaa 100644 --- a/src/mvt/android/artifacts/artifact.py +++ b/src/mvt/android/artifacts/artifact.py @@ -20,23 +20,39 @@ def extract_dumpsys_section( :param binary: whether the dumpsys should be pared as binary or not (bool) :return: section extracted (string or bytes) """ - lines = [] in_section = False - delimiter = "------------------------------------------------------------------------------" + delimiter_str = "------------------------------------------------------------------------------" + delimiter_bytes = b"------------------------------------------------------------------------------" + if binary: - delimiter = delimiter.encode("utf-8") + lines_bytes = [] + for line in dumpsys.splitlines(): # type: ignore[union-attr] + if line.strip() == separator: # type: ignore[arg-type] + in_section = True + continue + + if not in_section: + continue + + if line.strip().startswith(delimiter_bytes): # type: ignore[arg-type] + break + + lines_bytes.append(line) # type: ignore[arg-type] - for line in dumpsys.splitlines(): - if line.strip() == separator: - in_section = True - continue + return b"\n".join(lines_bytes) # type: ignore[return-value,arg-type] + else: + lines_str = [] + for line in dumpsys.splitlines(): # type: ignore[union-attr] + if line.strip() == separator: # type: ignore[arg-type] + in_section = True + continue - if not in_section: - continue + if not in_section: + continue - if line.strip().startswith(delimiter): - break + if line.strip().startswith(delimiter_str): # type: ignore[arg-type] + break - lines.append(line) + lines_str.append(line) # type: ignore[arg-type] - return b"\n".join(lines) if binary else "\n".join(lines) + return "\n".join(lines_str) # type: ignore[return-value,arg-type] diff --git a/src/mvt/android/artifacts/dumpsys_adb.py b/src/mvt/android/artifacts/dumpsys_adb.py index 2bc9abe47..4ed166999 100644 --- a/src/mvt/android/artifacts/dumpsys_adb.py +++ b/src/mvt/android/artifacts/dumpsys_adb.py @@ -84,7 +84,7 @@ def parse_xml(self, xml_data): return keystore @staticmethod - def calculate_key_info(user_key: bytes) -> str: + def calculate_key_info(user_key: bytes) -> dict: if b" " in user_key: key_base64, user = user_key.split(b" ", 1) else: diff --git a/src/mvt/android/artifacts/getprop.py b/src/mvt/android/artifacts/getprop.py index 71e4993fe..debcfc4cd 100644 --- a/src/mvt/android/artifacts/getprop.py +++ b/src/mvt/android/artifacts/getprop.py @@ -60,7 +60,8 @@ def check_indicators(self) -> None: if entry["name"] == "ro.build.version.security_patch": warning_message = warn_android_patch_level(entry["value"], self.log) - self.alertstore.medium(warning_message, "", entry) + if isinstance(warning_message, str): + self.alertstore.medium(warning_message, "", entry) if not self.indicators: return diff --git a/src/mvt/android/cmd_check_androidqf.py b/src/mvt/android/cmd_check_androidqf.py index 0427b1d58..ff96a240f 100644 --- a/src/mvt/android/cmd_check_androidqf.py +++ b/src/mvt/android/cmd_check_androidqf.py @@ -167,6 +167,8 @@ def run_bugreport_cmd(self) -> bool: if bugreport: bugreport.close() + return True + def run_backup_cmd(self) -> bool: try: backup = self.load_backup() diff --git a/src/mvt/android/cmd_check_backup.py b/src/mvt/android/cmd_check_backup.py index 8fc4d7161..c6fe9224c 100644 --- a/src/mvt/android/cmd_check_backup.py +++ b/src/mvt/android/cmd_check_backup.py @@ -9,7 +9,7 @@ import sys import tarfile from pathlib import Path -from typing import List, Optional +from typing import List, Optional, cast from mvt.android.modules.backup.base import BackupModule from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password @@ -93,22 +93,28 @@ def from_ab(self, ab_file_bytes: bytes) -> None: self.__files.append(member.name) def init(self) -> None: - if not self.target_path: + if not self.target_path: # type: ignore[has-type] return - if os.path.isfile(self.target_path): + # Type guard: we know it's not None here after the check above + assert self.target_path is not None # type: ignore[has-type] + # Use a different local variable name to avoid any scoping issues + backup_path: str = self.target_path # type: ignore[has-type] + + if os.path.isfile(backup_path): self.__type = "ab" - with open(self.target_path, "rb") as handle: + with open(backup_path, "rb") as handle: ab_file_bytes = handle.read() self.from_ab(ab_file_bytes) - elif os.path.isdir(self.target_path): + elif os.path.isdir(backup_path): self.__type = "folder" - self.target_path = Path(self.target_path).absolute().as_posix() - for root, subdirs, subfiles in os.walk(os.path.abspath(self.target_path)): + backup_path = Path(backup_path).absolute().as_posix() + self.target_path = backup_path + for root, subdirs, subfiles in os.walk(os.path.abspath(backup_path)): for fname in subfiles: self.__files.append( - os.path.relpath(os.path.join(root, fname), self.target_path) + os.path.relpath(os.path.join(root, fname), backup_path) ) else: log.critical( diff --git a/src/mvt/android/cmd_check_bugreport.py b/src/mvt/android/cmd_check_bugreport.py index 7cc827fe6..b6f136747 100644 --- a/src/mvt/android/cmd_check_bugreport.py +++ b/src/mvt/android/cmd_check_bugreport.py @@ -96,6 +96,8 @@ def module_init(self, module: BugReportModule) -> None: # type: ignore[override if self.__format == "zip": module.from_zip(self.__zip, self.__files) else: + if not self.target_path: + raise ValueError("target_path is not set") module.from_dir(self.target_path, self.__files) def finish(self) -> None: diff --git a/src/mvt/android/modules/adb/chrome_history.py b/src/mvt/android/modules/adb/chrome_history.py index 2be1d330a..16a516494 100644 --- a/src/mvt/android/modules/adb/chrome_history.py +++ b/src/mvt/android/modules/adb/chrome_history.py @@ -40,7 +40,7 @@ def __init__( log=log, results=results, ) - self.results = [] + self.results: list = [] def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { diff --git a/src/mvt/android/modules/adb/selinux_status.py b/src/mvt/android/modules/adb/selinux_status.py index 39925c756..1f15dbc5e 100644 --- a/src/mvt/android/modules/adb/selinux_status.py +++ b/src/mvt/android/modules/adb/selinux_status.py @@ -6,9 +6,10 @@ import logging from typing import Optional -from .base import AndroidExtraction from mvt.common.module_types import ModuleResults +from .base import AndroidExtraction + class SELinuxStatus(AndroidExtraction): """This module checks if SELinux is being enforced.""" @@ -33,7 +34,7 @@ def __init__( results=results, ) - self.results = {} if not results else results + self.results: dict = {} def run(self) -> None: self._adb_connect() diff --git a/src/mvt/android/modules/androidqf/aqf_files.py b/src/mvt/android/modules/androidqf/aqf_files.py index 35a848dae..941de9f2f 100644 --- a/src/mvt/android/modules/androidqf/aqf_files.py +++ b/src/mvt/android/modules/androidqf/aqf_files.py @@ -112,7 +112,7 @@ def check_indicators(self) -> None: if result.get("sha256", "") == "": continue - ioc_match = self.indicators.check_file_hash(result.get("sha256")) + ioc_match = self.indicators.check_file_hash(result.get("sha256") or "") if ioc_match: self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc diff --git a/src/mvt/android/modules/androidqf/aqf_getprop.py b/src/mvt/android/modules/androidqf/aqf_getprop.py index 68dca906d..f41029ba8 100644 --- a/src/mvt/android/modules/androidqf/aqf_getprop.py +++ b/src/mvt/android/modules/androidqf/aqf_getprop.py @@ -7,9 +7,9 @@ from typing import Optional from mvt.android.artifacts.getprop import GetProp as GetPropArtifact +from mvt.common.module_types import ModuleResults from .base import AndroidQFModule -from mvt.common.module_types import ModuleResults class AQFGetProp(GetPropArtifact, AndroidQFModule): @@ -32,7 +32,7 @@ def __init__( log=log, results=results, ) - self.results = [] + self.results: list = [] def run(self) -> None: getprop_files = self._get_files_by_pattern("*/getprop.txt") diff --git a/src/mvt/android/modules/androidqf/aqf_log_timestamps.py b/src/mvt/android/modules/androidqf/aqf_log_timestamps.py index fc4680451..1070d1bf5 100644 --- a/src/mvt/android/modules/androidqf/aqf_log_timestamps.py +++ b/src/mvt/android/modules/androidqf/aqf_log_timestamps.py @@ -3,15 +3,16 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ -import os import datetime import logging +import os from typing import Optional -from mvt.common.utils import convert_datetime_to_iso +from mvt.android.artifacts.file_timestamps import FileTimestampsArtifact from mvt.common.module_types import ModuleResults +from mvt.common.utils import convert_datetime_to_iso + from .base import AndroidQFModule -from mvt.android.artifacts.file_timestamps import FileTimestampsArtifact class AQFLogTimestamps(FileTimestampsArtifact, AndroidQFModule): @@ -37,11 +38,13 @@ def __init__( results=results, ) - def _get_file_modification_time(self, file_path: str) -> dict: + def _get_file_modification_time(self, file_path: str) -> datetime.datetime: if self.archive: file_timetuple = self.archive.getinfo(file_path).date_time return datetime.datetime(*file_timetuple) else: + if not self.parent_path: + raise ValueError("parent_path is not set") file_stat = os.stat(os.path.join(self.parent_path, file_path)) return datetime.datetime.fromtimestamp(file_stat.st_mtime) diff --git a/src/mvt/android/modules/androidqf/aqf_packages.py b/src/mvt/android/modules/androidqf/aqf_packages.py index 34ed0cfa8..6fa4ef5f1 100644 --- a/src/mvt/android/modules/androidqf/aqf_packages.py +++ b/src/mvt/android/modules/androidqf/aqf_packages.py @@ -98,7 +98,7 @@ def check_indicators(self) -> None: if not self.indicators: continue - ioc_match = self.indicators.check_app_id(result.get("name")) + ioc_match = self.indicators.check_app_id(result.get("name") or "") if ioc_match: self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc @@ -106,7 +106,9 @@ def check_indicators(self) -> None: self.alertstore.log_latest() for package_file in result.get("files", []): - ioc_match = self.indicators.check_file_hash(package_file["sha256"]) + ioc_match = self.indicators.check_file_hash( + package_file.get("sha256") or "" + ) if ioc_match: self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc diff --git a/src/mvt/android/modules/androidqf/aqf_settings.py b/src/mvt/android/modules/androidqf/aqf_settings.py index 1dc3b6eaf..2974932d6 100644 --- a/src/mvt/android/modules/androidqf/aqf_settings.py +++ b/src/mvt/android/modules/androidqf/aqf_settings.py @@ -7,9 +7,9 @@ from typing import Optional from mvt.android.artifacts.settings import Settings as SettingsArtifact +from mvt.common.module_types import ModuleResults from .base import AndroidQFModule -from mvt.common.module_types import ModuleResults class AQFSettings(SettingsArtifact, AndroidQFModule): @@ -32,7 +32,7 @@ def __init__( log=log, results=results, ) - self.results = {} + self.results: dict = {} def run(self) -> None: for setting_file in self._get_files_by_pattern("*/settings_*.txt"): diff --git a/src/mvt/android/modules/androidqf/base.py b/src/mvt/android/modules/androidqf/base.py index a784158f5..be898fced 100644 --- a/src/mvt/android/modules/androidqf/base.py +++ b/src/mvt/android/modules/androidqf/base.py @@ -33,8 +33,8 @@ def __init__( log=log, results=results, ) - self.parent_path = None - self._path: str = target_path + self.parent_path: Optional[str] = None + self._path: Optional[str] = target_path self.files: List[str] = [] self.archive: Optional[zipfile.ZipFile] = None diff --git a/src/mvt/android/modules/androidqf/mounts.py b/src/mvt/android/modules/androidqf/mounts.py index 1a5ba5cd1..fb1274ad8 100644 --- a/src/mvt/android/modules/androidqf/mounts.py +++ b/src/mvt/android/modules/androidqf/mounts.py @@ -3,8 +3,8 @@ # 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 import json +import logging from typing import Optional from mvt.android.artifacts.mounts import Mounts as MountsArtifact @@ -32,7 +32,7 @@ def __init__( log=log, results=results, ) - self.results = [] + self.results: list = [] def run(self) -> None: """ diff --git a/src/mvt/android/modules/backup/base.py b/src/mvt/android/modules/backup/base.py index 8da8a2001..d49a9456e 100644 --- a/src/mvt/android/modules/backup/base.py +++ b/src/mvt/android/modules/backup/base.py @@ -9,7 +9,7 @@ from tarfile import TarFile from typing import List, Optional -from mvt.common.module import MVTModule, ModuleResults +from mvt.common.module import ModuleResults, MVTModule class BackupModule(MVTModule): @@ -32,10 +32,10 @@ def __init__( log=log, results=results, ) - self.ab = None - self.backup_path = None - self.tar = None - self.files = [] + self.ab: Optional[str] = None + self.backup_path: Optional[str] = None + self.tar: Optional[TarFile] = None + self.files: list = [] def from_dir(self, backup_path: Optional[str], files: List[str]) -> None: self.backup_path = backup_path @@ -55,12 +55,15 @@ def _get_files_by_pattern(self, pattern: str) -> list: return fnmatch.filter(self.files, pattern) def _get_file_content(self, file_path: str) -> bytes: + handle = None if self.tar: try: member = self.tar.getmember(file_path) + handle = self.tar.extractfile(member) + if not handle: + raise ValueError(f"Could not extract file: {file_path}") except KeyError: - return None - handle = self.tar.extractfile(member) + raise FileNotFoundError(f"File not found in tar: {file_path}") elif self.backup_path: handle = open(os.path.join(self.backup_path, file_path), "rb") else: diff --git a/src/mvt/android/modules/bugreport/base.py b/src/mvt/android/modules/bugreport/base.py index 025e2c162..1240aed46 100644 --- a/src/mvt/android/modules/bugreport/base.py +++ b/src/mvt/android/modules/bugreport/base.py @@ -6,11 +6,10 @@ import fnmatch import logging import os - from typing import List, Optional from zipfile import ZipFile -from mvt.common.module import MVTModule, ModuleResults +from mvt.common.module import ModuleResults, MVTModule class BugReportModule(MVTModule): @@ -69,6 +68,8 @@ def _get_file_content(self, file_path: str) -> bytes: if self.zip_archive: handle = self.zip_archive.open(file_path) else: + if not self.extract_path: + raise ValueError("extract_path is not set") handle = open(os.path.join(self.extract_path, file_path), "rb") data = handle.read() @@ -76,7 +77,7 @@ def _get_file_content(self, file_path: str) -> bytes: return data - def _get_dumpstate_file(self) -> bytes: + def _get_dumpstate_file(self) -> Optional[bytes]: main = self._get_files_by_pattern("main_entry.txt") if main: main_content = self._get_file_content(main[0]) @@ -91,10 +92,12 @@ def _get_dumpstate_file(self) -> bytes: return self._get_file_content(dumpstate_logs[0]) - def _get_file_modification_time(self, file_path: str) -> dict: + def _get_file_modification_time(self, file_path: str) -> datetime.datetime: if self.zip_archive: file_timetuple = self.zip_archive.getinfo(file_path).date_time return datetime.datetime(*file_timetuple) else: + if not self.extract_path: + raise ValueError("extract_path is not set") file_stat = os.stat(os.path.join(self.extract_path, file_path)) return datetime.datetime.fromtimestamp(file_stat.st_mtime) diff --git a/src/mvt/android/modules/bugreport/dumpsys_packages.py b/src/mvt/android/modules/bugreport/dumpsys_packages.py index 0fb4713f1..6bc5d27d3 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_packages.py +++ b/src/mvt/android/modules/bugreport/dumpsys_packages.py @@ -6,9 +6,9 @@ import logging from typing import Optional -from mvt.common.module_types import ModuleResults from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact from mvt.android.utils import DANGEROUS_PERMISSIONS, DANGEROUS_PERMISSIONS_THRESHOLD +from mvt.common.module_types import ModuleResults from .base import BugReportModule @@ -43,8 +43,9 @@ def run(self) -> None: ) return - data = data.decode("utf-8", errors="replace") - content = self.extract_dumpsys_section(data, "DUMP OF SERVICE package:") + content = self.extract_dumpsys_section( + data.decode("utf-8", errors="replace"), "DUMP OF SERVICE package:" + ) self.parse(content) for result in self.results: diff --git a/src/mvt/common/command.py b/src/mvt/common/command.py index e79639184..c2b366430 100644 --- a/src/mvt/common/command.py +++ b/src/mvt/common/command.py @@ -154,7 +154,7 @@ def _store_info(self) -> None: if not self.results_path: return - target_path = None + target_path: Optional[str] = None if self.target_path: target_path = os.path.abspath(self.target_path) diff --git a/src/mvt/common/config.py b/src/mvt/common/config.py index d2e4e20c1..cecaac54a 100644 --- a/src/mvt/common/config.py +++ b/src/mvt/common/config.py @@ -1,8 +1,8 @@ -import os -import yaml import json +import os +from typing import Optional, Tuple, Type -from typing import Tuple, Type, Optional +import yaml from appdirs import user_config_dir from pydantic import AnyHttpUrl, Field from pydantic_settings import ( @@ -22,51 +22,51 @@ class MVTSettings(BaseSettings): env_prefix="MVT_", env_nested_delimiter="_", extra="ignore", - nested_model_default_partial_updates=True, ) # Allow to decided if want to load environment variables load_env: bool = Field(True, exclude=True) # General settings - PYPI_UPDATE_URL: AnyHttpUrl = Field( - "https://pypi.org/pypi/mvt/json", - validate_default=False, + PYPI_UPDATE_URL: str = Field( + default="https://pypi.org/pypi/mvt/json", ) NETWORK_ACCESS_ALLOWED: bool = True NETWORK_TIMEOUT: int = 15 # Command default settings, all can be specified by MVT_ prefixed environment variables too. IOS_BACKUP_PASSWORD: Optional[str] = Field( - None, description="Default password to use to decrypt iOS backups" + default=None, description="Default password to use to decrypt iOS backups" ) ANDROID_BACKUP_PASSWORD: Optional[str] = Field( - None, description="Default password to use to decrypt Android backups" + default=None, description="Default password to use to decrypt Android backups" ) STIX2: Optional[str] = Field( - None, description="List of directories where STIX2 files are stored" + default=None, description="List of directories where STIX2 files are stored" ) VT_API_KEY: Optional[str] = Field( - None, description="API key to use for VirusTotal lookups" + default=None, description="API key to use for VirusTotal lookups" + ) + PROFILE: bool = Field( + default=False, description="Profile the execution of MVT modules" ) - PROFILE: bool = Field(False, description="Profile the execution of MVT modules") - HASH_FILES: bool = Field(False, description="Should MVT hash output files") + HASH_FILES: bool = Field(default=False, description="Should MVT hash output files") @classmethod def settings_customise_sources( cls, settings_cls: Type[BaseSettings], - init_settings: InitSettingsSource, + init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> Tuple[PydanticBaseSettingsSource, ...]: - sources = ( - YamlConfigSettingsSource(settings_cls, MVT_CONFIG_PATH), + yaml_source = YamlConfigSettingsSource(settings_cls, MVT_CONFIG_PATH) + sources: Tuple[PydanticBaseSettingsSource, ...] = ( + yaml_source, init_settings, ) - # Load env variables if enabled - if init_settings.init_kwargs.get("load_env", True): - sources = (env_settings,) + sources + # Always load env variables by default + sources = (env_settings,) + sources return sources def save_settings( @@ -94,11 +94,11 @@ def initialise(cls) -> "MVTSettings": Afterwards we load the settings again, this time including the env variables. """ # Set invalid env prefix to avoid loading env variables. - settings = MVTSettings(load_env=False) + settings = cls(load_env=False) settings.save_settings() # Load the settings again with any ENV variables. - settings = MVTSettings(load_env=True) + settings = cls(load_env=True) return settings diff --git a/src/mvt/common/indicators.py b/src/mvt/common/indicators.py index c34418d4f..d17668852 100644 --- a/src/mvt/common/indicators.py +++ b/src/mvt/common/indicators.py @@ -7,15 +7,15 @@ import json import logging import os +from dataclasses import dataclass from functools import lru_cache from typing import Any, Dict, Iterator, List, Optional -from dataclasses import dataclass import ahocorasick from appdirs import user_data_dir -from .url import URL from .config import settings +from .url import URL MVT_DATA_FOLDER = user_data_dir("mvt") MVT_INDICATORS_FOLDER = os.path.join(MVT_DATA_FOLDER, "indicators") @@ -68,7 +68,7 @@ def _check_stix2_env_variable(self) -> None: self.parse_stix2(path) elif os.path.isdir(path): for file in glob.glob( - os.path.join(path, "**", "*.stix2", recursive=True) + os.path.join(path, "**", "*.stix2"), recursive=True ): self.parse_stix2(file) else: @@ -350,7 +350,7 @@ def get_iocs(self, ioc_type: str) -> Iterator[Indicator]: @lru_cache() def get_ioc_matcher( - self, ioc_type: Optional[str] = None, ioc_list: Optional[list] = None + self, ioc_type: Optional[str] = None, ioc_list: Optional[List[Indicator]] = None ) -> ahocorasick.Automaton: """ Build an Aho-Corasick automaton from a list of iocs (i.e indicators) @@ -370,9 +370,9 @@ def get_ioc_matcher( """ automaton = ahocorasick.Automaton() if ioc_type: - iocs = self.get_iocs(ioc_type) + iocs: Iterator[Indicator] = self.get_iocs(ioc_type) elif ioc_list: - iocs = ioc_list + iocs = iter(ioc_list) else: raise ValueError("Must provide either ioc_type or ioc_list") diff --git a/src/mvt/common/module.py b/src/mvt/common/module.py index d5a76288d..ce8652fa4 100644 --- a/src/mvt/common/module.py +++ b/src/mvt/common/module.py @@ -146,7 +146,10 @@ def _deduplicate_timeline(timeline: list) -> list: for record in timeline: timeline_set.add( json.dumps( - asdict(record) if is_dataclass(record) else record, sort_keys=True + asdict(record) + if is_dataclass(record) and not isinstance(record, type) + else record, + sort_keys=True, ) ) @@ -161,9 +164,9 @@ def to_timeline(self) -> None: record: ModuleSerializedResult = self.serialize(result) if record: if isinstance(record, list): - self.timeline.extend(record) + self.timeline.extend(record) # type: ignore[arg-type] else: - self.timeline.append(record) + self.timeline.append(record) # type: ignore[arg-type] # De-duplicate timeline entries. self.timeline = self._deduplicate_timeline(self.timeline) diff --git a/src/mvt/common/module_types.py b/src/mvt/common/module_types.py index f433482de..9b924549b 100644 --- a/src/mvt/common/module_types.py +++ b/src/mvt/common/module_types.py @@ -3,15 +3,18 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ -from .indicators import Indicator from dataclasses import dataclass -from typing import List, Union, Optional +from typing import Any, Dict, List, Optional, Union +from .indicators import Indicator -@dataclass -class ModuleAtomicResult: - timestamp: Optional[str] - matched_indicator: Optional[Indicator] +# ModuleAtomicResult is a flexible dictionary that can contain any data. +# Common fields include: +# - timestamp: Optional[str] - timestamp string +# - isodate: Optional[str] - ISO formatted date string +# - matched_indicator: Optional[Indicator] - indicator that matched this result +# - Any other module-specific fields +ModuleAtomicResult = Dict[str, Any] ModuleResults = List[ModuleAtomicResult] @@ -26,4 +29,7 @@ class ModuleAtomicTimeline: ModuleTimeline = List[ModuleAtomicTimeline] -ModuleSerializedResult = Union[ModuleAtomicTimeline, ModuleTimeline] +# ModuleSerializedResult can be a proper timeline object or a plain dict for compatibility +ModuleSerializedResult = Union[ + ModuleAtomicTimeline, ModuleTimeline, Dict[str, Any], List[Dict[str, Any]] +] diff --git a/src/mvt/common/updates.py b/src/mvt/common/updates.py index c9c380bb3..c95f9ceb8 100644 --- a/src/mvt/common/updates.py +++ b/src/mvt/common/updates.py @@ -12,9 +12,9 @@ import yaml from packaging import version +from .config import settings from .indicators import MVT_DATA_FOLDER, MVT_INDICATORS_FOLDER from .version import MVT_VERSION -from .config import settings log = logging.getLogger(__name__) @@ -25,7 +25,7 @@ class MVTUpdates: def check(self) -> str: try: - res = requests.get(settings.PYPI_UPDATE_URL, timeout=5) + res = requests.get(str(settings.PYPI_UPDATE_URL), timeout=5) except requests.exceptions.RequestException as e: log.error("Failed to check for updates, skipping updates: %s", e) return "" diff --git a/src/mvt/common/url.py b/src/mvt/common/url.py index 40240c5b7..a83b53b5d 100644 --- a/src/mvt/common/url.py +++ b/src/mvt/common/url.py @@ -338,11 +338,12 @@ def get_domain(self) -> str: :rtype: str """ - return ( - get_tld(self.url, as_object=True, fix_protocol=True) - .parsed_url.netloc.lower() - .lstrip("www.") - ) + tld_obj = get_tld(self.url, as_object=True, fix_protocol=True) + if isinstance(tld_obj, str): + return tld_obj + if tld_obj is None: + return "" + return tld_obj.parsed_url.netloc.lower().lstrip("www.") def get_top_level(self) -> str: """Get only the top-level domain from a URL. @@ -351,7 +352,12 @@ def get_top_level(self) -> str: :rtype: str """ - return get_tld(self.url, as_object=True, fix_protocol=True).fld.lower() + tld_obj = get_tld(self.url, as_object=True, fix_protocol=True) + if isinstance(tld_obj, str): + return tld_obj + if tld_obj is None: + return "" + return tld_obj.fld.lower() def check_if_shortened(self) -> bool: """Check if the URL is among list of shortener services. diff --git a/src/mvt/ios/decrypt.py b/src/mvt/ios/decrypt.py index ffb2cb757..607725491 100644 --- a/src/mvt/ios/decrypt.py +++ b/src/mvt/ios/decrypt.py @@ -58,6 +58,7 @@ def is_encrypted(backup_path: str) -> bool: def _process_file( self, relative_path: str, domain: str, item, file_id: str, item_folder: str ) -> None: + assert self._backup is not None self._backup.getFileDecryptedCopy( manifestEntry=item, targetName=file_id, targetFolder=item_folder ) @@ -70,6 +71,9 @@ def _process_file( ) def process_backup(self) -> None: + assert self._backup is not None + assert self.dest_path is not None + if not os.path.exists(self.dest_path): os.makedirs(self.dest_path) @@ -97,7 +101,7 @@ def process_backup(self) -> None: ) continue - item_folder = os.path.join(self.dest_path, file_id[0:2]) + item_folder = os.path.join(self.dest_path, file_id[0:2]) # type: ignore[arg-type] if not os.path.exists(item_folder): os.makedirs(item_folder) diff --git a/src/mvt/ios/modules/backup/backup_info.py b/src/mvt/ios/modules/backup/backup_info.py index 07baa2f6a..9e0720086 100644 --- a/src/mvt/ios/modules/backup/backup_info.py +++ b/src/mvt/ios/modules/backup/backup_info.py @@ -36,9 +36,11 @@ def __init__( results=results, ) - self.results = {} + self.results: dict = {} def run(self) -> None: + if not self.target_path: + raise DatabaseNotFoundError("target_path is not set") info_path = os.path.join(self.target_path, "Info.plist") if not os.path.exists(info_path): raise DatabaseNotFoundError( diff --git a/src/mvt/ios/modules/backup/configuration_profiles.py b/src/mvt/ios/modules/backup/configuration_profiles.py index 306ea4f0a..7aa635b09 100644 --- a/src/mvt/ios/modules/backup/configuration_profiles.py +++ b/src/mvt/ios/modules/backup/configuration_profiles.py @@ -9,12 +9,12 @@ from base64 import b64encode from typing import Optional -from mvt.common.utils import convert_datetime_to_iso from mvt.common.module_types import ( ModuleAtomicResult, - ModuleSerializedResult, ModuleResults, + ModuleSerializedResult, ) +from mvt.common.utils import convert_datetime_to_iso from ..base import IOSExtraction @@ -72,12 +72,10 @@ def check_indicators(self) -> None: result["plist"]["PayloadUUID"] ) if ioc_match: - warning_message = ( - f'Found a known malicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with UUID "{result["plist"]["PayloadUUID"]}"', - ) + warning_message = f'Found a known malicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with UUID "{result["plist"]["PayloadUUID"]}"' result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( - self.get_slug(), warning_message, "", result + warning_message, "", result, matched_indicator=ioc_match.ioc ) self.alertstore.log_latest() continue @@ -85,10 +83,8 @@ def check_indicators(self) -> None: # Highlight suspicious configuration profiles which may be used # to hide notifications. if payload_content["PayloadType"] in ["com.apple.notificationsettings"]: - warning_message = ( - f'Found a potentially suspicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with payload type {payload_content["PayloadType"]}', - ) - self.alertstore.medum(self.get_slug(), warning_message, "", result) + warning_message = f'Found a potentially suspicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with payload type {payload_content["PayloadType"]}' + self.alertstore.medium(warning_message, "", result) self.alertstore.log_latest() continue diff --git a/src/mvt/ios/modules/backup/manifest.py b/src/mvt/ios/modules/backup/manifest.py index 099a682ad..4673b2527 100644 --- a/src/mvt/ios/modules/backup/manifest.py +++ b/src/mvt/ios/modules/backup/manifest.py @@ -11,13 +11,13 @@ from typing import Optional from mvt.common.module import DatabaseNotFoundError -from mvt.common.url import URL -from mvt.common.utils import convert_datetime_to_iso, convert_unix_to_iso from mvt.common.module_types import ( - ModuleResults, ModuleAtomicResult, + ModuleResults, ModuleSerializedResult, ) +from mvt.common.url import URL +from mvt.common.utils import convert_datetime_to_iso, convert_unix_to_iso from ..base import IOSExtraction @@ -66,7 +66,7 @@ def _convert_timestamp(timestamp_or_unix_time_int): return convert_unix_to_iso(timestamp_or_unix_time_int) def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: - records = [] + records: list = [] if "modified" not in record or "status_changed" not in record: return records @@ -103,7 +103,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_file_path("/" + result["relative_path"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.high(self.get_slug(), ioc_match.message, "", result) + self.alertstore.high( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue rel_path = result["relative_path"].lower() @@ -118,13 +120,15 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.high( - self.get_slug(), f'Found mention of domain "{ioc_match.ioc.value}" in a backup file with path: {rel_path}', "", result, + matched_indicator=ioc_match.ioc, ) def run(self) -> None: + if not self.target_path: + raise DatabaseNotFoundError("target_path is not set") manifest_db_path = os.path.join(self.target_path, "Manifest.db") if not os.path.isfile(manifest_db_path): raise DatabaseNotFoundError("unable to find backup's Manifest.db") diff --git a/src/mvt/ios/modules/backup/profile_events.py b/src/mvt/ios/modules/backup/profile_events.py index 98c87b160..83d82af9b 100644 --- a/src/mvt/ios/modules/backup/profile_events.py +++ b/src/mvt/ios/modules/backup/profile_events.py @@ -7,12 +7,12 @@ import plistlib from typing import Optional -from mvt.common.utils import convert_datetime_to_iso from mvt.common.module_types import ( ModuleAtomicResult, ModuleResults, ModuleSerializedResult, ) +from mvt.common.utils import convert_datetime_to_iso from ..base import IOSExtraction @@ -58,29 +58,31 @@ def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: def check_indicators(self) -> None: for result in self.results: message = f'On {result.get("timestamp")} process "{result.get("process")}" started operation "{result.get("operation")}" of profile "{result.get("profile_id")}"' - self.alertstore.low( - self.get_slug(), message, result.get("timestamp"), result - ) + self.alertstore.low(message, result.get("timestamp") or "", result) self.alertstore.log_latest() if not self.indicators: return for result in self.results: - ioc_match = self.indicators.check_process(result.get("process")) + ioc_match = self.indicators.check_process(result.get("process") or "") if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue - ioc_match = self.indicators.check_profile(result.get("profile_id")) + ioc_match = self.indicators.check_profile(result.get("profile_id") or "") if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) @staticmethod def parse_profile_events(file_data: bytes) -> list: - results = [] + results: list = [] events_plist = plistlib.loads(file_data) diff --git a/src/mvt/ios/modules/base.py b/src/mvt/ios/modules/base.py index 1a4861bf0..72260326b 100644 --- a/src/mvt/ios/modules/base.py +++ b/src/mvt/ios/modules/base.py @@ -11,8 +11,12 @@ import subprocess from typing import Iterator, Optional, Union -from mvt.common.module import DatabaseCorruptedError, DatabaseNotFoundError -from mvt.common.module import MVTModule, ModuleResults +from mvt.common.module import ( + DatabaseCorruptedError, + DatabaseNotFoundError, + ModuleResults, + MVTModule, +) class IOSExtraction(MVTModule): @@ -110,6 +114,8 @@ def _get_backup_files_from_manifest( (Default value = None) """ + if not self.target_path: + raise DatabaseNotFoundError("target_path is not set") manifest_db_path = os.path.join(self.target_path, "Manifest.db") if not os.path.exists(manifest_db_path): raise DatabaseNotFoundError("unable to find backup's Manifest.db") @@ -146,6 +152,8 @@ def _get_backup_files_from_manifest( } def _get_backup_file_from_id(self, file_id: str) -> Union[str, None]: + if not self.target_path: + return None file_path = os.path.join(self.target_path, file_id[0:2], file_id) if os.path.exists(file_path): return file_path @@ -153,6 +161,8 @@ def _get_backup_file_from_id(self, file_id: str) -> Union[str, None]: return None def _get_fs_files_from_patterns(self, root_paths: list) -> Iterator[str]: + if not self.target_path: + return for root_path in root_paths: for found_path in glob.glob(os.path.join(self.target_path, root_path)): if not os.path.exists(found_path): @@ -174,9 +184,10 @@ def _find_ios_database( :param backup_ids: Default value = None) """ - file_path = None + file_path: Optional[str] = None # First we check if the was an explicit file path specified. if not self.file_path: + # Type narrowing: we know self.file_path is None here, work with local file_path # If not, we first try with backups. # We construct the path to the file according to the iTunes backup # folder structure, if we have a valid ID. @@ -198,8 +209,9 @@ def _find_ios_database( # If we do not find any, we fail. if file_path: - self.file_path = file_path + self.file_path = file_path # type: str else: raise DatabaseNotFoundError("unable to find the module's database file") + assert self.file_path is not None self._recover_sqlite_db_if_needed(self.file_path) diff --git a/src/mvt/ios/modules/fs/analytics.py b/src/mvt/ios/modules/fs/analytics.py index b5cba0d5a..1d6d96192 100644 --- a/src/mvt/ios/modules/fs/analytics.py +++ b/src/mvt/ios/modules/fs/analytics.py @@ -9,12 +9,12 @@ import sqlite3 from typing import Optional -from mvt.common.utils import convert_mactime_to_iso from mvt.common.module_types import ( + ModuleAtomicResult, ModuleResults, ModuleSerializedResult, - ModuleAtomicResult, ) +from mvt.common.utils import convert_mactime_to_iso from ..base import IOSExtraction @@ -44,6 +44,7 @@ def __init__( log=log, results=results, ) + self.results: list = [] def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { @@ -64,13 +65,11 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_process(value) if ioc_match: - warning_message = ( - f'Found mention of a malicious process "{value}" in {result["artifact"]} file at {result["isodate"]}', - ) + warning_message = f'Found mention of a malicious process "{value}" in {result["artifact"]} file at {result["isodate"]}' new_result = copy.copy(result) new_result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( - self.get_slug(), warning_message, "", new_result + warning_message, "", new_result, matched_indicator=ioc_match.ioc ) self.alertstore.log_latest() continue @@ -80,7 +79,10 @@ def check_indicators(self) -> None: new_result = copy.copy(result) result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( - self.get_slug(), ioc_match.message, "", new_result + ioc_match.message, + "", + new_result, + matched_indicator=ioc_match.ioc, ) def _extract_analytics_data(self): diff --git a/src/mvt/ios/modules/fs/cache_files.py b/src/mvt/ios/modules/fs/cache_files.py index fa34b08f6..a8fdfe651 100644 --- a/src/mvt/ios/modules/fs/cache_files.py +++ b/src/mvt/ios/modules/fs/cache_files.py @@ -10,9 +10,10 @@ from mvt.common.module_types import ( ModuleAtomicResult, - ModuleSerializedResult, ModuleResults, + ModuleSerializedResult, ) + from ..base import IOSExtraction @@ -95,7 +96,7 @@ def _process_cache_file(self, file_path): ) def run(self) -> None: - self.results = {} + self.results: dict = {} for root, _, files in os.walk(self.target_path): for file_name in files: if file_name != "Cache.db": diff --git a/src/mvt/ios/modules/fs/safari_favicon.py b/src/mvt/ios/modules/fs/safari_favicon.py index c7579fb13..cffb26b94 100644 --- a/src/mvt/ios/modules/fs/safari_favicon.py +++ b/src/mvt/ios/modules/fs/safari_favicon.py @@ -6,12 +6,12 @@ import logging from typing import Optional -from mvt.common.utils import convert_mactime_to_iso from mvt.common.module_types import ( + ModuleAtomicResult, ModuleResults, ModuleSerializedResult, - ModuleAtomicResult, ) +from mvt.common.utils import convert_mactime_to_iso from ..base import IOSExtraction @@ -41,6 +41,7 @@ def __init__( log=log, results=results, ) + self.results: list = [] def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { @@ -61,7 +62,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_url(result["icon_url"]) if ioc_match: - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) self.alertstore.log_latest() def _process_favicon_db(self, file_path): diff --git a/src/mvt/ios/modules/fs/shutdownlog.py b/src/mvt/ios/modules/fs/shutdownlog.py index ad8fcd967..242617163 100644 --- a/src/mvt/ios/modules/fs/shutdownlog.py +++ b/src/mvt/ios/modules/fs/shutdownlog.py @@ -6,12 +6,12 @@ import logging from typing import Optional -from mvt.common.utils import convert_mactime_to_iso from mvt.common.module_types import ( ModuleAtomicResult, ModuleResults, ModuleSerializedResult, ) +from mvt.common.utils import convert_mactime_to_iso from ..base import IOSExtraction @@ -57,7 +57,9 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_file_path(result["client"]) if ioc_match: - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) self.alertstore.log_latest() continue @@ -66,10 +68,10 @@ def check_indicators(self) -> None: if ioc.value in parts: result["matched_indicator"] = ioc self.alertstore.critical( - self.get_slug(), f'Found mention of a known malicious process "{ioc.value}" in shutdown.log', "", result, + matched_indicator=ioc, ) self.alertstore.log_latest() continue @@ -135,5 +137,8 @@ def process_shutdownlog(self, content): def run(self) -> None: self._find_ios_database(root_paths=SHUTDOWN_LOG_PATH) self.log.info("Found shutdown log at path: %s", self.file_path) + + if not self.file_path: + return with open(self.file_path, "r", encoding="utf-8") as handle: self.process_shutdownlog(handle.read()) diff --git a/src/mvt/ios/modules/fs/version_history.py b/src/mvt/ios/modules/fs/version_history.py index 8c0af0f46..bd5515c49 100644 --- a/src/mvt/ios/modules/fs/version_history.py +++ b/src/mvt/ios/modules/fs/version_history.py @@ -8,12 +8,12 @@ import logging from typing import Optional -from mvt.common.utils import convert_datetime_to_iso from mvt.common.module_types import ( ModuleAtomicResult, ModuleResults, ModuleSerializedResult, ) +from mvt.common.utils import convert_datetime_to_iso from ..base import IOSExtraction @@ -42,6 +42,7 @@ def __init__( log=log, results=results, ) + self.results: list = [] def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: return { diff --git a/src/mvt/ios/modules/fs/webkit_base.py b/src/mvt/ios/modules/fs/webkit_base.py index 00d4c192d..6d72d8e10 100644 --- a/src/mvt/ios/modules/fs/webkit_base.py +++ b/src/mvt/ios/modules/fs/webkit_base.py @@ -21,7 +21,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_url(result["url"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue def _process_webkit_folder(self, root_paths): diff --git a/src/mvt/ios/modules/mixed/applications.py b/src/mvt/ios/modules/mixed/applications.py index 558430a21..bbbab7680 100644 --- a/src/mvt/ios/modules/mixed/applications.py +++ b/src/mvt/ios/modules/mixed/applications.py @@ -11,10 +11,14 @@ from typing import Any, Dict, Optional from mvt.common.module import DatabaseNotFoundError +from mvt.common.module_types import ( + ModuleAtomicResult, + ModuleResults, + ModuleSerializedResult, +) from mvt.common.utils import convert_datetime_to_iso -from mvt.ios.modules.base import IOSExtraction -from mvt.common.module import ModuleResults, ModuleAtomicResult, ModuleSerializedResult +from ..base import IOSExtraction APPLICATIONS_DB_PATH = [ "private/var/containers/Bundle/Application/*/iTunesMetadata.plist" @@ -63,7 +67,6 @@ def check_indicators(self) -> None: if self.indicators: if "softwareVersionBundleId" not in result: self.alertstore.high( - self.get_slug(), "Suspicious application identified without softwareVersionBundleId", "", result, @@ -76,10 +79,10 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( - self.get_slug(), f"Malicious application {result['softwareVersionBundleId']} identified", "", result, + matched_indicator=ioc_match.ioc, ) continue @@ -89,10 +92,10 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( - self.get_slug(), f"Malicious application {result['softwareVersionBundleId']} identified", "", result, + matched_indicator=ioc_match.ioc, ) continue @@ -102,7 +105,6 @@ def check_indicators(self) -> None: not in KNOWN_APP_INSTALLERS ): self.alertstore.medium( - self.get_slug(), f"Suspicious app not installed from the App Store or MDM: {result['softwareVersionBundleId']}", "", result, @@ -157,6 +159,8 @@ def _parse_info_plist(self, plist_path: str) -> None: def run(self) -> None: if self.is_backup: + if not self.target_path: + return plist_path = os.path.join(self.target_path, "Info.plist") if not os.path.isfile(plist_path): raise DatabaseNotFoundError("Impossible to find Info.plist file") diff --git a/src/mvt/ios/modules/mixed/calendar.py b/src/mvt/ios/modules/mixed/calendar.py index 6c111862a..75a811d30 100644 --- a/src/mvt/ios/modules/mixed/calendar.py +++ b/src/mvt/ios/modules/mixed/calendar.py @@ -6,12 +6,12 @@ import logging from typing import Optional -from mvt.common.utils import convert_mactime_to_iso from mvt.common.module_types import ( + ModuleAtomicResult, ModuleResults, ModuleSerializedResult, - ModuleAtomicResult, ) +from mvt.common.utils import convert_mactime_to_iso from ..base import IOSExtraction @@ -73,14 +73,13 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( - self.get_slug(), ioc_match.message, "", result + ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) continue # Custom check for Quadream exploit if result["summary"] == "Meeting" and result["description"] == "Notes": self.alertstore.high( - self.get_slug(), f"Potential Quadream exploit event identified: {result['uuid']}", "", result, diff --git a/src/mvt/ios/modules/mixed/calls.py b/src/mvt/ios/modules/mixed/calls.py index 82debbae9..4d411bf69 100644 --- a/src/mvt/ios/modules/mixed/calls.py +++ b/src/mvt/ios/modules/mixed/calls.py @@ -6,8 +6,8 @@ import logging from typing import Optional -from mvt.common.utils import convert_mactime_to_iso from mvt.common.module_types import ModuleAtomicResult, ModuleSerializedResult +from mvt.common.utils import convert_mactime_to_iso from ..base import IOSExtraction @@ -22,9 +22,9 @@ class Calls(IOSExtraction): def __init__( self, - file_path: str = None, - target_path: str = None, - results_path: str = None, + file_path: Optional[str] = None, + target_path: Optional[str] = None, + results_path: Optional[str] = None, module_options: Optional[dict] = None, log: logging.Logger = logging.getLogger(__name__), results: list = [], @@ -53,6 +53,8 @@ def run(self) -> None: ) self.log.info("Found Calls database at path: %s", self.file_path) + if not self.file_path: + return conn = self._open_sqlite_db(self.file_path) cur = conn.cursor() cur.execute( diff --git a/src/mvt/ios/modules/mixed/chrome_favicon.py b/src/mvt/ios/modules/mixed/chrome_favicon.py index 00d0b6ceb..cc44a680a 100644 --- a/src/mvt/ios/modules/mixed/chrome_favicon.py +++ b/src/mvt/ios/modules/mixed/chrome_favicon.py @@ -6,12 +6,12 @@ import logging from typing import Optional -from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso from mvt.common.module_types import ( + ModuleAtomicResult, ModuleResults, ModuleSerializedResult, - ModuleAtomicResult, ) +from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso from ..base import IOSExtraction @@ -62,7 +62,9 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) continue def run(self) -> None: @@ -71,6 +73,8 @@ def run(self) -> None: ) self.log.info("Found Chrome favicon cache database at path: %s", self.file_path) + if not self.file_path: + return conn = self._open_sqlite_db(self.file_path) # Fetch icon cache diff --git a/src/mvt/ios/modules/mixed/chrome_history.py b/src/mvt/ios/modules/mixed/chrome_history.py index 012b703c2..fa3b88f92 100644 --- a/src/mvt/ios/modules/mixed/chrome_history.py +++ b/src/mvt/ios/modules/mixed/chrome_history.py @@ -6,12 +6,12 @@ import logging from typing import Optional -from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso from mvt.common.module_types import ( - ModuleResults, ModuleAtomicResult, + ModuleResults, ModuleSerializedResult, ) +from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso from ..base import IOSExtraction @@ -63,7 +63,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_url(result["url"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) def run(self) -> None: self._find_ios_database( @@ -71,6 +73,8 @@ def run(self) -> None: ) self.log.info("Found Chrome history database at path: %s", self.file_path) + if not self.file_path: + return conn = self._open_sqlite_db(self.file_path) cur = conn.cursor() cur.execute( diff --git a/src/mvt/ios/modules/mixed/contacts.py b/src/mvt/ios/modules/mixed/contacts.py index 2bd5bee29..1966ac15b 100644 --- a/src/mvt/ios/modules/mixed/contacts.py +++ b/src/mvt/ios/modules/mixed/contacts.py @@ -8,6 +8,7 @@ from typing import Optional from mvt.common.module_types import ModuleResults + from ..base import IOSExtraction CONTACTS_BACKUP_IDS = [ @@ -45,6 +46,8 @@ def run(self) -> None: ) self.log.info("Found Contacts database at path: %s", self.file_path) + if not self.file_path: + return conn = self._open_sqlite_db(self.file_path) cur = conn.cursor() try: diff --git a/src/mvt/ios/modules/mixed/firefox_favicon.py b/src/mvt/ios/modules/mixed/firefox_favicon.py index 048911484..3357a59d7 100644 --- a/src/mvt/ios/modules/mixed/firefox_favicon.py +++ b/src/mvt/ios/modules/mixed/firefox_favicon.py @@ -6,12 +6,12 @@ import logging from typing import Optional -from mvt.common.utils import convert_unix_to_iso from mvt.common.module_types import ( + ModuleAtomicResult, ModuleResults, ModuleSerializedResult, - ModuleAtomicResult, ) +from mvt.common.utils import convert_unix_to_iso from ..base import IOSExtraction @@ -64,7 +64,9 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) def run(self) -> None: self._find_ios_database( @@ -72,6 +74,8 @@ def run(self) -> None: ) self.log.info("Found Firefox favicon database at path: %s", self.file_path) + if not self.file_path: + return conn = self._open_sqlite_db(self.file_path) cur = conn.cursor() cur.execute( diff --git a/src/mvt/ios/modules/mixed/firefox_history.py b/src/mvt/ios/modules/mixed/firefox_history.py index 67adda5d4..d61f19cb4 100644 --- a/src/mvt/ios/modules/mixed/firefox_history.py +++ b/src/mvt/ios/modules/mixed/firefox_history.py @@ -6,12 +6,12 @@ import logging from typing import Optional -from mvt.common.utils import convert_unix_to_iso from mvt.common.module_types import ( + ModuleAtomicResult, ModuleResults, ModuleSerializedResult, - ModuleAtomicResult, ) +from mvt.common.utils import convert_unix_to_iso from ..base import IOSExtraction @@ -64,7 +64,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_url(result["url"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) def run(self) -> None: self._find_ios_database( @@ -72,6 +74,8 @@ def run(self) -> None: ) self.log.info("Found Firefox history database at path: %s", self.file_path) + if not self.file_path: + return conn = self._open_sqlite_db(self.file_path) cur = conn.cursor() cur.execute( diff --git a/src/mvt/ios/modules/mixed/global_preferences.py b/src/mvt/ios/modules/mixed/global_preferences.py index 9f11db970..04b11bcb1 100644 --- a/src/mvt/ios/modules/mixed/global_preferences.py +++ b/src/mvt/ios/modules/mixed/global_preferences.py @@ -42,9 +42,9 @@ def check_indicators(self) -> None: for entry in self.results: if entry["entry"] == "LDMGlobalEnabled": if entry["value"]: - self.alertstore.info("Lockdown mode enabled", "", None) + self.alertstore.info("Lockdown mode enabled", "", entry) else: - self.alertstore.low("Lockdown mode disabled", "", None) + self.alertstore.low("Lockdown mode disabled", "", entry) continue def process_file(self, file_path: str) -> None: @@ -61,6 +61,8 @@ def run(self) -> None: ) self.log.info("Found Global Preference database at path: %s", self.file_path) + if not self.file_path: + return self.process_file(self.file_path) self.log.info("Extracted a total of %d Global Preferences", len(self.results)) diff --git a/src/mvt/ios/modules/mixed/idstatuscache.py b/src/mvt/ios/modules/mixed/idstatuscache.py index 1d836fc48..b1e06dda9 100644 --- a/src/mvt/ios/modules/mixed/idstatuscache.py +++ b/src/mvt/ios/modules/mixed/idstatuscache.py @@ -8,12 +8,12 @@ import plistlib from typing import Optional -from mvt.common.utils import convert_mactime_to_iso from mvt.common.module_types import ( ModuleAtomicResult, ModuleResults, ModuleSerializedResult, ) +from mvt.common.utils import convert_mactime_to_iso from ..base import IOSExtraction @@ -67,13 +67,12 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( - self.get_slug(), ioc_match.message, "", result + ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) continue if "\\x00\\x00" in result.get("user", ""): self.alertstore.high( - self.get_slug(), f"Found an ID Status Cache entry with suspicious patterns: {result.get('user')}", "", result, diff --git a/src/mvt/ios/modules/mixed/interactionc.py b/src/mvt/ios/modules/mixed/interactionc.py index 8eadda48d..078ac6a68 100644 --- a/src/mvt/ios/modules/mixed/interactionc.py +++ b/src/mvt/ios/modules/mixed/interactionc.py @@ -7,12 +7,12 @@ import sqlite3 from typing import Optional -from mvt.common.utils import convert_mactime_to_iso from mvt.common.module_types import ( ModuleAtomicResult, - ModuleSerializedResult, ModuleResults, + ModuleSerializedResult, ) +from mvt.common.utils import convert_mactime_to_iso from ..base import IOSExtraction @@ -285,6 +285,8 @@ def run(self) -> None: ) self.log.info("Found InteractionC database at path: %s", self.file_path) + if not self.file_path: + return conn = self._open_sqlite_db(self.file_path) cur = conn.cursor() diff --git a/src/mvt/ios/modules/mixed/locationd.py b/src/mvt/ios/modules/mixed/locationd.py index 35a1c492e..b16c0a19a 100644 --- a/src/mvt/ios/modules/mixed/locationd.py +++ b/src/mvt/ios/modules/mixed/locationd.py @@ -8,12 +8,12 @@ import plistlib from typing import Optional -from mvt.common.utils import convert_mactime_to_iso from mvt.common.module_types import ( ModuleAtomicResult, - ModuleSerializedResult, ModuleResults, + ModuleSerializedResult, ) +from mvt.common.utils import convert_mactime_to_iso from ..base import IOSExtraction @@ -86,7 +86,6 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.high( - self.get_slug(), f"Found a suspicious process name in LocationD entry {result['package']}", "", result, @@ -99,7 +98,6 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.high( - self.get_slug(), f"Found a suspicious process name in LocationD entry {result['package']}", "", result, @@ -111,8 +109,7 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.high( - self.get_slug(), - f"Found a suspicious file path in LocationD entry {result['BundlePath']}", + f"Found a known malicious domain in LocationD entry {result['package']}", "", result, ) @@ -124,7 +121,6 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.high( - self.get_slug(), f"Found a suspicious file path in LocationD entry {result['Executable']}", "", result, @@ -141,7 +137,6 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.high( - self.get_slug(), f"Found a suspicious file path in LocationD entry {result['Registered']}", "", result, diff --git a/src/mvt/ios/modules/mixed/osanalytics_addaily.py b/src/mvt/ios/modules/mixed/osanalytics_addaily.py index 1238d3c2b..719197583 100644 --- a/src/mvt/ios/modules/mixed/osanalytics_addaily.py +++ b/src/mvt/ios/modules/mixed/osanalytics_addaily.py @@ -7,12 +7,12 @@ import plistlib from typing import Optional -from mvt.common.utils import convert_datetime_to_iso from mvt.common.module_types import ( - ModuleResults, ModuleAtomicResult, + ModuleResults, ModuleSerializedResult, ) +from mvt.common.utils import convert_datetime_to_iso from ..base import IOSExtraction @@ -65,7 +65,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_process(result["package"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) def run(self) -> None: self._find_ios_database( @@ -76,6 +78,8 @@ def run(self) -> None: "Found com.apple.osanalytics.addaily plist at path: %s", self.file_path ) + if not self.file_path: + return with open(self.file_path, "rb") as handle: file_plist = plistlib.load(handle) diff --git a/src/mvt/ios/modules/mixed/safari_browserstate.py b/src/mvt/ios/modules/mixed/safari_browserstate.py index cbeeb242b..021f690e9 100644 --- a/src/mvt/ios/modules/mixed/safari_browserstate.py +++ b/src/mvt/ios/modules/mixed/safari_browserstate.py @@ -10,12 +10,12 @@ import sqlite3 from typing import Optional -from mvt.common.utils import convert_mactime_to_iso, keys_bytes_to_string from mvt.common.module_types import ( + ModuleAtomicResult, ModuleResults, ModuleSerializedResult, - ModuleAtomicResult, ) +from mvt.common.utils import convert_mactime_to_iso, keys_bytes_to_string from ..base import IOSExtraction @@ -67,7 +67,7 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( - self.get_slug(), ioc_match.message, "", result + ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) continue @@ -80,7 +80,10 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( - self.get_slug(), ioc_match.message, "", result + ioc_match.message, + "", + result, + matched_indicator=ioc_match.ioc, ) def _process_browser_state_db(self, db_path): diff --git a/src/mvt/ios/modules/mixed/safari_history.py b/src/mvt/ios/modules/mixed/safari_history.py index 213cab675..8b5dd8c34 100644 --- a/src/mvt/ios/modules/mixed/safari_history.py +++ b/src/mvt/ios/modules/mixed/safari_history.py @@ -7,13 +7,13 @@ import os from typing import Optional -from mvt.common.url import URL -from mvt.common.utils import convert_mactime_to_datetime, convert_mactime_to_iso from mvt.common.module_types import ( - ModuleResults, ModuleAtomicResult, + ModuleResults, ModuleSerializedResult, ) +from mvt.common.url import URL +from mvt.common.utils import convert_mactime_to_datetime, convert_mactime_to_iso from ..base import IOSExtraction @@ -117,7 +117,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_url(result["url"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) def _process_history_db(self, history_path): self._recover_sqlite_db_if_needed(history_path) diff --git a/src/mvt/ios/modules/mixed/shortcuts.py b/src/mvt/ios/modules/mixed/shortcuts.py index 38735a0aa..762769f7e 100644 --- a/src/mvt/ios/modules/mixed/shortcuts.py +++ b/src/mvt/ios/modules/mixed/shortcuts.py @@ -10,12 +10,12 @@ import sqlite3 from typing import Optional -from mvt.common.utils import check_for_links, convert_mactime_to_iso from mvt.common.module_types import ( ModuleAtomicResult, ModuleResults, ModuleSerializedResult, ) +from mvt.common.utils import check_for_links, convert_mactime_to_iso from ..base import IOSExtraction @@ -80,7 +80,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_urls(result["action_urls"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) def run(self) -> None: self._find_ios_database( @@ -88,6 +90,8 @@ def run(self) -> None: ) self.log.info("Found Shortcuts database at path: %s", self.file_path) + if not self.file_path: + return conn = self._open_sqlite_db(self.file_path) conn.text_factory = bytes cur = conn.cursor() diff --git a/src/mvt/ios/modules/mixed/sms.py b/src/mvt/ios/modules/mixed/sms.py index 5f64941bd..5005ff54b 100644 --- a/src/mvt/ios/modules/mixed/sms.py +++ b/src/mvt/ios/modules/mixed/sms.py @@ -8,12 +8,12 @@ from base64 import b64encode from typing import Optional -from mvt.common.utils import check_for_links, convert_mactime_to_iso from mvt.common.module_types import ( ModuleAtomicResult, ModuleResults, ModuleSerializedResult, ) +from mvt.common.utils import check_for_links, convert_mactime_to_iso from ..base import IOSExtraction @@ -95,12 +95,17 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_urls(message_links) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) def run(self) -> None: self._find_ios_database(backup_ids=SMS_BACKUP_IDS, root_paths=SMS_ROOT_PATHS) self.log.info("Found SMS database at path: %s", self.file_path) + if not self.file_path: + return + try: conn = self._open_sqlite_db(self.file_path) cur = conn.cursor() @@ -118,6 +123,7 @@ def run(self) -> None: except sqlite3.DatabaseError as exc: conn.close() if "database disk image is malformed" in str(exc): + assert self.file_path is not None self._recover_sqlite_db_if_needed(self.file_path, forced=True) conn = self._open_sqlite_db(self.file_path) cur = conn.cursor() diff --git a/src/mvt/ios/modules/mixed/sms_attachments.py b/src/mvt/ios/modules/mixed/sms_attachments.py index 0b9a6e342..53943a889 100644 --- a/src/mvt/ios/modules/mixed/sms_attachments.py +++ b/src/mvt/ios/modules/mixed/sms_attachments.py @@ -7,12 +7,12 @@ from base64 import b64encode from typing import Optional -from mvt.common.utils import convert_mactime_to_iso from mvt.common.module_types import ( ModuleAtomicResult, - ModuleSerializedResult, ModuleResults, + ModuleSerializedResult, ) +from mvt.common.utils import convert_mactime_to_iso from ..base import IOSExtraction @@ -65,9 +65,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_file_path(attachment["filename"]) if ioc_match: attachment["matched_indicator"] = ioc_match.ioc - self.alertstore.high( - self.get_slug(), ioc_match.message, "", attachment - ) + self.alertstore.high(ioc_match.message, "", attachment) if ( attachment["filename"].startswith("/var/tmp/") @@ -85,6 +83,8 @@ def run(self) -> None: self._find_ios_database(backup_ids=SMS_BACKUP_IDS, root_paths=SMS_ROOT_PATHS) self.log.info("Found SMS database at path: %s", self.file_path) + if not self.file_path: + return conn = self._open_sqlite_db(self.file_path) cur = conn.cursor() cur.execute( diff --git a/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py b/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py index 4de6df6f7..cae6a03e5 100644 --- a/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py +++ b/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py @@ -8,12 +8,12 @@ import sqlite3 from typing import Optional -from mvt.common.utils import convert_unix_to_iso from mvt.common.module_types import ( ModuleAtomicResult, - ModuleSerializedResult, ModuleResults, + ModuleSerializedResult, ) +from mvt.common.utils import convert_unix_to_iso from ..base import IOSExtraction @@ -69,7 +69,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_url(result["registrable_domain"]) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) self.alertstore.log_latest() def _process_observations_db(self, db_path: str, domain: str, path: str) -> None: diff --git a/src/mvt/ios/modules/mixed/webkit_session_resource_log.py b/src/mvt/ios/modules/mixed/webkit_session_resource_log.py index 8c88c660e..27d320ad4 100644 --- a/src/mvt/ios/modules/mixed/webkit_session_resource_log.py +++ b/src/mvt/ios/modules/mixed/webkit_session_resource_log.py @@ -8,8 +8,8 @@ import plistlib from typing import Optional -from mvt.common.utils import convert_datetime_to_iso from mvt.common.module_types import ModuleResults +from mvt.common.utils import convert_datetime_to_iso from ..base import IOSExtraction @@ -50,7 +50,7 @@ def __init__( results=results, ) - self.results = {} if not results else results + self.results: dict = {} @staticmethod def _extract_domains(entries): @@ -83,15 +83,15 @@ def check_indicators(self) -> None: # subresource_domains = self._extract_domains( # entry["subresource_under_origin"]) - all_origins = set( - [entry["origin"]] + source_domains + destination_domains + all_origins = list( + set([entry["origin"]] + source_domains + destination_domains) ) ioc_match = self.indicators.check_urls(all_origins) if ioc_match: entry["matched_indicator"] = ioc_match.ioc self.alertstore.critical( - self.get_slug(), ioc_match.message, "", entry + ioc_match.message, "", entry, matched_indicator=ioc_match.ioc ) redirect_path = "" @@ -114,7 +114,6 @@ def check_indicators(self) -> None: redirect_path += ", ".join(destination_domains) self.alertstore.high( - self.get_slug(), f"Found HTTP redirect between suspicious domains: {redirect_path}", "", entry, @@ -190,6 +189,8 @@ def run(self) -> None: self.log.info( "Found Safari browsing session resource log at path: %s", log_path ) + if not self.target_path: + continue key = os.path.relpath(log_path, self.target_path) self.results[key] = self._extract_browsing_stats(log_path) diff --git a/src/mvt/ios/modules/mixed/whatsapp.py b/src/mvt/ios/modules/mixed/whatsapp.py index 4fdeba34c..c1c04087d 100644 --- a/src/mvt/ios/modules/mixed/whatsapp.py +++ b/src/mvt/ios/modules/mixed/whatsapp.py @@ -6,12 +6,12 @@ import logging from typing import Optional -from mvt.common.utils import check_for_links, convert_mactime_to_iso from mvt.common.module_types import ( ModuleAtomicResult, ModuleResults, ModuleSerializedResult, ) +from mvt.common.utils import check_for_links, convert_mactime_to_iso from ..base import IOSExtraction @@ -65,7 +65,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_urls(result.get("links", [])) if ioc_match: result["matched_indicator"] = ioc_match.ioc - self.alertstore.critical(self.get_slug(), ioc_match.message, "", result) + self.alertstore.critical( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) def run(self) -> None: self._find_ios_database( @@ -73,6 +75,8 @@ def run(self) -> None: ) self.log.info("Found WhatsApp database at path: %s", self.file_path) + if not self.file_path: + return conn = self._open_sqlite_db(self.file_path) cur = conn.cursor() @@ -102,7 +106,9 @@ def run(self) -> None: for index, value in enumerate(message_row): message[names[index]] = value - message["isodate"] = convert_mactime_to_iso(message.get("ZMESSAGEDATE")) + message["isodate"] = convert_mactime_to_iso( + message.get("ZMESSAGEDATE") or 0 + ) message["ZTEXT"] = message["ZTEXT"] if message["ZTEXT"] else "" # Extract links from the WhatsApp message. URLs can be stored in diff --git a/src/mvt/ios/versions.py b/src/mvt/ios/versions.py index bae3bd0e3..1ce18008a 100644 --- a/src/mvt/ios/versions.py +++ b/src/mvt/ios/versions.py @@ -9,8 +9,10 @@ import packaging -IPHONE_MODELS = json.loads(pkgutil.get_data("mvt", "ios/data/ios_models.json")) -IPHONE_IOS_VERSIONS = json.loads(pkgutil.get_data("mvt", "ios/data/ios_versions.json")) +IPHONE_MODELS = json.loads(pkgutil.get_data("mvt", "ios/data/ios_models.json") or b"[]") +IPHONE_IOS_VERSIONS = json.loads( + pkgutil.get_data("mvt", "ios/data/ios_versions.json") or b"[]" +) def get_device_desc_from_id(identifier: str, devices_list: list = IPHONE_MODELS) -> str: From 088a3f453a93a274a9eb81b27b2e92e2e81328c3 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Tue, 27 Jan 2026 19:22:30 +0100 Subject: [PATCH 24/37] Remove unused type imports --- src/mvt/android/cmd_check_backup.py | 2 +- src/mvt/common/config.py | 3 +-- src/mvt/common/indicators.py | 3 +-- src/mvt/common/module_types.py | 3 +-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/mvt/android/cmd_check_backup.py b/src/mvt/android/cmd_check_backup.py index c6fe9224c..1b239f10a 100644 --- a/src/mvt/android/cmd_check_backup.py +++ b/src/mvt/android/cmd_check_backup.py @@ -9,7 +9,7 @@ import sys import tarfile from pathlib import Path -from typing import List, Optional, cast +from typing import List, Optional from mvt.android.modules.backup.base import BackupModule from mvt.android.modules.backup.helpers import prompt_or_load_android_backup_password diff --git a/src/mvt/common/config.py b/src/mvt/common/config.py index cecaac54a..ce29fb33c 100644 --- a/src/mvt/common/config.py +++ b/src/mvt/common/config.py @@ -4,10 +4,9 @@ import yaml from appdirs import user_config_dir -from pydantic import AnyHttpUrl, Field +from pydantic import Field from pydantic_settings import ( BaseSettings, - InitSettingsSource, PydanticBaseSettingsSource, SettingsConfigDict, YamlConfigSettingsSource, diff --git a/src/mvt/common/indicators.py b/src/mvt/common/indicators.py index 34aef882f..47669d5f2 100644 --- a/src/mvt/common/indicators.py +++ b/src/mvt/common/indicators.py @@ -9,7 +9,7 @@ import os from dataclasses import dataclass from functools import lru_cache -from typing import Any, Dict, Iterator, List, Optional +from typing import Any, Dict, Iterator, List, Optional, Union import ahocorasick from appdirs import user_data_dir @@ -718,7 +718,6 @@ def check_app_id(self, app_id: str) -> Optional[IndicatorMatch]: return None - def check_receiver_prefix(self, receiver_name: str) -> Union[dict, None]: """Check the provided receiver name against the list of indicators. An IoC match is detected when a substring of the receiver matches the indicator diff --git a/src/mvt/common/module_types.py b/src/mvt/common/module_types.py index 9b924549b..41bbdaab1 100644 --- a/src/mvt/common/module_types.py +++ b/src/mvt/common/module_types.py @@ -4,9 +4,8 @@ # https://license.mvt.re/1.1/ from dataclasses import dataclass -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Union -from .indicators import Indicator # ModuleAtomicResult is a flexible dictionary that can contain any data. # Common fields include: From 38822515ea3cfe8b2fa32e3d8955e228da3106dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Fri, 10 Apr 2026 20:40:05 +0200 Subject: [PATCH 25/37] Fix check_receiver_prefix and check_android_property_name - check_receiver_prefix() used dict syntax (ioc["value"]) on Indicator dataclass objects from get_iocs(). Changed to ioc.value/ioc.name. - check_receiver_prefix() returned raw ioc instead of IndicatorMatch. Now returns IndicatorMatch with descriptive message. - Fixed return type annotations on both methods to Optional[IndicatorMatch]. - Removed unused Union import. --- src/mvt/common/indicators.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/mvt/common/indicators.py b/src/mvt/common/indicators.py index 6299828d6..b725fcfbe 100644 --- a/src/mvt/common/indicators.py +++ b/src/mvt/common/indicators.py @@ -9,7 +9,7 @@ import os from dataclasses import dataclass from functools import lru_cache -from typing import Any, Dict, Iterator, List, Optional, Union +from typing import Any, Dict, Iterator, List, Optional import ahocorasick from appdirs import user_data_dir @@ -716,30 +716,32 @@ def check_app_id(self, app_id: str) -> Optional[IndicatorMatch]: return None - def check_receiver_prefix(self, receiver_name: str) -> Union[dict, None]: + def check_receiver_prefix( + self, receiver_name: str + ) -> Optional[IndicatorMatch]: """Check the provided receiver name against the list of indicators. - An IoC match is detected when a substring of the receiver matches the indicator - :param app_id: App ID to check against the list of indicators - :type app_id: str - :returns: Indicator details if matched, otherwise None + An IoC match is detected when a substring of the receiver matches the indicator. + + :param receiver_name: Receiver name to check against app ID indicators + :type receiver_name: str + :returns: IndicatorMatch if matched, otherwise None """ if not receiver_name: return None for ioc in self.get_iocs("app_ids"): - if ioc["value"].lower() in receiver_name.lower(): - self.log.warning( - 'Found a known suspicious receiver with name "%s" ' - 'matching indicators from "%s"', - receiver_name, - ioc["name"], + if ioc.value.lower() in receiver_name.lower(): + return IndicatorMatch( + ioc=ioc, + message=f'Found a known suspicious receiver with name "{receiver_name}" matching indicators from "{ioc.name}"', ) - return ioc return None - def check_android_property_name(self, property_name: str) -> Optional[dict]: + def check_android_property_name( + self, property_name: str + ) -> Optional[IndicatorMatch]: """Check the android property name against the list of indicators. :param property_name: Name of the Android property From ddb89931398f1f15b76306ba3c38f626c4ec5af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Fri, 10 Apr 2026 20:40:22 +0200 Subject: [PATCH 26/37] Fix residual self.detected usage in packages and dumpsys_receivers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These modules still used self.detected.append() which no longer exists after the alertstore migration. Converted to alertstore calls: - packages.py: ROOT_PACKAGES detection → alertstore.high() - dumpsys_receivers.py: receiver IOC match → alertstore.critical() --- src/mvt/android/modules/adb/packages.py | 8 ++++---- .../android/modules/bugreport/dumpsys_receivers.py | 13 +++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/mvt/android/modules/adb/packages.py b/src/mvt/android/modules/adb/packages.py index 74e3afc3b..1c95b2b75 100644 --- a/src/mvt/android/modules/adb/packages.py +++ b/src/mvt/android/modules/adb/packages.py @@ -73,11 +73,11 @@ def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: def check_indicators(self) -> None: for result in self.results: if result["package_name"] in ROOT_PACKAGES: - self.log.warning( - 'Found an installed package related to rooting/jailbreaking: "%s"', - result["package_name"], + self.alertstore.high( + f'Found an installed package related to rooting/jailbreaking: "{result["package_name"]}"', + "", + result, ) - self.detected.append(result) continue if result["package_name"] in SECURITY_PACKAGES and result["disabled"]: diff --git a/src/mvt/android/modules/bugreport/dumpsys_receivers.py b/src/mvt/android/modules/bugreport/dumpsys_receivers.py index 2b4be916d..3c365910e 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_receivers.py +++ b/src/mvt/android/modules/bugreport/dumpsys_receivers.py @@ -41,10 +41,15 @@ def check_indicators(self) -> None: receiver_name = self.results[result][0]["receiver"] # return IoC if the stix2 process name a substring of the receiver name - ioc = self.indicators.check_receiver_prefix(receiver_name) - if ioc: - self.results[result][0]["matched_indicator"] = ioc - self.detected.append(result) + ioc_match = self.indicators.check_receiver_prefix(receiver_name) + if ioc_match: + self.results[result][0]["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + ioc_match.message, + "", + self.results[result][0], + matched_indicator=ioc_match.ioc, + ) continue From 5a919ea42a1dd7c5a3710bfcf819236a008988b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Fri, 10 Apr 2026 20:40:45 +0200 Subject: [PATCH 27/37] Fix SMS module alertstore.high() call passing slug as message The first argument was self.get_slug() (module slug) instead of a human-readable message. The module is already auto-detected via AlertStore._get_calling_module(). Also removed redundant log_latest(). --- src/mvt/ios/modules/mixed/sms.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/mvt/ios/modules/mixed/sms.py b/src/mvt/ios/modules/mixed/sms.py index 5005ff54b..bc619c281 100644 --- a/src/mvt/ios/modules/mixed/sms.py +++ b/src/mvt/ios/modules/mixed/sms.py @@ -77,12 +77,10 @@ def check_indicators(self) -> None: "text", "" ).startswith(alert_new): self.alertstore.high( - self.get_slug(), - f"Apple warning about state-sponsored attack received on the {message['isodate']}", + f"Apple warning about state-sponsored attack received on {message['isodate']}", message["isodate"], message, ) - self.alertstore.log_latest() if not self.indicators: return From e8852812f3d551acf64b0669a579c01351d4d78d Mon Sep 17 00:00:00 2001 From: tes Date: Tue, 21 Apr 2026 08:53:51 -0300 Subject: [PATCH 28/37] Apply suggestions from code review Fix JSON serialization in `module.save_to_json` and fix argument order in iOS alertstore calls. Co-authored-by: tes --- src/mvt/common/module.py | 2 +- src/mvt/ios/modules/fs/filesystem.py | 4 ++-- src/mvt/ios/modules/mixed/safari_history.py | 1 - src/mvt/ios/modules/mixed/sms_attachments.py | 1 - src/mvt/ios/modules/net_base.py | 7 ++----- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/mvt/common/module.py b/src/mvt/common/module.py index ce8652fa4..77b935a43 100644 --- a/src/mvt/common/module.py +++ b/src/mvt/common/module.py @@ -129,7 +129,7 @@ def save_to_json(self) -> None: detected_json_path = os.path.join(self.results_path, detected_file_name) with open(detected_json_path, "w", encoding="utf-8") as handle: json.dump( - self.alertstore.alerts, handle, indent=4, cls=CustomJSONEncoder + self.alertstore.as_json(), handle, indent=4, cls=CustomJSONEncoder ) def serialize(self, result: ModuleAtomicResult) -> ModuleSerializedResult: diff --git a/src/mvt/ios/modules/fs/filesystem.py b/src/mvt/ios/modules/fs/filesystem.py index 980857a3b..c77764189 100644 --- a/src/mvt/ios/modules/fs/filesystem.py +++ b/src/mvt/ios/modules/fs/filesystem.py @@ -58,7 +58,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_file_path(result["path"]) if ioc_match: - self.alertstore.high(self.get_slug(), ioc_match.message, "", result) + self.alertstore.high(ioc_match.message, "", result) self.alertstore.log_latest() # If we are instructed to run fast, we skip the rest. @@ -67,7 +67,7 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_file_path_process(result["path"]) if ioc_match: - self.alertstore.high(self.get_slug(), ioc_match.message, "", result) + self.alertstore.high(ioc_match.message, "", result) self.alertstore.log_latest() def run(self) -> None: diff --git a/src/mvt/ios/modules/mixed/safari_history.py b/src/mvt/ios/modules/mixed/safari_history.py index 8b5dd8c34..5fe748fc7 100644 --- a/src/mvt/ios/modules/mixed/safari_history.py +++ b/src/mvt/ios/modules/mixed/safari_history.py @@ -101,7 +101,6 @@ def _find_injections(self): if elapsed_time.seconds == 0: self.alertstore.medium( - self.get_slug(), f"Redirect took less than a second! ({elapsed_ms} milliseconds)", result["timestamp"], result, diff --git a/src/mvt/ios/modules/mixed/sms_attachments.py b/src/mvt/ios/modules/mixed/sms_attachments.py index 53943a889..2f45470ce 100644 --- a/src/mvt/ios/modules/mixed/sms_attachments.py +++ b/src/mvt/ios/modules/mixed/sms_attachments.py @@ -73,7 +73,6 @@ def check_indicators(self) -> None: and attachment["direction"] == "received" ): self.alertstore.medium( - self.get_slug(), f"Suspicious iMessage attachment {attachment['filename']} on {attachment['isodate']}", attachment["isodate"], attachment, diff --git a/src/mvt/ios/modules/net_base.py b/src/mvt/ios/modules/net_base.py index 6de5bd16f..dcc9679d8 100644 --- a/src/mvt/ios/modules/net_base.py +++ b/src/mvt/ios/modules/net_base.py @@ -238,7 +238,7 @@ def _find_suspicious_processes(self): ) self.alertstore.medium( - self.get_slug(), msg, proc["live_isodate"], proc + msg, proc["live_isodate"], proc ) if not proc["live_proc_id"]: @@ -260,7 +260,6 @@ def check_manipulated(self): if result["live_proc_id"] not in missing_process_cache: missing_process_cache.add(result["live_proc_id"]) self.alertstore.high( - self.get_slug(), f"Found manipulated process entry {result['live_proc_id']}. Entry on {result['live_isodate']}", result["live_isodate"], result, @@ -271,7 +270,6 @@ def check_manipulated(self): result["first_isodate"] = result["isodate"] = result["live_isodate"] result["proc_name"] = "MANIPULATED [process record deleted]" self.alertstore.high( - self.get_slug(), f"Found manipulated process entry {result['live_proc_id']}/", result["first_isodate"], result, @@ -294,7 +292,6 @@ def find_deleted(self): if proc_id not in all_proc_id: previous_proc = results_by_proc[last_proc_id] self.alertstore.low( - self.get_slug(), f'Missing process {proc_id}. Previous process at "{previous_proc["first_isodate"]}" ({previous_proc["proc_name"]})', previous_proc["first_isodate"], previous_proc, @@ -353,5 +350,5 @@ def check_indicators(self) -> None: if ioc_match: result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( - self.get_slug(), ioc_match.message, result["first_isodate"], result + ioc_match.message, result["first_isodate"], result ) From 195d0738def632068b285bdc923dbc15defcba16 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Wed, 22 Apr 2026 15:51:33 +0200 Subject: [PATCH 29/37] Remove unsupported ADB modules --- src/mvt/android/modules/adb/__init__.py | 32 -- src/mvt/android/modules/adb/chrome_history.py | 117 ------- src/mvt/android/modules/adb/dumpsys_full.py | 46 --- src/mvt/android/modules/adb/files.py | 160 --------- src/mvt/android/modules/adb/getprop.py | 44 --- src/mvt/android/modules/adb/logcat.py | 58 ---- src/mvt/android/modules/adb/packages.py | 311 ------------------ src/mvt/android/modules/adb/processes.py | 43 --- src/mvt/android/modules/adb/root_binaries.py | 75 ----- src/mvt/android/modules/adb/selinux_status.py | 50 --- src/mvt/android/modules/adb/settings.py | 59 ---- src/mvt/android/modules/adb/sms.py | 186 ----------- src/mvt/android/modules/adb/whatsapp.py | 121 ------- src/mvt/ios/modules/fs/cache_files.py | 15 +- tests/ios_fs/test_cache_files.py | 37 +++ 15 files changed, 44 insertions(+), 1310 deletions(-) delete mode 100644 src/mvt/android/modules/adb/__init__.py delete mode 100644 src/mvt/android/modules/adb/chrome_history.py delete mode 100644 src/mvt/android/modules/adb/dumpsys_full.py delete mode 100644 src/mvt/android/modules/adb/files.py delete mode 100644 src/mvt/android/modules/adb/getprop.py delete mode 100644 src/mvt/android/modules/adb/logcat.py delete mode 100644 src/mvt/android/modules/adb/packages.py delete mode 100644 src/mvt/android/modules/adb/processes.py delete mode 100644 src/mvt/android/modules/adb/root_binaries.py delete mode 100644 src/mvt/android/modules/adb/selinux_status.py delete mode 100644 src/mvt/android/modules/adb/settings.py delete mode 100644 src/mvt/android/modules/adb/sms.py delete mode 100644 src/mvt/android/modules/adb/whatsapp.py create mode 100644 tests/ios_fs/test_cache_files.py diff --git a/src/mvt/android/modules/adb/__init__.py b/src/mvt/android/modules/adb/__init__.py deleted file mode 100644 index b2b436896..000000000 --- a/src/mvt/android/modules/adb/__init__.py +++ /dev/null @@ -1,32 +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 .chrome_history import ChromeHistory -from .dumpsys_full import DumpsysFull -from .files import Files -from .getprop import Getprop -from .logcat import Logcat -from .packages import Packages -from .processes import Processes -from .root_binaries import RootBinaries -from .selinux_status import SELinuxStatus -from .settings import Settings -from .sms import SMS -from .whatsapp import Whatsapp - -ADB_MODULES = [ - ChromeHistory, - SMS, - Whatsapp, - Processes, - Getprop, - Settings, - SELinuxStatus, - DumpsysFull, - Packages, - Logcat, - RootBinaries, - Files, -] diff --git a/src/mvt/android/modules/adb/chrome_history.py b/src/mvt/android/modules/adb/chrome_history.py deleted file mode 100644 index 16a516494..000000000 --- a/src/mvt/android/modules/adb/chrome_history.py +++ /dev/null @@ -1,117 +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 -import os -import sqlite3 -from typing import Optional - -from mvt.common.module_types import ( - ModuleAtomicResult, - ModuleResults, - ModuleSerializedResult, -) -from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to_iso - -from .base import AndroidExtraction - -CHROME_HISTORY_PATH = "data/data/com.android.chrome/app_chrome/Default/History" - - -class ChromeHistory(AndroidExtraction): - """This module extracts records from Android's Chrome browsing history.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: ModuleResults = [], - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - self.results: list = [] - - def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: - return { - "timestamp": record["isodate"], - "module": self.__class__.__name__, - "event": "visit", - "data": f"{record['id']} - {record['url']} (visit ID: {record['visit_id']}, " - f"redirect source: {record['redirect_source']})", - } - - def check_indicators(self) -> None: - if not self.indicators: - return - - for result in self.results: - ioc_match = self.indicators.check_url(result["url"]) - if ioc_match: - self.alertstore.critical( - ioc_match.message, "", result, matched_indicator=ioc_match.ioc - ) - - def _parse_db(self, db_path: str) -> None: - """Parse a Chrome History database file. - - :param db_path: Path to the History database to process. - - """ - assert isinstance(self.results, list) # assert results type for mypy - conn = sqlite3.connect(db_path) - cur = conn.cursor() - cur.execute( - """ - SELECT - urls.id, - urls.url, - visits.id, - visits.visit_time, - visits.from_visit - FROM urls - JOIN visits ON visits.url = urls.id - ORDER BY visits.visit_time; - """ - ) - - for item in cur: - self.results.append( - { - "id": item[0], - "url": item[1], - "visit_id": item[2], - "timestamp": item[3], - "isodate": convert_datetime_to_iso( - convert_chrometime_to_datetime(item[3]) - ), - "redirect_source": item[4], - } - ) - - cur.close() - conn.close() - - self.log.info("Extracted a total of %d history items", len(self.results)) - - def run(self) -> None: - self._adb_connect() - - try: - self._adb_process_file( - os.path.join("/", CHROME_HISTORY_PATH), self._parse_db - ) - except Exception as exc: - self.log.error(exc) - - self._adb_disconnect() diff --git a/src/mvt/android/modules/adb/dumpsys_full.py b/src/mvt/android/modules/adb/dumpsys_full.py deleted file mode 100644 index fa1a6b3ca..000000000 --- a/src/mvt/android/modules/adb/dumpsys_full.py +++ /dev/null @@ -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 -import os -from typing import Optional - -from .base import AndroidExtraction -from mvt.common.module_types import ModuleResults - - -class DumpsysFull(AndroidExtraction): - """This module extracts stats on battery consumption by processes.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: ModuleResults = [], - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - self._adb_connect() - - output = self._adb_command("dumpsys") - if self.results_path: - output_path = os.path.join(self.results_path, "dumpsys.txt") - with open(output_path, "w", encoding="utf-8") as handle: - handle.write(output) - - self.log.info("Full dumpsys output stored at %s", output_path) - - self._adb_disconnect() diff --git a/src/mvt/android/modules/adb/files.py b/src/mvt/android/modules/adb/files.py deleted file mode 100644 index 134d9d281..000000000 --- a/src/mvt/android/modules/adb/files.py +++ /dev/null @@ -1,160 +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 -import os -import stat -from typing import Optional, Union - -from mvt.common.module_types import ModuleResults -from mvt.common.utils import convert_unix_to_iso - -from .base import AndroidExtraction - -ANDROID_TMP_FOLDERS = [ - "/tmp/", - "/data/local/tmp/", -] -ANDROID_MEDIA_FOLDERS = [ - "/data/media/0", - "/sdcard/", -] - - -class Files(AndroidExtraction): - """This module extracts the list of files on the device.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: ModuleResults = [], - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - self.full_find = False - - def serialize(self, record: dict) -> Union[dict, list, None]: - if "modified_time" in record: - return { - "timestamp": record["modified_time"], - "module": self.__class__.__name__, - "event": "file_modified", - "data": record["path"], - } - - return None - - def check_indicators(self) -> None: - for result in self.results: - if result.get("is_suid"): - self.log.warning( - 'Found an SUID file in a non-standard directory "%s".', - result["path"], - ) - - if self.indicators: - ioc_match = self.indicators.check_file_path(result["path"]) - if ioc_match: - self.alertstore.critical( - f'Found a known suspicious file at path: "{result["path"]}"', - "", - result, - matched_indicator=ioc_match, - ) - - def backup_file(self, file_path: str) -> None: - if not self.results_path: - return - - local_file_name = file_path.replace("/", "_").replace(" ", "-") - local_files_folder = os.path.join(self.results_path, "files") - if not os.path.exists(local_files_folder): - os.mkdir(local_files_folder) - - local_file_path = os.path.join(local_files_folder, local_file_name) - - try: - self._adb_download(remote_path=file_path, local_path=local_file_path) - except Exception: - pass - else: - self.log.info( - "Downloaded file %s to local copy at %s", file_path, local_file_path - ) - - def find_files(self, folder: str) -> None: - assert isinstance(self.results, list) - if self.full_find: - cmd = f"find '{folder}' -type f -printf '%T@ %m %s %u %g %p\n' 2> /dev/null" - output = self._adb_command(cmd) - - for file_line in output.splitlines(): - file_info = file_line.rstrip().split(" ", 5) - if len(file_line) < 6: - self.log.info("Skipping invalid file info - %s", file_line.rstrip()) - continue - [unix_timestamp, mode, size, owner, group, full_path] = file_info - mod_time = convert_unix_to_iso(unix_timestamp) - - self.results.append( - { - "path": full_path, - "modified_time": mod_time, - "mode": mode, - "is_suid": (int(mode, 8) & stat.S_ISUID) == 2048, - "is_sgid": (int(mode, 8) & stat.S_ISGID) == 1024, - "size": size, - "owner": owner, - "group": group, - } - ) - else: - output = self._adb_command(f"find '{folder}' -type f 2> /dev/null") - for file_line in output.splitlines(): - self.results.append({"path": file_line.rstrip()}) - - def run(self) -> None: - self._adb_connect() - - cmd = "find '/' -maxdepth 1 -printf '%T@ %m %s %u %g %p\n' 2> /dev/null" - output = self._adb_command(cmd) - if output or output.strip().splitlines(): - self.full_find = True - - for tmp_folder in ANDROID_TMP_FOLDERS: - self.find_files(tmp_folder) - - for entry in self.results: - self.log.info("Found file in tmp folder at path %s", entry.get("path")) - self.backup_file(entry.get("path")) - - for media_folder in ANDROID_MEDIA_FOLDERS: - self.find_files(media_folder) - - self.log.info( - "Found %s files in primary Android tmp and media folders", len(self.results) - ) - - if self.module_options.get("fast_mode", None): - self.log.info( - "The `fast_mode` option was enabled: skipping full file listing" - ) - else: - self.log.info("Processing full file listing. This may take a while...") - self.find_files("/") - - self.log.info("Found %s total files", len(self.results)) - - self._adb_disconnect() diff --git a/src/mvt/android/modules/adb/getprop.py b/src/mvt/android/modules/adb/getprop.py deleted file mode 100644 index a9c77be07..000000000 --- a/src/mvt/android/modules/adb/getprop.py +++ /dev/null @@ -1,44 +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 typing import Optional - -from mvt.android.artifacts.getprop import GetProp as GetPropArtifact - -from .base import AndroidExtraction -from mvt.common.module_types import ModuleResults - - -class Getprop(GetPropArtifact, AndroidExtraction): - """This module extracts device properties from getprop command.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: ModuleResults = [], - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - self.results = {} if not results else results - - def run(self) -> None: - self._adb_connect() - output = self._adb_command("getprop") - self._adb_disconnect() - - self.parse(output) - self.log.info("Extracted %d Android system properties", len(self.results)) diff --git a/src/mvt/android/modules/adb/logcat.py b/src/mvt/android/modules/adb/logcat.py deleted file mode 100644 index 41418a1b9..000000000 --- a/src/mvt/android/modules/adb/logcat.py +++ /dev/null @@ -1,58 +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 -import os -from typing import Optional - -from .base import AndroidExtraction -from mvt.common.module_types import ModuleResults - - -class Logcat(AndroidExtraction): - """This module extracts details on installed packages.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: ModuleResults = [], - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - self._adb_connect() - - # Get the current logcat. - output = self._adb_command('logcat -d -b all "*:V"') - # Get the locat prior to last reboot. - last_output = self._adb_command('logcat -L -b all "*:V"') - - if self.results_path: - logcat_path = os.path.join(self.results_path, "logcat.txt") - with open(logcat_path, "w", encoding="utf-8") as handle: - handle.write(output) - - self.log.info("Current logcat logs stored at %s", logcat_path) - - logcat_last_path = os.path.join(self.results_path, "logcat_last.txt") - with open(logcat_last_path, "w", encoding="utf-8") as handle: - handle.write(last_output) - - self.log.info( - "Logcat logs prior to last reboot stored at %s", logcat_last_path - ) - - self._adb_disconnect() diff --git a/src/mvt/android/modules/adb/packages.py b/src/mvt/android/modules/adb/packages.py deleted file mode 100644 index 1c95b2b75..000000000 --- a/src/mvt/android/modules/adb/packages.py +++ /dev/null @@ -1,311 +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 typing import Optional - -from mvt.android.artifacts.dumpsys_packages import DumpsysPackagesArtifact -from mvt.android.utils import ( - DANGEROUS_PERMISSIONS, - DANGEROUS_PERMISSIONS_THRESHOLD, - ROOT_PACKAGES, - SECURITY_PACKAGES, - SYSTEM_UPDATE_PACKAGES, -) -from mvt.common.module_types import ( - ModuleAtomicResult, - ModuleResults, - ModuleSerializedResult, -) - -from .base import AndroidExtraction - - -class Packages(AndroidExtraction): - """This module extracts the list of installed packages.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: ModuleResults = [], - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - self._user_needed = False - - def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: - records = [] - - timestamps = [ - {"event": "package_install", "timestamp": record["timestamp"]}, - { - "event": "package_first_install", - "timestamp": record["first_install_time"], - }, - {"event": "package_last_update", "timestamp": record["last_update_time"]}, - ] - - for timestamp in timestamps: - records.append( - { - "timestamp": timestamp["timestamp"], - "module": self.__class__.__name__, - "event": timestamp["event"], - "data": f"{record['package_name']} (system: {record['system']}," - f" third party: {record['third_party']})", - } - ) - - return records - - def check_indicators(self) -> None: - for result in self.results: - if result["package_name"] in ROOT_PACKAGES: - self.alertstore.high( - f'Found an installed package related to rooting/jailbreaking: "{result["package_name"]}"', - "", - result, - ) - continue - - if result["package_name"] in SECURITY_PACKAGES and result["disabled"]: - self.log.warning( - 'Found a security package disabled: "%s"', result["package_name"] - ) - - if result["package_name"] in SYSTEM_UPDATE_PACKAGES and result["disabled"]: - self.log.warning( - 'System OTA update package "%s" disabled on the phone', - result["package_name"], - ) - - if not self.indicators: - continue - - ioc_match = self.indicators.check_app_id(result["package_name"]) - if ioc_match: - self.alertstore.critical( - ioc_match.message, "", result, matched_indicator=ioc_match.ioc - ) - - for package_file in result.get("files", []): - ioc_match = self.indicators.check_file_hash(package_file["sha256"]) - if ioc_match: - self.alertstore.critical( - ioc_match.message, "", result, matched_indicator=ioc_match.ioc - ) - - # @staticmethod - # def check_virustotal(packages: list) -> None: - # hashes = [] - # for package in packages: - # for file in package.get("files", []): - # if file["sha256"] not in hashes: - # hashes.append(file["sha256"]) - - # total_hashes = len(hashes) - # detections = {} - - # progress_desc = f"Looking up {total_hashes} files..." - # for i in track(range(total_hashes), description=progress_desc): - # try: - # results = virustotal_lookup(hashes[i]) - # except VTNoKey: - # return - # except VTQuotaExceeded as exc: - # print("Unable to continue: %s", exc) - # break - - # if not results: - # continue - - # positives = results["attributes"]["last_analysis_stats"]["malicious"] - # total = len(results["attributes"]["last_analysis_results"]) - - # detections[hashes[i]] = f"{positives}/{total}" - - # table = Table(title="VirusTotal Packages Detections") - # table.add_column("Package name") - # table.add_column("File path") - # table.add_column("Detections") - - # for package in packages: - # for file in package.get("files", []): - # row = [package["package_name"], file["path"]] - - # if file["sha256"] in detections: - # detection = detections[file["sha256"]] - # positives = detection.split("/")[0] - # if int(positives) > 0: - # row.append(Text(detection, "red bold")) - # else: - # row.append(detection) - # else: - # row.append("not found") - - # table.add_row(*row) - - # console = Console() - # console.print(table) - - @staticmethod - def parse_package_for_details(output: str) -> dict: - lines = [] - in_packages = False - for line in output.splitlines(): - if in_packages: - if line.strip() == "": - break - lines.append(line) - if line.strip() == "Packages:": - in_packages = True - - return DumpsysPackagesArtifact.parse_dumpsys_package_for_details( - "\n".join(lines) - ) - - def _get_files_for_package(self, package_name: str) -> list: - command = f"pm path {package_name}" - if self._user_needed: - command += " --user 0" - output = self._adb_command(command) - output = output.strip().replace("package:", "") - if not output: - return [] - - package_files = [] - for file_path in output.splitlines(): - file_path = file_path.strip() - - md5 = self._adb_command(f"md5sum {file_path}").split(" ", maxsplit=1)[0] - sha1 = self._adb_command(f"sha1sum {file_path}").split(" ", maxsplit=1)[0] - sha256 = self._adb_command(f"sha256sum {file_path}").split(" ", maxsplit=1)[ - 0 - ] - sha512 = self._adb_command(f"sha512sum {file_path}").split(" ", maxsplit=1)[ - 0 - ] - - package_files.append( - { - "path": file_path, - "md5": md5, - "sha1": sha1, - "sha256": sha256, - "sha512": sha512, - } - ) - - return package_files - - def run(self) -> None: - self._adb_connect() - - packages = self._adb_command("pm list packages -u -i -f") - if "java.lang.SecurityException" in packages or packages.strip() == "": - self._user_needed = True - packages = self._adb_command("pm list packages -u -i -f --user 0") - - for line in packages.splitlines(): - line = line.strip() - if not line.startswith("package:"): - continue - - fields = line.split() - file_name, package_name = fields[0].split(":")[1].rsplit("=", 1) - - try: - installer = fields[1].split("=")[1].strip() - except IndexError: - installer = None - else: - if installer == "null": - installer = None - - package_files = self._get_files_for_package(package_name) - new_package = { - "package_name": package_name, - "file_name": file_name, - "installer": installer, - "disabled": False, - "system": False, - "third_party": False, - "files": package_files, - } - - dumpsys_package = self._adb_command(f"dumpsys package {package_name}") - package_details = self.parse_package_for_details(dumpsys_package) - new_package.update(package_details) - - self.results.append(new_package) - - cmds = [ - {"field": "disabled", "arg": "-d"}, - {"field": "system", "arg": "-s"}, - {"field": "third_party", "arg": "-3"}, - ] - for cmd in cmds: - command = f"pm list packages {cmd['arg']}" - if self._user_needed: - command += " --user 0" - output = self._adb_command(command) - for line in output.splitlines(): - line = line.strip() - if not line.startswith("package:"): - continue - - package_name = line.split(":", 1)[1] - for i, result in enumerate(self.results): - if result["package_name"] == package_name: - self.results[i][cmd["field"]] = True - - for result in self.results: - if not result["third_party"]: - continue - - dangerous_permissions_count = 0 - for perm in result["requested_permissions"]: - if perm in DANGEROUS_PERMISSIONS: - dangerous_permissions_count += 1 - - if dangerous_permissions_count >= DANGEROUS_PERMISSIONS_THRESHOLD: - self.log.info( - 'Third-party package "%s" requested %d ' - "potentially dangerous permissions", - result["package_name"], - dangerous_permissions_count, - ) - - packages_to_lookup = [] - for result in self.results: - if result["system"]: - continue - - packages_to_lookup.append(result) - self.log.info( - 'Found non-system package with name "%s" installed by "%s" on %s', - result["package_name"], - result["installer"], - result["timestamp"], - ) - - if not self.module_options.get("fast_mode", None): - self.check_virustotal(packages_to_lookup) - - self.log.info( - "Extracted at total of %d installed package names", len(self.results) - ) - - self._adb_disconnect() diff --git a/src/mvt/android/modules/adb/processes.py b/src/mvt/android/modules/adb/processes.py deleted file mode 100644 index dcdb03640..000000000 --- a/src/mvt/android/modules/adb/processes.py +++ /dev/null @@ -1,43 +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 typing import Optional - -from mvt.android.artifacts.processes import Processes as ProcessesArtifact - -from .base import AndroidExtraction -from mvt.common.module_types import ModuleResults - - -class Processes(ProcessesArtifact, AndroidExtraction): - """This module extracts details on running processes.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: ModuleResults = [], - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def run(self) -> None: - self._adb_connect() - - output = self._adb_command("ps -A") - self.parse(output) - self._adb_disconnect() - - self.log.info("Extracted records on a total of %d processes", len(self.results)) diff --git a/src/mvt/android/modules/adb/root_binaries.py b/src/mvt/android/modules/adb/root_binaries.py deleted file mode 100644 index d9d726973..000000000 --- a/src/mvt/android/modules/adb/root_binaries.py +++ /dev/null @@ -1,75 +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 typing import Optional - -from mvt.common.module_types import ModuleResults - -from .base import AndroidExtraction - - -class RootBinaries(AndroidExtraction): - """This module extracts the list of installed packages.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: ModuleResults = [], - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def check_indicators(self) -> None: - for root_binary in self.results: - self.alertstore.high( - f'Found root binary "{root_binary}"', - "", - root_binary, - ) - - def run(self) -> None: - root_binaries = [ - "su", - "busybox", - "supersu", - "Superuser.apk", - "KingoUser.apk", - "SuperSu.apk", - "magisk", - "magiskhide", - "magiskinit", - "magiskpolicy", - ] - - self._adb_connect() - - for root_binary in root_binaries: - root_binary = root_binary.strip() - if not root_binary: - continue - - output = self._adb_command(f"which -a {root_binary}") - output = output.strip() - - if not output: - continue - - if "which: not found" in output: - continue - - self.results.append(root_binary) - - self._adb_disconnect() diff --git a/src/mvt/android/modules/adb/selinux_status.py b/src/mvt/android/modules/adb/selinux_status.py deleted file mode 100644 index 1f15dbc5e..000000000 --- a/src/mvt/android/modules/adb/selinux_status.py +++ /dev/null @@ -1,50 +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 typing import Optional - -from mvt.common.module_types import ModuleResults - -from .base import AndroidExtraction - - -class SELinuxStatus(AndroidExtraction): - """This module checks if SELinux is being enforced.""" - - slug = "selinux_status" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: ModuleResults = [], - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - self.results: dict = {} - - def run(self) -> None: - self._adb_connect() - output = self._adb_command("getenforce") - self._adb_disconnect() - - status = output.lower().strip() - self.results["status"] = status - - if status == "enforcing": - self.log.info("SELinux is being regularly enforced") - else: - self.log.warning('SELinux status is "%s"!', status) diff --git a/src/mvt/android/modules/adb/settings.py b/src/mvt/android/modules/adb/settings.py deleted file mode 100644 index dcfc6e564..000000000 --- a/src/mvt/android/modules/adb/settings.py +++ /dev/null @@ -1,59 +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 typing import Optional - -from mvt.android.artifacts.settings import Settings as SettingsArtifact -from mvt.common.module_types import ModuleResults - -from .base import AndroidExtraction - - -class Settings(SettingsArtifact, AndroidExtraction): - """This module extracts Android system settings.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: ModuleResults = [], - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - self.results = {} if not results else results - - def run(self) -> None: - self._adb_connect() - - for namespace in ["system", "secure", "global"]: - out = self._adb_command(f"cmd settings list {namespace}") - if not out: - continue - - self.results[namespace] = {} - - for line in out.splitlines(): - line = line.strip() - if line == "": - continue - - fields = line.split("=", 1) - try: - self.results[namespace][fields[0]] = fields[1] - except IndexError: - continue - - self._adb_disconnect() diff --git a/src/mvt/android/modules/adb/sms.py b/src/mvt/android/modules/adb/sms.py deleted file mode 100644 index 0b26131d1..000000000 --- a/src/mvt/android/modules/adb/sms.py +++ /dev/null @@ -1,186 +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 -import os -import sqlite3 -from typing import Optional - -from mvt.android.parsers.backup import AndroidBackupParsingError, parse_tar_for_sms -from mvt.common.module import InsufficientPrivileges -from mvt.common.module_types import ( - ModuleAtomicResult, - ModuleResults, - ModuleSerializedResult, -) -from mvt.common.utils import check_for_links, convert_unix_to_iso - -from .base import AndroidExtraction - -SMS_BUGLE_PATH = "data/data/com.google.android.apps.messaging/databases/bugle_db" -SMS_BUGLE_QUERY = """ -SELECT - ppl.normalized_destination AS address, - p.timestamp AS timestamp, -CASE WHEN m.sender_id IN -(SELECT _id FROM participants WHERE contact_id=-1) -THEN 2 ELSE 1 END incoming, p.text AS body -FROM messages m, conversations c, parts p, - participants ppl, conversation_participants cp -WHERE (m.conversation_id = c._id) - AND (m._id = p.message_id) - AND (cp.conversation_id = c._id) - AND (cp.participant_id = ppl._id); -""" - -SMS_MMSSMS_PATH = "data/data/com.android.providers.telephony/databases/mmssms.db" -SMS_MMSMS_QUERY = """ -SELECT - address AS address, - date_sent AS timestamp, - type as incoming, - body AS body -FROM sms; -""" - - -class SMS(AndroidExtraction): - """This module extracts all SMS messages.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: ModuleResults = [], - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - self.sms_db_type = 0 - - def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: - body = record["body"].replace("\n", "\\n") - return { - "timestamp": record["isodate"], - "module": self.__class__.__name__, - "event": f"sms_{record['direction']}", - "data": f'{record.get("address", "unknown source")}: "{body}"', - } - - def check_indicators(self) -> None: - if not self.indicators: - return - - for message in self.results: - if "body" not in message: - continue - - message_links = message.get("links", []) - if message_links == []: - message_links = check_for_links(message["body"]) - - ioc_match = self.indicators.check_urls(message_links) - if ioc_match: - self.alertstore.critical( - ioc_match.message, "", message, matched_indicator=ioc_match.ioc - ) - - def _parse_db(self, db_path: str) -> None: - """Parse an Android bugle_db SMS database file. - - :param db_path: Path to the Android SMS database file to process - - """ - conn = sqlite3.connect(db_path) - cur = conn.cursor() - - if self.sms_db_type == 1: - cur.execute(SMS_BUGLE_QUERY) - elif self.sms_db_type == 2: - cur.execute(SMS_MMSMS_QUERY) - - names = [description[0] for description in cur.description] - - for item in cur: - message = {} - for index, value in enumerate(item): - message[names[index]] = value - - message["direction"] = "received" if message["incoming"] == 1 else "sent" - message["isodate"] = convert_unix_to_iso(message["timestamp"]) - - # Extract links in the message body - body = message.get("body", None) - if body: - links = check_for_links(message["body"]) - message["links"] = links - - self.results.append(message) - - cur.close() - conn.close() - - self.log.info("Extracted a total of %d SMS messages", len(self.results)) - - def _extract_sms_adb(self) -> None: - """Use the Android backup command to extract SMS data from the native - SMS app. - - It is crucial to use the under-documented "-nocompress" flag to disable - the non-standard Java compression algorithm. This module only supports - an unencrypted ADB backup. - """ - backup_tar = self._generate_backup("com.android.providers.telephony") - if not backup_tar: - return - - try: - self.results = parse_tar_for_sms(backup_tar) - except AndroidBackupParsingError: - self.log.info( - "Impossible to read SMS from the Android Backup, " - "please extract the SMS and try extracting it with " - "Android Backup Extractor" - ) - return - - self.log.info("Extracted a total of %d SMS messages", len(self.results)) - - def run(self) -> None: - self._adb_connect() - - try: - if self._adb_check_file_exists(os.path.join("/", SMS_BUGLE_PATH)): - self.sms_db_type = 1 - self._adb_process_file( - os.path.join("/", SMS_BUGLE_PATH), self._parse_db - ) - elif self._adb_check_file_exists(os.path.join("/", SMS_MMSSMS_PATH)): - self.sms_db_type = 2 - self._adb_process_file( - os.path.join("/", SMS_MMSSMS_PATH), self._parse_db - ) - - self._adb_disconnect() - return - except InsufficientPrivileges: - pass - - self.log.info( - "No SMS database found. Trying extraction of SMS data " - "using Android backup feature." - ) - self._extract_sms_adb() - - self._adb_disconnect() diff --git a/src/mvt/android/modules/adb/whatsapp.py b/src/mvt/android/modules/adb/whatsapp.py deleted file mode 100644 index 76c130556..000000000 --- a/src/mvt/android/modules/adb/whatsapp.py +++ /dev/null @@ -1,121 +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 base64 -import logging -import os -import sqlite3 -from typing import Optional - -from mvt.common.module_types import ( - ModuleAtomicResult, - ModuleResults, - ModuleSerializedResult, -) -from mvt.common.utils import check_for_links, convert_unix_to_iso - -from .base import AndroidExtraction - -WHATSAPP_PATH = "data/data/com.whatsapp/databases/msgstore.db" - - -class Whatsapp(AndroidExtraction): - """This module extracts all WhatsApp messages containing links.""" - - def __init__( - self, - file_path: Optional[str] = None, - target_path: Optional[str] = None, - results_path: Optional[str] = None, - module_options: Optional[dict] = None, - log: logging.Logger = logging.getLogger(__name__), - results: ModuleResults = [], - ) -> None: - super().__init__( - file_path=file_path, - target_path=target_path, - results_path=results_path, - module_options=module_options, - log=log, - results=results, - ) - - def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: - text = record["data"].replace("\n", "\\n") - return { - "timestamp": record["isodate"], - "module": self.__class__.__name__, - "event": f"whatsapp_msg_{record['direction']}", - "data": f'"{text}"', - } - - def check_indicators(self) -> None: - if not self.indicators: - return - - for message in self.results: - if "data" not in message: - continue - - message_links = check_for_links(message["data"]) - ioc_match = self.indicators.check_urls(message_links) - if ioc_match: - self.alertstore.critical( - ioc_match.message, "", message, matched_indicator=ioc_match.ioc - ) - continue - - def _parse_db(self, db_path: str) -> None: - """Parse an Android msgstore.db WhatsApp database file. - - :param db_path: Path to the Android WhatsApp database file to process - - """ - conn = sqlite3.connect(db_path) - cur = conn.cursor() - cur.execute( - """ - SELECT * FROM messages; - """ - ) - names = [description[0] for description in cur.description] - - messages = [] - for item in cur: - message = {} - for index, value in enumerate(item): - message[names[index]] = value - - if not message["data"]: - continue - - message["direction"] = "send" if message["key_from_me"] == 1 else "received" - message["isodate"] = convert_unix_to_iso(message["timestamp"]) - - # If we find links in the messages or if they are empty we add them - # to the list. - if check_for_links(message["data"]) or message["data"].strip() == "": - if message.get("thumb_image"): - message["thumb_image"] = base64.b64encode(message["thumb_image"]) - - messages.append(message) - - cur.close() - conn.close() - - self.log.info( - "Extracted a total of %d WhatsApp messages containing links", len(messages) - ) - self.results = messages - - def run(self) -> None: - self._adb_connect() - - try: - self._adb_process_file(os.path.join("/", WHATSAPP_PATH), self._parse_db) - except Exception as exc: - self.log.error(exc) - - self._adb_disconnect() diff --git a/src/mvt/ios/modules/fs/cache_files.py b/src/mvt/ios/modules/fs/cache_files.py index a8fdfe651..2a49f66df 100644 --- a/src/mvt/ios/modules/fs/cache_files.py +++ b/src/mvt/ios/modules/fs/cache_files.py @@ -54,19 +54,18 @@ def check_indicators(self) -> None: if not self.indicators: return - self.alertstore.alerts = {} for key, values in self.results.items(): for value in values: ioc_match = self.indicators.check_url(value["url"]) if ioc_match: + value["cache_file"] = key value["matched_indicator"] = ioc_match.ioc - # XXX: Finish converting this method - if key not in self.alertstore.alerts: - self.alertstore.alerts[key] = [ - value, - ] - else: - self.alertstore.alerts[key].append(value) + self.alertstore.critical( + ioc_match.message, + value.get("isodate", ""), + value, + matched_indicator=ioc_match.ioc, + ) def _process_cache_file(self, file_path): self.log.info("Processing cache file at path: %s", file_path) diff --git a/tests/ios_fs/test_cache_files.py b/tests/ios_fs/test_cache_files.py new file mode 100644 index 000000000..08af2d9d5 --- /dev/null +++ b/tests/ios_fs/test_cache_files.py @@ -0,0 +1,37 @@ +# 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 mvt.common.indicators import Indicators +from mvt.ios.modules.fs.cache_files import CacheFiles + + +class TestCacheFiles: + def test_detection(self, indicator_file): + m = CacheFiles( + results={ + "Library/Caches/example/Cache.db": [ + { + "entry_id": 1, + "version": 1, + "hash_value": 123, + "storage_policy": 0, + "url": "http://example.com/thisisbad", + "isodate": "2026-01-01 00:00:00.000000", + } + ] + } + ) + ind = Indicators(log=logging.getLogger()) + ind.parse_stix2(indicator_file) + m.indicators = ind + + m.check_indicators() + + assert len(m.alertstore.alerts) == 1 + alert = m.alertstore.alerts[0] + assert alert.event["cache_file"] == "Library/Caches/example/Cache.db" + assert alert.event["url"] == "http://example.com/thisisbad" + assert alert.matched_indicator is not None From 14531f5f64c49d9a80ebe2a2615d236c7a9ac6c2 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Wed, 22 Apr 2026 15:55:24 +0200 Subject: [PATCH 30/37] Fail removed check-adb command --- docs/android/adb.md | 51 ++++++++----------------- docs/android/download_apks.md | 28 -------------- docs/android/methodology.md | 6 +-- mkdocs.yml | 2 - src/mvt/android/cli.py | 1 + tests/test_check_android_adb_removed.py | 16 ++++++++ 6 files changed, 34 insertions(+), 70 deletions(-) delete mode 100644 docs/android/download_apks.md create mode 100644 tests/test_check_android_adb_removed.py diff --git a/docs/android/adb.md b/docs/android/adb.md index 42bf02305..29a0e2857 100644 --- a/docs/android/adb.md +++ b/docs/android/adb.md @@ -1,50 +1,29 @@ -# Check over ADB +# ADB Analysis Removed -In order to check an Android device over the [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) you will first need to install [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools). If you have installed [Android Studio](https://developer.android.com/studio/) you should already have access to `adb` and other utilities. +The ability to analyze Android devices directly over the [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) has been removed from MVT. -While many Linux distributions already package Android Platform Tools (for example `android-platform-tools-base` on Debian), it is preferable to install the most recent version from the official website. Packaged versions might be outdated and incompatible with most recent Android handsets. - -Next you will need to enable debugging on the Android device you are testing. [Please follow the official instructions on how to do so.](https://developer.android.com/studio/command-line/adb) - -## Connecting over USB - -The easiest way to check the device is over a USB transport. You will need to have USB debugging enabled and the device plugged into your computer. If everything is configured appropriately you should see your device when launching the command `adb devices`. - -Now you can try launching MVT with: +Use [AndroidQF](https://github.com/mvt-project/androidqf) to collect forensic artifacts from an Android device, then analyze the collected output with: ```bash -mvt-android check-adb --output /path/to/results +mvt-android check-androidqf /path/to/androidqf-output ``` -!!! warning - The `check-adb` command is deprecated and will be removed in a future release. - Whenever possible, prefer acquiring device data using the AndroidQF project (https://github.com/mvt-project/androidqf/) and then analyze those acquisitions with MVT. - - Running `mvt-android check-adb` will also emit a runtime deprecation warning advising you to migrate to AndroidQF. - -If you have previously started an adb daemon MVT will alert you and require you to kill it with `adb kill-server` and relaunch the command. +For a standalone Android bug report, use: -!!! warning - - The `mvt-android check-adb` command has been deprecated and removed from MVT. - -The ability to analyze Android devices over ADB (`mvt-android check-adb`) has been removed from MVT due to several technical and forensic limitations. +```bash +mvt-android check-bugreport /path/to/bugreport.zip +``` -## Reasons for Deprecation +## Reasons for Removal 1. **Inconsistent Data Collection Across Devices** - Android devices vary significantly in their system architecture, security policies, and available diagnostic logs. This inconsistency makes it difficult to ensure that MVT can reliably collect necessary forensic data across all devices. + Android devices vary significantly in system architecture, security policy, and diagnostic log availability. This made it difficult to collect reliable forensic data across devices. 2. **Incomplete Forensic Data Acquisition** - The `check-adb` command did not retrieve a full forensic snapshot of all available data on the device. For example, critical logs such as the **full bugreport** were not systematically collected, leading to potential gaps in forensic analysis. This can be a serious problem in scenarios where the analyst only had one time access to the Android device. - -4. **Code Duplication and Difficulty Ensuring Consistent Behavior Across Sources** - Similar forensic data such as "dumpsys" logs were being loaded and parsed by MVT's ADB, AndroidQF and Bugreport commands. Multiple modules were needed to handle each source format which created duplication leading to inconsistent - behavior and difficulties in maintaining the code base. - -5. **Alignment with iOS Workflow** - MVT’s forensic workflow for iOS relies on pre-extracted artifacts, such as iTunes backups or filesystem dumps, rather than preforming commands or interactions directly on a live device. Removing the ADB functionality ensures a more consistent methodology across both Android and iOS mobile forensic. + Direct ADB analysis did not retrieve a complete forensic snapshot. Critical artifacts such as full bug reports could be missing. -## Alternative: Using AndroidQF for Forensic Data Collection +3. **Duplicated Analysis Paths** + Similar artifacts were parsed through separate ADB, AndroidQF, and bugreport modules, which made behavior harder to keep consistent. -To replace the deprecated ADB-based approach, forensic analysts should use [AndroidQF](https://github.com/mvt-project/androidqf) for comprehensive data collection, followed by MVT for forensic analysis. The workflow is outlined in the MVT [Android methodology](./methodology.md) +4. **Workflow Consistency** + MVT now focuses on analyzing acquired artifacts rather than interacting with live devices directly. diff --git a/docs/android/download_apks.md b/docs/android/download_apks.md deleted file mode 100644 index b7ea886bc..000000000 --- a/docs/android/download_apks.md +++ /dev/null @@ -1,28 +0,0 @@ -# Downloading APKs from an Android phone - -MVT allows you to attempt to download all available installed packages (APKs) from a device in order to further inspect them and potentially identify any which might be malicious in nature. - -You can do so by launching the following command: - -```bash -mvt-android download-apks --output /path/to/folder -``` - -It might take several minutes to complete. - -!!! info - MVT will likely warn you it was unable to download certain installed packages. There is no reason to be alarmed: this is typically expected behavior when MVT attempts to download a system package it has no privileges to access. - -Optionally, you can decide to enable lookups of the SHA256 hash of all the extracted APKs on [VirusTotal](https://www.virustotal.com). While these lookups do not provide any conclusive assessment on all of the extracted APKs, they might highlight any known malicious ones: - -```bash -MVT_VT_API_KEY= mvt-android download-apks --output /path/to/folder --virustotal -``` - -Please note that in order to use VirusTotal lookups you are required to provide your own API key through the `MVT_VT_API_KEY` environment variable. You should also note that VirusTotal enforces strict API usage. Be mindful that MVT might consume your hourly search quota. - -In case you have a previous extraction of APKs you want to later check against VirusTotal, you can do so with the following arguments: - -```bash -MVT_VT_API_KEY= mvt-android download-apks --from-file /path/to/folder/apks.json --virustotal -``` diff --git a/docs/android/methodology.md b/docs/android/methodology.md index 958f4475a..7d7a0190f 100644 --- a/docs/android/methodology.md +++ b/docs/android/methodology.md @@ -38,11 +38,9 @@ By separating artifact collection from forensic analysis, this approach ensures For more information, refer to the [AndroidQF project documentation](https://github.com/mvt-project/androidqf). -## Check the device over Android Debug Bridge +## Android Debug Bridge analysis removed -The ability to analyze Android devices over ADB (`mvt-android check-adb`) has been removed from MVT. - -See the [Android ADB documentation](./adb.md) for more information. +The ability to analyze Android devices directly over ADB has been removed from MVT. Use AndroidQF for device acquisition and `mvt-android check-androidqf` for analysis. ## Check an Android Backup (SMS messages) diff --git a/mkdocs.yml b/mkdocs.yml index 7918ec608..ab4b34bd8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,9 +42,7 @@ nav: - Records extracted by mvt-ios: "ios/records.md" - MVT for Android: - Android Forensic Methodology: "android/methodology.md" - - Check over ADB: "android/adb.md" - Check an Android Backup (SMS messages): "android/backup.md" - - Download APKs: "android/download_apks.md" - Indicators of Compromise: "iocs.md" - Development: "development.md" - License: "license.md" diff --git a/src/mvt/android/cli.py b/src/mvt/android/cli.py index f194a4518..96c1c7d65 100644 --- a/src/mvt/android/cli.py +++ b/src/mvt/android/cli.py @@ -96,6 +96,7 @@ def version(): @click.pass_context def check_adb(ctx): log.error(HELP_MSG_CHECK_ADB_REMOVED_DESCRIPTION) + ctx.exit(1) # ============================================================================== diff --git a/tests/test_check_android_adb_removed.py b/tests/test_check_android_adb_removed.py new file mode 100644 index 000000000..51984e6cb --- /dev/null +++ b/tests/test_check_android_adb_removed.py @@ -0,0 +1,16 @@ +# 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 click.testing import CliRunner + +from mvt.android.cli import check_adb + + +class TestCheckAndroidADBRemovedCommand: + def test_check_adb_exits_nonzero(self): + runner = CliRunner() + result = runner.invoke(check_adb) + + assert result.exit_code == 1 From a15858e4326cceeb4e536d37e265fa3407f77ca7 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Wed, 22 Apr 2026 16:03:33 +0200 Subject: [PATCH 31/37] Fix alert serialization and logging --- src/mvt/android/artifacts/dumpsys_packages.py | 2 -- src/mvt/android/artifacts/mounts.py | 8 ++--- .../android/modules/androidqf/aqf_files.py | 2 -- .../android/modules/androidqf/aqf_packages.py | 9 ----- .../modules/androidqf/root_binaries.py | 1 - src/mvt/common/command.py | 3 +- .../modules/backup/configuration_profiles.py | 2 -- src/mvt/ios/modules/backup/profile_events.py | 1 - src/mvt/ios/modules/fs/analytics.py | 1 - src/mvt/ios/modules/fs/filesystem.py | 2 -- src/mvt/ios/modules/fs/safari_favicon.py | 1 - src/mvt/ios/modules/fs/shutdownlog.py | 2 -- src/mvt/ios/modules/mixed/calendar.py | 1 - src/mvt/ios/modules/mixed/locationd.py | 5 --- .../mixed/webkit_resource_load_statistics.py | 1 - src/mvt/ios/modules/net_base.py | 2 -- tests/android_androidqf/test_mounts.py | 33 +++++++++++++++++++ tests/common/test_command.py | 23 +++++++++++++ 18 files changed, 62 insertions(+), 37 deletions(-) create mode 100644 tests/common/test_command.py diff --git a/src/mvt/android/artifacts/dumpsys_packages.py b/src/mvt/android/artifacts/dumpsys_packages.py index 41891e0fa..f0cd854b8 100644 --- a/src/mvt/android/artifacts/dumpsys_packages.py +++ b/src/mvt/android/artifacts/dumpsys_packages.py @@ -22,7 +22,6 @@ def check_indicators(self) -> None: "", result, ) - self.alertstore.log_latest() continue if not self.indicators: @@ -33,7 +32,6 @@ def check_indicators(self) -> None: self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) - self.alertstore.log_latest() def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: records = [] diff --git a/src/mvt/android/artifacts/mounts.py b/src/mvt/android/artifacts/mounts.py index d4370ca64..da430a708 100644 --- a/src/mvt/android/artifacts/mounts.py +++ b/src/mvt/android/artifacts/mounts.py @@ -180,18 +180,18 @@ def check_indicators(self) -> None: ioc = self.indicators.check_file_path(mount.get("mount_point", "")) if ioc: self.alertstore.critical( - f"Mount point matches indicator: {mount.get('mount_point', '')}", + ioc.message, "", mount, - matched_indicator=ioc, + matched_indicator=ioc.ioc, ) # Check device paths for indicators ioc = self.indicators.check_file_path(mount.get("device", "")) if ioc: self.alertstore.critical( - f"Device path matches indicator: {mount.get('device', '')}", + ioc.message, "", mount, - matched_indicator=ioc, + matched_indicator=ioc.ioc, ) diff --git a/src/mvt/android/modules/androidqf/aqf_files.py b/src/mvt/android/modules/androidqf/aqf_files.py index 941de9f2f..0200db4e4 100644 --- a/src/mvt/android/modules/androidqf/aqf_files.py +++ b/src/mvt/android/modules/androidqf/aqf_files.py @@ -92,7 +92,6 @@ def check_indicators(self) -> None: self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) - self.alertstore.log_latest() continue # NOTE: Update with final path used for Android collector. @@ -107,7 +106,6 @@ def check_indicators(self) -> None: msg = f'Found {file_type}file at suspicious path "{result["path"]}"' self.alertstore.high(msg, "", result) - self.alertstore.log_latest() if result.get("sha256", "") == "": continue diff --git a/src/mvt/android/modules/androidqf/aqf_packages.py b/src/mvt/android/modules/androidqf/aqf_packages.py index 6fa4ef5f1..207a7ea20 100644 --- a/src/mvt/android/modules/androidqf/aqf_packages.py +++ b/src/mvt/android/modules/androidqf/aqf_packages.py @@ -49,7 +49,6 @@ def check_indicators(self) -> None: "", result, ) - self.alertstore.log_latest() continue # Detections for apps installed via unusual methods. @@ -59,21 +58,18 @@ def check_indicators(self) -> None: "", result, ) - self.alertstore.log_latest() elif result["installer"] in BROWSER_INSTALLERS: self.alertstore.medium( f'Found a package installed via a browser (installer="{result["installer"]}"): "{result["name"]}"', "", result, ) - self.alertstore.log_latest() elif result["installer"] == "null" and result["system"] is False: self.alertstore.high( f'Found a non-system package installed via adb or another method: "{result["name"]}"', "", result, ) - self.alertstore.log_latest() elif result["installer"] in PLAY_STORE_INSTALLERS: pass @@ -85,7 +81,6 @@ def check_indicators(self) -> None: "", result, ) - self.alertstore.log_latest() if result["name"] in SYSTEM_UPDATE_PACKAGES and package_disabled: self.alertstore.high( @@ -93,7 +88,6 @@ def check_indicators(self) -> None: "", result, ) - self.alertstore.log_latest() if not self.indicators: continue @@ -103,7 +97,6 @@ def check_indicators(self) -> None: self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) - self.alertstore.log_latest() for package_file in result.get("files", []): ioc_match = self.indicators.check_file_hash( @@ -113,7 +106,6 @@ def check_indicators(self) -> None: self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) - self.alertstore.log_latest() if "certificate" not in package_file: continue @@ -131,7 +123,6 @@ def check_indicators(self) -> None: result, matched_indicator=ioc_match.ioc, ) - self.alertstore.log_latest() break def run(self) -> None: diff --git a/src/mvt/android/modules/androidqf/root_binaries.py b/src/mvt/android/modules/androidqf/root_binaries.py index 7a2cb3406..81e8173b8 100644 --- a/src/mvt/android/modules/androidqf/root_binaries.py +++ b/src/mvt/android/modules/androidqf/root_binaries.py @@ -51,7 +51,6 @@ def check_indicators(self) -> None: "", result, ) - self.alertstore.log_latest() if self.results: self.log.warning( diff --git a/src/mvt/common/command.py b/src/mvt/common/command.py index ee317e25d..03d2b00ca 100644 --- a/src/mvt/common/command.py +++ b/src/mvt/common/command.py @@ -19,6 +19,7 @@ from .indicators import Indicators from .module import EncryptedBackupError, MVTModule, run_module, save_timeline from .utils import ( + CustomJSONEncoder, convert_datetime_to_iso, generate_hashes_from_path, get_sha256_from_file_path, @@ -141,7 +142,7 @@ def _store_alerts(self) -> None: alerts_path = os.path.join(self.results_path, "alerts.json") with open(alerts_path, "w+", encoding="utf-8") as handle: - json.dump(alerts, handle, indent=4) + json.dump(alerts, handle, indent=4, cls=CustomJSONEncoder) def _store_alerts_timeline(self) -> None: if not self.results_path: diff --git a/src/mvt/ios/modules/backup/configuration_profiles.py b/src/mvt/ios/modules/backup/configuration_profiles.py index 7aa635b09..2b8a87a4f 100644 --- a/src/mvt/ios/modules/backup/configuration_profiles.py +++ b/src/mvt/ios/modules/backup/configuration_profiles.py @@ -77,7 +77,6 @@ def check_indicators(self) -> None: self.alertstore.critical( warning_message, "", result, matched_indicator=ioc_match.ioc ) - self.alertstore.log_latest() continue # Highlight suspicious configuration profiles which may be used @@ -85,7 +84,6 @@ def check_indicators(self) -> None: if payload_content["PayloadType"] in ["com.apple.notificationsettings"]: warning_message = f'Found a potentially suspicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with payload type {payload_content["PayloadType"]}' self.alertstore.medium(warning_message, "", result) - self.alertstore.log_latest() continue def run(self) -> None: diff --git a/src/mvt/ios/modules/backup/profile_events.py b/src/mvt/ios/modules/backup/profile_events.py index 83d82af9b..443b9b1b6 100644 --- a/src/mvt/ios/modules/backup/profile_events.py +++ b/src/mvt/ios/modules/backup/profile_events.py @@ -59,7 +59,6 @@ def check_indicators(self) -> None: for result in self.results: message = f'On {result.get("timestamp")} process "{result.get("process")}" started operation "{result.get("operation")}" of profile "{result.get("profile_id")}"' self.alertstore.low(message, result.get("timestamp") or "", result) - self.alertstore.log_latest() if not self.indicators: return diff --git a/src/mvt/ios/modules/fs/analytics.py b/src/mvt/ios/modules/fs/analytics.py index 1d6d96192..b2eda1b27 100644 --- a/src/mvt/ios/modules/fs/analytics.py +++ b/src/mvt/ios/modules/fs/analytics.py @@ -71,7 +71,6 @@ def check_indicators(self) -> None: self.alertstore.critical( warning_message, "", new_result, matched_indicator=ioc_match.ioc ) - self.alertstore.log_latest() continue ioc_match = self.indicators.check_url(value) diff --git a/src/mvt/ios/modules/fs/filesystem.py b/src/mvt/ios/modules/fs/filesystem.py index c77764189..463722002 100644 --- a/src/mvt/ios/modules/fs/filesystem.py +++ b/src/mvt/ios/modules/fs/filesystem.py @@ -59,7 +59,6 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_file_path(result["path"]) if ioc_match: self.alertstore.high(ioc_match.message, "", result) - self.alertstore.log_latest() # If we are instructed to run fast, we skip the rest. if self.module_options.get("fast_mode", None): @@ -68,7 +67,6 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_file_path_process(result["path"]) if ioc_match: self.alertstore.high(ioc_match.message, "", result) - self.alertstore.log_latest() def run(self) -> None: for root, dirs, files in os.walk(self.target_path): diff --git a/src/mvt/ios/modules/fs/safari_favicon.py b/src/mvt/ios/modules/fs/safari_favicon.py index cffb26b94..c5c078b3d 100644 --- a/src/mvt/ios/modules/fs/safari_favicon.py +++ b/src/mvt/ios/modules/fs/safari_favicon.py @@ -65,7 +65,6 @@ def check_indicators(self) -> None: self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) - self.alertstore.log_latest() def _process_favicon_db(self, file_path): conn = self._open_sqlite_db(file_path) diff --git a/src/mvt/ios/modules/fs/shutdownlog.py b/src/mvt/ios/modules/fs/shutdownlog.py index 242617163..71353338c 100644 --- a/src/mvt/ios/modules/fs/shutdownlog.py +++ b/src/mvt/ios/modules/fs/shutdownlog.py @@ -60,7 +60,6 @@ def check_indicators(self) -> None: self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) - self.alertstore.log_latest() continue for ioc in self.indicators.get_iocs("processes"): @@ -73,7 +72,6 @@ def check_indicators(self) -> None: result, matched_indicator=ioc, ) - self.alertstore.log_latest() continue def process_shutdownlog(self, content): diff --git a/src/mvt/ios/modules/mixed/calendar.py b/src/mvt/ios/modules/mixed/calendar.py index 75a811d30..a5adbf7f1 100644 --- a/src/mvt/ios/modules/mixed/calendar.py +++ b/src/mvt/ios/modules/mixed/calendar.py @@ -84,7 +84,6 @@ def check_indicators(self) -> None: "", result, ) - self.alertstore.log_latest() def _parse_calendar_db(self): """ diff --git a/src/mvt/ios/modules/mixed/locationd.py b/src/mvt/ios/modules/mixed/locationd.py index b16c0a19a..6bfbcd305 100644 --- a/src/mvt/ios/modules/mixed/locationd.py +++ b/src/mvt/ios/modules/mixed/locationd.py @@ -90,7 +90,6 @@ def check_indicators(self) -> None: "", result, ) - self.alertstore.log_latest() continue if "BundleId" in result: @@ -102,7 +101,6 @@ def check_indicators(self) -> None: "", result, ) - self.alertstore.log_latest() if "BundlePath" in result: ioc_match = self.indicators.check_file_path(result["BundlePath"]) @@ -113,7 +111,6 @@ def check_indicators(self) -> None: "", result, ) - self.alertstore.log_latest() continue if "Executable" in result: @@ -125,7 +122,6 @@ def check_indicators(self) -> None: "", result, ) - self.alertstore.log_latest() continue if "Registered" in result: @@ -141,7 +137,6 @@ def check_indicators(self) -> None: "", result, ) - self.alertstore.log_latest() continue def _extract_locationd_entries(self, file_path): diff --git a/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py b/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py index cae6a03e5..644d91c33 100644 --- a/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py +++ b/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py @@ -72,7 +72,6 @@ def check_indicators(self) -> None: self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) - self.alertstore.log_latest() def _process_observations_db(self, db_path: str, domain: str, path: str) -> None: self.log.info( diff --git a/src/mvt/ios/modules/net_base.py b/src/mvt/ios/modules/net_base.py index dcc9679d8..211818f1f 100644 --- a/src/mvt/ios/modules/net_base.py +++ b/src/mvt/ios/modules/net_base.py @@ -264,7 +264,6 @@ def check_manipulated(self): result["live_isodate"], result, ) - self.alertstore.log_latest() # Set manipulated proc timestamp so it appears in timeline. result["first_isodate"] = result["isodate"] = result["live_isodate"] @@ -296,7 +295,6 @@ def find_deleted(self): previous_proc["first_isodate"], previous_proc, ) - self.alertstore.log_latest() missing_procs[proc_id] = { "proc_id": proc_id, diff --git a/tests/android_androidqf/test_mounts.py b/tests/android_androidqf/test_mounts.py index d36511acc..d66925d9c 100644 --- a/tests/android_androidqf/test_mounts.py +++ b/tests/android_androidqf/test_mounts.py @@ -6,6 +6,7 @@ import logging from pathlib import Path +from mvt.common.indicators import Indicator, IndicatorMatch from mvt.common.module import run_module from ..utils import get_android_androidqf, list_files @@ -72,6 +73,38 @@ def concat_values(entry): (("by-name/data" in s or "/data" in s) and "rw" in s) for s in concatenated ), f"No data-like tokens (data + rw) found in parsed results: {concatenated}" + def test_mount_ioc_alert_uses_indicator(self): + from mvt.android.artifacts.mounts import Mounts as MountsArtifact + + indicator = Indicator( + value="/system", + type="file_path", + name="TestMalware", + stix2_file_name="indicators.stix2", + ) + m = MountsArtifact() + m.indicators = type( + "MountIndicators", + (), + { + "check_file_path": lambda self, path: IndicatorMatch( + ioc=indicator, message="matched file path" + ) + if path == "/system" + else None + }, + )() + + m.parse("/dev/block/by-name/system on /system type ext4 (rw,seclabel)") + m.check_indicators() + + indicator_alerts = [ + alert for alert in m.alertstore.alerts if alert.matched_indicator + ] + assert len(indicator_alerts) == 1 + assert indicator_alerts[0].matched_indicator == indicator + assert indicator_alerts[0].message == "matched file path" + class TestAndroidqfMountsModule: def test_androidqf_module_no_mounts_file(self): diff --git a/tests/common/test_command.py b/tests/common/test_command.py new file mode 100644 index 000000000..fcd62a01d --- /dev/null +++ b/tests/common/test_command.py @@ -0,0 +1,23 @@ +# 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 json + +from mvt.common.command import Command + + +class TestCommand: + def test_store_alerts_handles_bytes(self, tmp_path): + cmd = Command(results_path=str(tmp_path)) + cmd.alertstore.medium( + "bytes event", + "", + {"payload": b"\xa8\xa9"}, + ) + + cmd._store_alerts() + + alerts = json.loads((tmp_path / "alerts.json").read_text()) + assert alerts[0]["event"]["payload"] == "\\xa8\\xa9" From 0f5a2e854e2d918c2633efd094dc82593416e9f6 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Wed, 22 Apr 2026 16:12:03 +0200 Subject: [PATCH 32/37] Close sqlite connections in iOS modules --- src/mvt/ios/decrypt.py | 4 +- src/mvt/ios/modules/base.py | 22 ++- src/mvt/ios/modules/fs/cache_files.py | 42 +++--- src/mvt/ios/modules/mixed/contacts.py | 43 +++--- src/mvt/ios/modules/mixed/interactionc.py | 49 ++++--- .../ios/modules/mixed/safari_browserstate.py | 137 +++++++++--------- .../mixed/webkit_resource_load_statistics.py | 54 +++---- tests/ios_backup/test_manifest.py | 21 +++ 8 files changed, 210 insertions(+), 162 deletions(-) diff --git a/src/mvt/ios/decrypt.py b/src/mvt/ios/decrypt.py index 607725491..620ee7e40 100644 --- a/src/mvt/ios/decrypt.py +++ b/src/mvt/ios/decrypt.py @@ -46,14 +46,16 @@ def is_encrypted(backup_path: str) -> bool: """ conn = sqlite3.connect(os.path.join(backup_path, "Manifest.db")) - cur = conn.cursor() try: + cur = conn.cursor() cur.execute("SELECT fileID FROM Files LIMIT 1;") except sqlite3.DatabaseError: return True else: log.critical("The backup does not seem encrypted!") return False + finally: + conn.close() def _process_file( self, relative_path: str, domain: str, item, file_id: str, item_folder: str diff --git a/src/mvt/ios/modules/base.py b/src/mvt/ios/modules/base.py index 72260326b..3ff550977 100644 --- a/src/mvt/ios/modules/base.py +++ b/src/mvt/ios/modules/base.py @@ -122,6 +122,8 @@ def _get_backup_files_from_manifest( base_sql = "SELECT fileID, domain, relativePath FROM Files WHERE " + conn: Optional[sqlite3.Connection] = None + cur: Optional[sqlite3.Cursor] = None try: conn = self._open_sqlite_db(manifest_db_path) cur = conn.cursor() @@ -141,15 +143,23 @@ def _get_backup_files_from_manifest( cur.execute(f"{base_sql} relativePath = ?;", (relative_path,)) elif domain: cur.execute(f"{base_sql} domain = ?;", (domain,)) + records = [ + { + "file_id": row[0], + "domain": row[1], + "relative_path": row[2], + } + for row in cur + ] except Exception as exc: raise DatabaseCorruptedError(f"failed to query Manifest.db: {exc}") from exc + finally: + if cur: + cur.close() + if conn: + conn.close() - for row in cur: - yield { - "file_id": row[0], - "domain": row[1], - "relative_path": row[2], - } + return iter(records) def _get_backup_file_from_id(self, file_id: str) -> Union[str, None]: if not self.target_path: diff --git a/src/mvt/ios/modules/fs/cache_files.py b/src/mvt/ios/modules/fs/cache_files.py index 2a49f66df..2fa563b0b 100644 --- a/src/mvt/ios/modules/fs/cache_files.py +++ b/src/mvt/ios/modules/fs/cache_files.py @@ -74,25 +74,29 @@ def _process_cache_file(self, file_path): cur = conn.cursor() try: - cur.execute("SELECT * FROM cfurl_cache_response;") - except sqlite3.OperationalError: - return - - key_name = os.path.relpath(file_path, self.target_path) - if key_name not in self.results: - self.results[key_name] = [] - - for row in cur: - self.results[key_name].append( - { - "entry_id": row[0], - "version": row[1], - "hash_value": row[2], - "storage_policy": row[3], - "url": row[4], - "isodate": row[5], - } - ) + try: + cur.execute("SELECT * FROM cfurl_cache_response;") + except sqlite3.OperationalError: + return + + key_name = os.path.relpath(file_path, self.target_path) + if key_name not in self.results: + self.results[key_name] = [] + + for row in cur: + self.results[key_name].append( + { + "entry_id": row[0], + "version": row[1], + "hash_value": row[2], + "storage_policy": row[3], + "url": row[4], + "isodate": row[5], + } + ) + finally: + cur.close() + conn.close() def run(self) -> None: self.results: dict = {} diff --git a/src/mvt/ios/modules/mixed/contacts.py b/src/mvt/ios/modules/mixed/contacts.py index 1966ac15b..e2ed3ddf6 100644 --- a/src/mvt/ios/modules/mixed/contacts.py +++ b/src/mvt/ios/modules/mixed/contacts.py @@ -51,30 +51,31 @@ def run(self) -> None: conn = self._open_sqlite_db(self.file_path) cur = conn.cursor() try: - cur.execute( + try: + cur.execute( + """ + SELECT + multi.value, person.first, person.middle, person.last, + person.organization + FROM ABPerson person, ABMultiValue multi + WHERE person.rowid = multi.record_id and multi.value not null + ORDER by person.rowid ASC; """ - SELECT - multi.value, person.first, person.middle, person.last, - person.organization - FROM ABPerson person, ABMultiValue multi - WHERE person.rowid = multi.record_id and multi.value not null - ORDER by person.rowid ASC; - """ - ) - except sqlite3.OperationalError as e: - self.log.info("Error while reading the contact table: %s", e) - return None - names = [description[0] for description in cur.description] + ) + except sqlite3.OperationalError as e: + self.log.info("Error while reading the contact table: %s", e) + return None + names = [description[0] for description in cur.description] - for row in cur: - new_contact = {} - for index, value in enumerate(row): - new_contact[names[index]] = value + for row in cur: + new_contact = {} + for index, value in enumerate(row): + new_contact[names[index]] = value - self.results.append(new_contact) - - cur.close() - conn.close() + self.results.append(new_contact) + finally: + cur.close() + conn.close() self.log.info( "Extracted a total of %d contacts from the address book", len(self.results) diff --git a/src/mvt/ios/modules/mixed/interactionc.py b/src/mvt/ios/modules/mixed/interactionc.py index 078ac6a68..ba07bcda4 100644 --- a/src/mvt/ios/modules/mixed/interactionc.py +++ b/src/mvt/ios/modules/mixed/interactionc.py @@ -291,37 +291,38 @@ def run(self) -> None: cur = conn.cursor() try: - cur.execute(QUERIES[0]) - except sqlite3.OperationalError: try: - cur.execute(QUERIES[1]) + cur.execute(QUERIES[0]) except sqlite3.OperationalError: try: - cur.execute(QUERIES[2]) + cur.execute(QUERIES[1]) except sqlite3.OperationalError: try: - cur.execute(QUERIES[3]) - except sqlite3.OperationalError as e: - self.log.info( - "Error while reading the InteractionC table: %s", e - ) - return None + cur.execute(QUERIES[2]) + except sqlite3.OperationalError: + try: + cur.execute(QUERIES[3]) + except sqlite3.OperationalError as e: + self.log.info( + "Error while reading the InteractionC table: %s", e + ) + return None - names = [description[0] for description in cur.description] - for item in cur: - entry = {} - for index, value in enumerate(item): - if names[index] in self.timestamps: - if value is None or isinstance(value, str): - entry[names[index]] = value + names = [description[0] for description in cur.description] + for item in cur: + entry = {} + for index, value in enumerate(item): + if names[index] in self.timestamps: + if value is None or isinstance(value, str): + entry[names[index]] = value + else: + entry[names[index]] = convert_mactime_to_iso(value) else: - entry[names[index]] = convert_mactime_to_iso(value) - else: - entry[names[index]] = value - - self.results.append(entry) + entry[names[index]] = value - cur.close() - conn.close() + self.results.append(entry) + finally: + cur.close() + conn.close() self.log.info("Extracted a total of %d InteractionC events", len(self.results)) diff --git a/src/mvt/ios/modules/mixed/safari_browserstate.py b/src/mvt/ios/modules/mixed/safari_browserstate.py index 021f690e9..047ae8b0e 100644 --- a/src/mvt/ios/modules/mixed/safari_browserstate.py +++ b/src/mvt/ios/modules/mixed/safari_browserstate.py @@ -92,82 +92,87 @@ def _process_browser_state_db(self, db_path): cur = conn.cursor() try: - cur.execute( - """ - SELECT - tabs.title, - tabs.url, - tabs.user_visible_url, - tabs.last_viewed_time, - tab_sessions.session_data - FROM tabs - JOIN tab_sessions ON tabs.uuid = tab_sessions.tab_uuid - ORDER BY tabs.last_viewed_time; - """ - ) - except sqlite3.OperationalError: - # Old version iOS <12 likely try: cur.execute( """ SELECT - title, url, user_visible_url, last_viewed_time, session_data + tabs.title, + tabs.url, + tabs.user_visible_url, + tabs.last_viewed_time, + tab_sessions.session_data FROM tabs - ORDER BY last_viewed_time; + JOIN tab_sessions ON tabs.uuid = tab_sessions.tab_uuid + ORDER BY tabs.last_viewed_time; """ ) - except sqlite3.OperationalError as e: - self.log.error(f"Error executing query: {e}") - - for row in cur: - session_entries = [] - - if row[4]: - # Skip a 4 byte header before the plist content. - session_plist = row[4][4:] - session_data = {} + except sqlite3.OperationalError: + # Old version iOS <12 likely try: - session_data = plistlib.load(io.BytesIO(session_plist)) - session_data = keys_bytes_to_string(session_data) - except plistlib.InvalidFileException: - pass - - if "SessionHistoryEntries" in session_data.get("SessionHistory", {}): - for session_entry in session_data["SessionHistory"].get( - "SessionHistoryEntries" - ): - self._session_history_count += 1 - - data_length = 0 - if "SessionHistoryEntryData" in session_entry: - data_length = len( - session_entry.get("SessionHistoryEntryData") + cur.execute( + """ + SELECT + title, url, user_visible_url, last_viewed_time, session_data + FROM tabs + ORDER BY last_viewed_time; + """ + ) + except sqlite3.OperationalError as e: + self.log.error(f"Error executing query: {e}") + return + + for row in cur: + session_entries = [] + + if row[4]: + # Skip a 4 byte header before the plist content. + session_plist = row[4][4:] + session_data = {} + try: + session_data = plistlib.load(io.BytesIO(session_plist)) + session_data = keys_bytes_to_string(session_data) + except plistlib.InvalidFileException: + pass + + if "SessionHistoryEntries" in session_data.get("SessionHistory", {}): + for session_entry in session_data["SessionHistory"].get( + "SessionHistoryEntries" + ): + self._session_history_count += 1 + + data_length = 0 + if "SessionHistoryEntryData" in session_entry: + data_length = len( + session_entry.get("SessionHistoryEntryData") + ) + + session_entries.append( + { + "entry_title": session_entry.get( + "SessionHistoryEntryOriginalURL" + ), + "entry_url": session_entry.get( + "SessionHistoryEntryURL" + ), + "data_length": data_length, + } ) - session_entries.append( - { - "entry_title": session_entry.get( - "SessionHistoryEntryOriginalURL" - ), - "entry_url": session_entry.get( - "SessionHistoryEntryURL" - ), - "data_length": data_length, - } - ) - - self.results.append( - { - "tab_title": row[0], - "tab_url": row[1], - "tab_visible_url": row[2], - "last_viewed_timestamp": convert_mactime_to_iso(row[3]), - "session_data": session_entries, - "safari_browser_state_db": os.path.relpath( - db_path, self.target_path - ), - } - ) + self.results.append( + { + "tab_title": row[0], + "tab_url": row[1], + "tab_visible_url": row[2], + "last_viewed_timestamp": convert_mactime_to_iso(row[3]), + "session_data": session_entries, + "safari_browser_state_db": os.path.relpath( + db_path, self.target_path + ), + } + ) + finally: + cur.close() + conn.close() def run(self) -> None: if self.is_backup: diff --git a/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py b/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py index 644d91c33..76415899d 100644 --- a/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py +++ b/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py @@ -85,32 +85,36 @@ def _process_observations_db(self, db_path: str, domain: str, path: str) -> None cur = conn.cursor() try: - # FIXME: table contains extra fields with timestamp here - cur.execute( + try: + # FIXME: table contains extra fields with timestamp here + cur.execute( + """ + SELECT + domainID, + registrableDomain, + lastSeen, + hadUserInteraction + from ObservedDomains; """ - SELECT - domainID, - registrableDomain, - lastSeen, - hadUserInteraction - from ObservedDomains; - """ - ) - except sqlite3.OperationalError: - return - - for row in cur: - self.results.append( - { - "domain_id": row[0], - "registrable_domain": row[1], - "last_seen": row[2], - "had_user_interaction": bool(row[3]), - "last_seen_isodate": convert_unix_to_iso(row[2]), - "domain": domain, - "path": path, - } - ) + ) + except sqlite3.OperationalError: + return + + for row in cur: + self.results.append( + { + "domain_id": row[0], + "registrable_domain": row[1], + "last_seen": row[2], + "had_user_interaction": bool(row[3]), + "last_seen_isodate": convert_unix_to_iso(row[2]), + "domain": domain, + "path": path, + } + ) + finally: + cur.close() + conn.close() if len(self.results) > 0: self.log.info( diff --git a/tests/ios_backup/test_manifest.py b/tests/ios_backup/test_manifest.py index 44df42dec..aaa747d0d 100644 --- a/tests/ios_backup/test_manifest.py +++ b/tests/ios_backup/test_manifest.py @@ -3,15 +3,36 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ +import gc import logging +import warnings from mvt.common.indicators import Indicators from mvt.common.module import run_module +from mvt.ios.modules.base import IOSExtraction from mvt.ios.modules.backup.manifest import Manifest from ..utils import get_ios_backup_folder +class TestIOSExtraction: + def test_get_backup_files_from_manifest_closes_connection(self): + m = IOSExtraction(target_path=get_ios_backup_folder()) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always", ResourceWarning) + files = list(m._get_backup_files_from_manifest(domain="CameraRollDomain")) + gc.collect() + + assert files + assert not [ + warning + for warning in caught + if issubclass(warning.category, ResourceWarning) + and "unclosed database" in str(warning.message) + ] + + class TestManifestModule: def test_manifest(self): m = Manifest(target_path=get_ios_backup_folder()) From 644a3c46cd1f61556bfc33e063a0ead1dd826e59 Mon Sep 17 00:00:00 2001 From: tes Date: Wed, 22 Apr 2026 18:39:55 -0300 Subject: [PATCH 33/37] Fix DEBUG messages not reaching handlers, save_to_json for dictionary results and TypeError on mixed event_time types in safary_history --- src/mvt/common/module.py | 11 +++++++---- src/mvt/common/utils.py | 6 +++++- src/mvt/ios/modules/mixed/safari_history.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/mvt/common/module.py b/src/mvt/common/module.py index c299645ee..f555e409b 100644 --- a/src/mvt/common/module.py +++ b/src/mvt/common/module.py @@ -109,10 +109,13 @@ def save_to_json(self) -> None: name = self.get_slug() if self.results: - converted_results = [ - asdict(result) if is_dataclass(result) else result - for result in self.results - ] + if isinstance(self.results, dict): + converted_results = self.results + else: + converted_results = [ + asdict(result) if is_dataclass(result) else result + for result in self.results + ] results_file_name = f"{name}.json" results_json_path = os.path.join(self.results_path, results_file_name) with open(results_json_path, "w", encoding="utf-8") as handle: diff --git a/src/mvt/common/utils.py b/src/mvt/common/utils.py index 500fd8539..78352d44d 100644 --- a/src/mvt/common/utils.py +++ b/src/mvt/common/utils.py @@ -10,6 +10,7 @@ import logging import os import re +from dataclasses import asdict, is_dataclass from typing import Any, Iterator, Union from .log import MVTLogHandler @@ -30,6 +31,9 @@ class CustomJSONEncoder(json.JSONEncoder): """ def default(self, o): + # Unwrap dataclass instances (such as Indicator) to dict. Skip class itself. + if is_dataclass(o) and not isinstance(o, type): + return asdict(o) if isinstance(o, bytes): # Decode as utf-8, replace any invalid UTF-8 bytes with escaped hex return o.decode("utf-8", errors="backslashreplace") @@ -235,7 +239,7 @@ def init_logging(verbose: bool = False): Initialise logging for the MVT module """ log = logging.getLogger("mvt") - log.setLevel(logging.INFO) + log.setLevel(logging.DEBUG) consoleHandler = MVTLogHandler() consoleHandler.setFormatter(logging.Formatter("%(message)s")) if verbose: diff --git a/src/mvt/ios/modules/mixed/safari_history.py b/src/mvt/ios/modules/mixed/safari_history.py index 5fe748fc7..bd4784c5c 100644 --- a/src/mvt/ios/modules/mixed/safari_history.py +++ b/src/mvt/ios/modules/mixed/safari_history.py @@ -102,7 +102,7 @@ def _find_injections(self): if elapsed_time.seconds == 0: self.alertstore.medium( f"Redirect took less than a second! ({elapsed_ms} milliseconds)", - result["timestamp"], + convert_mactime_to_iso(result["timestamp"]), result, ) From 3ae82fc5bf427f69c8f1a43283bb40ce074de4ac Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Mon, 27 Apr 2026 14:59:17 +0200 Subject: [PATCH 34/37] add matched_indicator via alertstore instead of directly modifying json objects --- .../modules/bugreport/dumpsys_receivers.py | 1 - src/mvt/common/alerts.py | 7 +++ .../modules/backup/configuration_profiles.py | 1 - src/mvt/ios/modules/backup/manifest.py | 2 - src/mvt/ios/modules/backup/profile_events.py | 2 - src/mvt/ios/modules/fs/analytics.py | 2 - src/mvt/ios/modules/fs/cache_files.py | 1 - src/mvt/ios/modules/fs/filesystem.py | 8 ++- src/mvt/ios/modules/fs/shutdownlog.py | 1 - src/mvt/ios/modules/fs/webkit_base.py | 1 - src/mvt/ios/modules/mixed/applications.py | 2 - src/mvt/ios/modules/mixed/calendar.py | 1 - src/mvt/ios/modules/mixed/chrome_favicon.py | 1 - src/mvt/ios/modules/mixed/chrome_history.py | 1 - src/mvt/ios/modules/mixed/firefox_favicon.py | 1 - src/mvt/ios/modules/mixed/firefox_history.py | 1 - src/mvt/ios/modules/mixed/idstatuscache.py | 1 - src/mvt/ios/modules/mixed/locationd.py | 10 ++-- .../ios/modules/mixed/osanalytics_addaily.py | 1 - .../ios/modules/mixed/safari_browserstate.py | 2 - src/mvt/ios/modules/mixed/safari_history.py | 1 - src/mvt/ios/modules/mixed/shortcuts.py | 1 - src/mvt/ios/modules/mixed/sms.py | 1 - src/mvt/ios/modules/mixed/sms_attachments.py | 8 ++- .../mixed/webkit_resource_load_statistics.py | 1 - .../mixed/webkit_session_resource_log.py | 1 - src/mvt/ios/modules/mixed/whatsapp.py | 1 - src/mvt/ios/modules/net_base.py | 6 ++- tests/common/test_alerts.py | 49 +++++++++++++++++++ tests/ios_backup/test_datausage.py | 12 +++++ 30 files changed, 89 insertions(+), 39 deletions(-) create mode 100644 tests/common/test_alerts.py diff --git a/src/mvt/android/modules/bugreport/dumpsys_receivers.py b/src/mvt/android/modules/bugreport/dumpsys_receivers.py index 3c365910e..1c4a02878 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_receivers.py +++ b/src/mvt/android/modules/bugreport/dumpsys_receivers.py @@ -43,7 +43,6 @@ def check_indicators(self) -> None: # return IoC if the stix2 process name a substring of the receiver name ioc_match = self.indicators.check_receiver_prefix(receiver_name) if ioc_match: - self.results[result][0]["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", diff --git a/src/mvt/common/alerts.py b/src/mvt/common/alerts.py index 004d4227e..64f1334a8 100644 --- a/src/mvt/common/alerts.py +++ b/src/mvt/common/alerts.py @@ -207,6 +207,13 @@ def as_json(self) -> List[Dict[str, Any]]: alert_dict = asdict(alert) # This is required because an Enum is not JSON serializable. alert_dict["level"] = alert.level.name + if isinstance(alert_dict.get("event"), dict): + event_matched_indicator = alert_dict["event"].pop( + "matched_indicator", None + ) + if alert_dict["matched_indicator"] is None: + alert_dict["matched_indicator"] = event_matched_indicator + alerts.append(alert_dict) return alerts diff --git a/src/mvt/ios/modules/backup/configuration_profiles.py b/src/mvt/ios/modules/backup/configuration_profiles.py index 2b8a87a4f..07a9c0c5e 100644 --- a/src/mvt/ios/modules/backup/configuration_profiles.py +++ b/src/mvt/ios/modules/backup/configuration_profiles.py @@ -73,7 +73,6 @@ def check_indicators(self) -> None: ) if ioc_match: warning_message = f'Found a known malicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with UUID "{result["plist"]["PayloadUUID"]}"' - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( warning_message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/backup/manifest.py b/src/mvt/ios/modules/backup/manifest.py index 4074ff533..0eaa6bd1f 100644 --- a/src/mvt/ios/modules/backup/manifest.py +++ b/src/mvt/ios/modules/backup/manifest.py @@ -103,7 +103,6 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_file_path("/" + result["relative_path"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.high( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) @@ -119,7 +118,6 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_url(part) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.high( f'Found mention of domain "{ioc_match.ioc.value}" in a backup file with path: {rel_path}', "", diff --git a/src/mvt/ios/modules/backup/profile_events.py b/src/mvt/ios/modules/backup/profile_events.py index 443b9b1b6..7bd7b94ec 100644 --- a/src/mvt/ios/modules/backup/profile_events.py +++ b/src/mvt/ios/modules/backup/profile_events.py @@ -66,7 +66,6 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_process(result.get("process") or "") if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) @@ -74,7 +73,6 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_profile(result.get("profile_id") or "") if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/fs/analytics.py b/src/mvt/ios/modules/fs/analytics.py index b2eda1b27..bf5374586 100644 --- a/src/mvt/ios/modules/fs/analytics.py +++ b/src/mvt/ios/modules/fs/analytics.py @@ -67,7 +67,6 @@ def check_indicators(self) -> None: if ioc_match: warning_message = f'Found mention of a malicious process "{value}" in {result["artifact"]} file at {result["isodate"]}' new_result = copy.copy(result) - new_result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( warning_message, "", new_result, matched_indicator=ioc_match.ioc ) @@ -76,7 +75,6 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_url(value) if ioc_match: new_result = copy.copy(result) - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", diff --git a/src/mvt/ios/modules/fs/cache_files.py b/src/mvt/ios/modules/fs/cache_files.py index 2fa563b0b..021924a0c 100644 --- a/src/mvt/ios/modules/fs/cache_files.py +++ b/src/mvt/ios/modules/fs/cache_files.py @@ -59,7 +59,6 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_url(value["url"]) if ioc_match: value["cache_file"] = key - value["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, value.get("isodate", ""), diff --git a/src/mvt/ios/modules/fs/filesystem.py b/src/mvt/ios/modules/fs/filesystem.py index 463722002..de3aa650c 100644 --- a/src/mvt/ios/modules/fs/filesystem.py +++ b/src/mvt/ios/modules/fs/filesystem.py @@ -58,7 +58,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_file_path(result["path"]) if ioc_match: - self.alertstore.high(ioc_match.message, "", result) + self.alertstore.high( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) # If we are instructed to run fast, we skip the rest. if self.module_options.get("fast_mode", None): @@ -66,7 +68,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_file_path_process(result["path"]) if ioc_match: - self.alertstore.high(ioc_match.message, "", result) + self.alertstore.high( + ioc_match.message, "", result, matched_indicator=ioc_match.ioc + ) def run(self) -> None: for root, dirs, files in os.walk(self.target_path): diff --git a/src/mvt/ios/modules/fs/shutdownlog.py b/src/mvt/ios/modules/fs/shutdownlog.py index 71353338c..ac028093d 100644 --- a/src/mvt/ios/modules/fs/shutdownlog.py +++ b/src/mvt/ios/modules/fs/shutdownlog.py @@ -65,7 +65,6 @@ def check_indicators(self) -> None: for ioc in self.indicators.get_iocs("processes"): parts = result["client"].split("/") if ioc.value in parts: - result["matched_indicator"] = ioc self.alertstore.critical( f'Found mention of a known malicious process "{ioc.value}" in shutdown.log', "", diff --git a/src/mvt/ios/modules/fs/webkit_base.py b/src/mvt/ios/modules/fs/webkit_base.py index 6d72d8e10..5cccfd521 100644 --- a/src/mvt/ios/modules/fs/webkit_base.py +++ b/src/mvt/ios/modules/fs/webkit_base.py @@ -20,7 +20,6 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_url(result["url"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/applications.py b/src/mvt/ios/modules/mixed/applications.py index bbbab7680..dac18868f 100644 --- a/src/mvt/ios/modules/mixed/applications.py +++ b/src/mvt/ios/modules/mixed/applications.py @@ -77,7 +77,6 @@ def check_indicators(self) -> None: result["softwareVersionBundleId"] ) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( f"Malicious application {result['softwareVersionBundleId']} identified", "", @@ -90,7 +89,6 @@ def check_indicators(self) -> None: result["softwareVersionBundleId"] ) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( f"Malicious application {result['softwareVersionBundleId']} identified", "", diff --git a/src/mvt/ios/modules/mixed/calendar.py b/src/mvt/ios/modules/mixed/calendar.py index a5adbf7f1..dddd11c02 100644 --- a/src/mvt/ios/modules/mixed/calendar.py +++ b/src/mvt/ios/modules/mixed/calendar.py @@ -71,7 +71,6 @@ def check_indicators(self) -> None: if result["participant_email"] and self.indicators: ioc_match = self.indicators.check_email(result["participant_email"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/chrome_favicon.py b/src/mvt/ios/modules/mixed/chrome_favicon.py index cc44a680a..297d48d00 100644 --- a/src/mvt/ios/modules/mixed/chrome_favicon.py +++ b/src/mvt/ios/modules/mixed/chrome_favicon.py @@ -61,7 +61,6 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_url(result["icon_url"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/chrome_history.py b/src/mvt/ios/modules/mixed/chrome_history.py index fa3b88f92..d9d50f326 100644 --- a/src/mvt/ios/modules/mixed/chrome_history.py +++ b/src/mvt/ios/modules/mixed/chrome_history.py @@ -62,7 +62,6 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_url(result["url"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/firefox_favicon.py b/src/mvt/ios/modules/mixed/firefox_favicon.py index 3357a59d7..8df6165ec 100644 --- a/src/mvt/ios/modules/mixed/firefox_favicon.py +++ b/src/mvt/ios/modules/mixed/firefox_favicon.py @@ -63,7 +63,6 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_url(result.get("history_url", "")) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/firefox_history.py b/src/mvt/ios/modules/mixed/firefox_history.py index d61f19cb4..8b13279d9 100644 --- a/src/mvt/ios/modules/mixed/firefox_history.py +++ b/src/mvt/ios/modules/mixed/firefox_history.py @@ -63,7 +63,6 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_url(result["url"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/idstatuscache.py b/src/mvt/ios/modules/mixed/idstatuscache.py index b1e06dda9..34d5bda0b 100644 --- a/src/mvt/ios/modules/mixed/idstatuscache.py +++ b/src/mvt/ios/modules/mixed/idstatuscache.py @@ -65,7 +65,6 @@ def check_indicators(self) -> None: email = result["user"][7:].strip("'") ioc_match = self.indicators.check_email(email) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/locationd.py b/src/mvt/ios/modules/mixed/locationd.py index 6bfbcd305..b022bc87e 100644 --- a/src/mvt/ios/modules/mixed/locationd.py +++ b/src/mvt/ios/modules/mixed/locationd.py @@ -84,43 +84,43 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_process(proc_name) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.high( f"Found a suspicious process name in LocationD entry {result['package']}", "", result, + matched_indicator=ioc_match.ioc, ) continue if "BundleId" in result: ioc_match = self.indicators.check_process(result["BundleId"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.high( f"Found a suspicious process name in LocationD entry {result['package']}", "", result, + matched_indicator=ioc_match.ioc, ) if "BundlePath" in result: ioc_match = self.indicators.check_file_path(result["BundlePath"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.high( f"Found a known malicious domain in LocationD entry {result['package']}", "", result, + matched_indicator=ioc_match.ioc, ) continue if "Executable" in result: ioc_match = self.indicators.check_file_path(result["Executable"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.high( f"Found a suspicious file path in LocationD entry {result['Executable']}", "", result, + matched_indicator=ioc_match.ioc, ) continue @@ -131,11 +131,11 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_file_path(result["Registered"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.high( f"Found a suspicious file path in LocationD entry {result['Registered']}", "", result, + matched_indicator=ioc_match.ioc, ) continue diff --git a/src/mvt/ios/modules/mixed/osanalytics_addaily.py b/src/mvt/ios/modules/mixed/osanalytics_addaily.py index 719197583..7f3243f50 100644 --- a/src/mvt/ios/modules/mixed/osanalytics_addaily.py +++ b/src/mvt/ios/modules/mixed/osanalytics_addaily.py @@ -64,7 +64,6 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_process(result["package"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/safari_browserstate.py b/src/mvt/ios/modules/mixed/safari_browserstate.py index 047ae8b0e..20a594c21 100644 --- a/src/mvt/ios/modules/mixed/safari_browserstate.py +++ b/src/mvt/ios/modules/mixed/safari_browserstate.py @@ -65,7 +65,6 @@ def check_indicators(self) -> None: if "tab_url" in result: ioc_match = self.indicators.check_url(result["tab_url"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) @@ -78,7 +77,6 @@ def check_indicators(self) -> None: if "entry_url" in session_entry: ioc_match = self.indicators.check_url(session_entry["entry_url"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", diff --git a/src/mvt/ios/modules/mixed/safari_history.py b/src/mvt/ios/modules/mixed/safari_history.py index bd4784c5c..4583ffbb1 100644 --- a/src/mvt/ios/modules/mixed/safari_history.py +++ b/src/mvt/ios/modules/mixed/safari_history.py @@ -115,7 +115,6 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_url(result["url"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/shortcuts.py b/src/mvt/ios/modules/mixed/shortcuts.py index 762769f7e..9ca273912 100644 --- a/src/mvt/ios/modules/mixed/shortcuts.py +++ b/src/mvt/ios/modules/mixed/shortcuts.py @@ -79,7 +79,6 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_urls(result["action_urls"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/sms.py b/src/mvt/ios/modules/mixed/sms.py index 78f37def7..bf6890b31 100644 --- a/src/mvt/ios/modules/mixed/sms.py +++ b/src/mvt/ios/modules/mixed/sms.py @@ -92,7 +92,6 @@ def check_indicators(self) -> None: message_links = check_for_links(result.get("text", "")) ioc_match = self.indicators.check_urls(message_links) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/sms_attachments.py b/src/mvt/ios/modules/mixed/sms_attachments.py index 3d63eff1b..981bf39b1 100644 --- a/src/mvt/ios/modules/mixed/sms_attachments.py +++ b/src/mvt/ios/modules/mixed/sms_attachments.py @@ -65,8 +65,12 @@ def check_indicators(self) -> None: if self.indicators: ioc_match = self.indicators.check_file_path(attachment["filename"]) if ioc_match: - attachment["matched_indicator"] = ioc_match.ioc - self.alertstore.high(ioc_match.message, "", attachment) + self.alertstore.high( + ioc_match.message, + "", + attachment, + matched_indicator=ioc_match.ioc, + ) if ( attachment["filename"].startswith("/var/tmp/") diff --git a/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py b/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py index 76415899d..6499a6a52 100644 --- a/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py +++ b/src/mvt/ios/modules/mixed/webkit_resource_load_statistics.py @@ -68,7 +68,6 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_url(result["registrable_domain"]) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/webkit_session_resource_log.py b/src/mvt/ios/modules/mixed/webkit_session_resource_log.py index 27d320ad4..b56cc5e3f 100644 --- a/src/mvt/ios/modules/mixed/webkit_session_resource_log.py +++ b/src/mvt/ios/modules/mixed/webkit_session_resource_log.py @@ -89,7 +89,6 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_urls(all_origins) if ioc_match: - entry["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", entry, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/mixed/whatsapp.py b/src/mvt/ios/modules/mixed/whatsapp.py index c1c04087d..e28d4cb1a 100644 --- a/src/mvt/ios/modules/mixed/whatsapp.py +++ b/src/mvt/ios/modules/mixed/whatsapp.py @@ -64,7 +64,6 @@ def check_indicators(self) -> None: for result in self.results: ioc_match = self.indicators.check_urls(result.get("links", [])) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( ioc_match.message, "", result, matched_indicator=ioc_match.ioc ) diff --git a/src/mvt/ios/modules/net_base.py b/src/mvt/ios/modules/net_base.py index 211818f1f..302123eb6 100644 --- a/src/mvt/ios/modules/net_base.py +++ b/src/mvt/ios/modules/net_base.py @@ -346,7 +346,9 @@ def check_indicators(self) -> None: ioc_match = self.indicators.check_process(proc_name) if ioc_match: - result["matched_indicator"] = ioc_match.ioc self.alertstore.critical( - ioc_match.message, result["first_isodate"], result + ioc_match.message, + result["first_isodate"], + result, + matched_indicator=ioc_match.ioc, ) diff --git a/tests/common/test_alerts.py b/tests/common/test_alerts.py new file mode 100644 index 000000000..a90f5e1e2 --- /dev/null +++ b/tests/common/test_alerts.py @@ -0,0 +1,49 @@ +# 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/ + +from mvt.common.alerts import Alert, AlertLevel, AlertStore + + +def test_as_json_promotes_nested_matched_indicator(): + indicator = {"value": "com.apple.weather", "type": "processes"} + alertstore = AlertStore() + alertstore.add( + Alert( + level=AlertLevel.CRITICAL, + module="datausage", + message="Matched indicator", + event_time="2026-01-01 00:00:00", + event={ + "proc_name": "WeatherWidget/com.apple.weather", + "matched_indicator": indicator, + }, + ) + ) + + alert = alertstore.as_json()[0] + + assert alert["matched_indicator"] == indicator + assert "matched_indicator" not in alert["event"] + + +def test_as_json_removes_nested_matched_indicator_when_parent_exists(): + event_indicator = {"value": "nested", "type": "processes"} + alert_indicator = {"value": "parent", "type": "processes"} + alertstore = AlertStore() + alertstore.add( + Alert( + level=AlertLevel.CRITICAL, + module="manifest", + message="Matched indicator", + event_time="2026-01-01 00:00:00", + event={"path": "/tmp/example", "matched_indicator": event_indicator}, + matched_indicator=alert_indicator, + ) + ) + + alert = alertstore.as_json()[0] + + assert alert["matched_indicator"] == alert_indicator + assert "matched_indicator" not in alert["event"] diff --git a/tests/ios_backup/test_datausage.py b/tests/ios_backup/test_datausage.py index 8ab25f812..17b07629d 100644 --- a/tests/ios_backup/test_datausage.py +++ b/tests/ios_backup/test_datausage.py @@ -36,3 +36,15 @@ def test_detection(self, indicator_file): alert for alert in m.alertstore.alerts if alert.level == AlertLevel.CRITICAL ] assert len(critical_alerts) == 2 + assert all( + "matched_indicator" not in alert.event for alert in critical_alerts + ) + serialized_alerts = [ + alert + for alert in m.alertstore.as_json() + if alert["matched_indicator"] is not None + ] + assert len(serialized_alerts) == 2 + assert all( + "matched_indicator" not in alert["event"] for alert in serialized_alerts + ) From a2ae28435d5fe4aa1ce3d7e24e852dcfa5ae4e0b Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Mon, 27 Apr 2026 17:51:45 +0200 Subject: [PATCH 35/37] Alert on battery daily uninstall and downgrade --- .../artifacts/dumpsys_battery_daily.py | 15 ++++---- .../test_artifact_dumpsys_battery_daily.py | 34 +++++++++++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/mvt/android/artifacts/dumpsys_battery_daily.py b/src/mvt/android/artifacts/dumpsys_battery_daily.py index ae95c16ef..2bf909b51 100644 --- a/src/mvt/android/artifacts/dumpsys_battery_daily.py +++ b/src/mvt/android/artifacts/dumpsys_battery_daily.py @@ -91,10 +91,10 @@ def parse(self, output: str) -> None: # Check for uninstall (version 0) if vers_nr == "0": - self.log.warning( - "Detected uninstall of package %s (vers 0) on %s", - package_name, + self.alertstore.medium( + f"Detected uninstall of package {package_name} (vers 0)", daily["from"], + update_record, ) # Check for downgrade elif package_name in package_versions: @@ -104,12 +104,11 @@ def parse(self, output: str) -> None: if current_vers < previous_vers: update_record["action"] = "downgrade" update_record["previous_vers"] = str(previous_vers) - self.log.warning( - "Detected downgrade of package %s from vers %d to vers %d on %s", - package_name, - previous_vers, - current_vers, + self.alertstore.medium( + f"Detected downgrade of package {package_name} " + f"from vers {previous_vers} to vers {current_vers}", daily["from"], + update_record, ) except ValueError: # If version numbers aren't integers, skip comparison diff --git a/tests/android/test_artifact_dumpsys_battery_daily.py b/tests/android/test_artifact_dumpsys_battery_daily.py index 26917f968..a909df2f4 100644 --- a/tests/android/test_artifact_dumpsys_battery_daily.py +++ b/tests/android/test_artifact_dumpsys_battery_daily.py @@ -5,6 +5,7 @@ import logging from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtifact +from mvt.common.alerts import AlertLevel from mvt.common.indicators import Indicators from ..utils import get_artifact @@ -35,3 +36,36 @@ def test_ioc_check(self, indicator_file): assert len(dba.alertstore.alerts) == 0 dba.check_indicators() assert len(dba.alertstore.alerts) == 1 + + def test_uninstall_and_downgrade_create_medium_alerts(self): + dba = DumpsysBatteryDailyArtifact() + dba.parse( + """ + Daily from 2022-08-16-15-56-39 to 2022-08-17-01-15-45: + Update com.example.app vers=10 + Update com.example.removed vers=0 + Daily from 2022-08-17-15-56-39 to 2022-08-18-01-15-45: + Update com.example.app vers=9 +""" + ) + + assert len(dba.results) == 3 + assert len(dba.alertstore.alerts) == 2 + + uninstall_alert, downgrade_alert = dba.alertstore.alerts + assert uninstall_alert.level == AlertLevel.MEDIUM + assert uninstall_alert.message == ( + "Detected uninstall of package com.example.removed (vers 0)" + ) + assert uninstall_alert.event_time == "2022-08-16" + assert uninstall_alert.event["package_name"] == "com.example.removed" + assert uninstall_alert.event["vers"] == "0" + + assert downgrade_alert.level == AlertLevel.MEDIUM + assert downgrade_alert.message == ( + "Detected downgrade of package com.example.app from vers 10 to vers 9" + ) + assert downgrade_alert.event_time == "2022-08-17" + assert downgrade_alert.event["package_name"] == "com.example.app" + assert downgrade_alert.event["action"] == "downgrade" + assert downgrade_alert.event["previous_vers"] == "10" From b570668e9846907597f1a535bb8d1e8acd54ef43 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Tue, 28 Apr 2026 06:11:26 +0200 Subject: [PATCH 36/37] Lower alert severity to medium for suspicious items --- src/mvt/android/artifacts/mounts.py | 2 +- src/mvt/android/modules/androidqf/aqf_packages.py | 6 +++--- src/mvt/ios/modules/mixed/applications.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mvt/android/artifacts/mounts.py b/src/mvt/android/artifacts/mounts.py index da430a708..a35d889fa 100644 --- a/src/mvt/android/artifacts/mounts.py +++ b/src/mvt/android/artifacts/mounts.py @@ -154,7 +154,7 @@ def check_indicators(self) -> None: ): continue suspicious_mounts.append(mount) - self.alertstore.high( + self.alertstore.medium( f"Suspicious mount options found for {mount_point}: {', '.join(suspicious_opts)}", "", mount, diff --git a/src/mvt/android/modules/androidqf/aqf_packages.py b/src/mvt/android/modules/androidqf/aqf_packages.py index 207a7ea20..0ec0122bc 100644 --- a/src/mvt/android/modules/androidqf/aqf_packages.py +++ b/src/mvt/android/modules/androidqf/aqf_packages.py @@ -65,7 +65,7 @@ def check_indicators(self) -> None: result, ) elif result["installer"] == "null" and result["system"] is False: - self.alertstore.high( + self.alertstore.medium( f'Found a non-system package installed via adb or another method: "{result["name"]}"', "", result, @@ -76,14 +76,14 @@ def check_indicators(self) -> None: # Check for disabled security or software update packages. package_disabled = result.get("disabled", None) if result["name"] in SECURITY_PACKAGES and package_disabled: - self.alertstore.high( + self.alertstore.medium( f'Security package "{result["name"]}" disabled on the phone', "", result, ) if result["name"] in SYSTEM_UPDATE_PACKAGES and package_disabled: - self.alertstore.high( + self.alertstore.medium( f'System OTA update package "{result["name"]}" disabled on the phone', "", result, diff --git a/src/mvt/ios/modules/mixed/applications.py b/src/mvt/ios/modules/mixed/applications.py index dac18868f..d508910f4 100644 --- a/src/mvt/ios/modules/mixed/applications.py +++ b/src/mvt/ios/modules/mixed/applications.py @@ -66,7 +66,7 @@ def check_indicators(self) -> None: for result in self.results: if self.indicators: if "softwareVersionBundleId" not in result: - self.alertstore.high( + self.alertstore.medium( "Suspicious application identified without softwareVersionBundleId", "", result, From e93e825fc7be5e276213e62984b86c74f04823fc Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Tue, 28 Apr 2026 14:20:27 +0200 Subject: [PATCH 37/37] Switch version to 2026.4.28 CalVer --- src/mvt/common/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mvt/common/version.py b/src/mvt/common/version.py index b31392c9d..a96d1e279 100644 --- a/src/mvt/common/version.py +++ b/src/mvt/common/version.py @@ -3,4 +3,4 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ -MVT_VERSION = "3.0.0" +MVT_VERSION = "2026.4.28"