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/11] 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 e274040..c1444f4 100644 --- a/src/mvt/android/cmd_check_adb.py +++ b/src/mvt/android/cmd_check_adb.py @@ -7,6 +7,7 @@ import logging 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 @@ class CmdAndroidCheckADB(Command): 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 e079807..628548c 100644 --- a/src/mvt/android/cmd_check_androidqf.py +++ b/src/mvt/android/cmd_check_androidqf.py @@ -10,58 +10,182 @@ from pathlib import Path 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): - if self.format == "zip": - module.from_zip_file(self.archive, self.files) + 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) + + 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) + 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() + 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 2a68900..e366d2b 100644 --- a/src/mvt/android/cmd_check_backup.py +++ b/src/mvt/android/cmd_check_backup.py @@ -20,6 +20,7 @@ from mvt.android.parsers.backup import ( 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 @@ class CmdAndroidCheckBackup(Command): 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 @@ class CmdAndroidCheckBackup(Command): 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 @@ class CmdAndroidCheckBackup(Command): 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 @@ class CmdAndroidCheckBackup(Command): 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 08a266f..a3d9b3b 100644 --- a/src/mvt/android/cmd_check_bugreport.py +++ b/src/mvt/android/cmd_check_bugreport.py @@ -11,6 +11,7 @@ from zipfile import ZipFile 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 @@ class CmdAndroidCheckBugreport(Command): 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 d871059..43e6210 100644 --- a/src/mvt/android/modules/androidqf/base.py +++ b/src/mvt/android/modules/androidqf/base.py @@ -37,11 +37,11 @@ class AndroidQFModule(MVTModule): 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 5141bfe..29238ba 100644 --- a/src/mvt/android/modules/backup/base.py +++ b/src/mvt/android/modules/backup/base.py @@ -37,10 +37,7 @@ class BackupExtraction(MVTModule): 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 @@ class BackupExtraction(MVTModule): 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 a75be26..a194a1e 100644 --- a/src/mvt/android/modules/backup/sms.py +++ b/src/mvt/android/modules/backup/sms.py @@ -50,13 +50,13 @@ class SMS(BackupExtraction): 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 77802b2..158bc28 100644 --- a/src/mvt/android/modules/bugreport/base.py +++ b/src/mvt/android/modules/bugreport/base.py @@ -39,9 +39,7 @@ class BugReportModule(MVTModule): 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 696803a..1f8bde5 100644 --- a/src/mvt/common/cmd_check_iocs.py +++ b/src/mvt/common/cmd_check_iocs.py @@ -22,6 +22,8 @@ class CmdCheckIOCS(Command): 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 @@ class CmdCheckIOCS(Command): 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 7f65843..b6d7aaf 100644 --- a/src/mvt/common/command.py +++ b/src/mvt/common/command.py @@ -27,10 +27,12 @@ class Command: 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 @@ class Command: 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 @@ class Command: # 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 @@ class Command: 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 66dfc8e..dcdc013 100644 --- a/src/mvt/ios/cmd_check_backup.py +++ b/src/mvt/ios/cmd_check_backup.py @@ -7,6 +7,7 @@ import logging 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 @@ class CmdIOSCheckBackup(Command): 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 3484138..605bfd0 100644 --- a/src/mvt/ios/cmd_check_fs.py +++ b/src/mvt/ios/cmd_check_fs.py @@ -7,6 +7,7 @@ import logging 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 @@ class CmdIOSCheckFS(Command): 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 29bc8e9..57cb891 100644 --- a/tests/android/test_backup_module.py +++ b/tests/android/test_backup_module.py @@ -22,7 +22,7 @@ class TestBackupModule: for root, subdirs, subfiles in os.walk(os.path.abspath(backup_path)): for fname in subfiles: files.append(os.path.relpath(os.path.join(root, fname), backup_path)) - mod.from_folder(backup_path, files) + mod.from_dir(backup_path, files) run_module(mod) assert len(mod.results) == 2 assert len(mod.results[0]["links"]) == 1 diff --git a/tests/android_androidqf/test_dumpsys_adbstate.py b/tests/android_androidqf/test_dumpsys_adbstate.py index c94fe34..a3ac72e 100644 --- a/tests/android_androidqf/test_dumpsys_adbstate.py +++ b/tests/android_androidqf/test_dumpsys_adbstate.py @@ -17,7 +17,7 @@ class TestDumpsysADBModule: 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 66a45d7..f82f330 100644 --- a/tests/android_androidqf/test_dumpsys_battery_daily.py +++ b/tests/android_androidqf/test_dumpsys_battery_daily.py @@ -17,7 +17,7 @@ class TestDumpsysBatteryDailyModule: 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 8b76459..fd1d0ae 100644 --- a/tests/android_androidqf/test_dumpsys_battery_history.py +++ b/tests/android_androidqf/test_dumpsys_battery_history.py @@ -17,7 +17,7 @@ class TestDumpsysBatteryHistoryModule: 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 83addbf..371a3bc 100644 --- a/tests/android_androidqf/test_dumpsys_dbinfo.py +++ b/tests/android_androidqf/test_dumpsys_dbinfo.py @@ -17,7 +17,7 @@ class TestDumpsysDBInfoModule: 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 8123432..bddc322 100644 --- a/tests/android_androidqf/test_dumpsys_platform_compat.py +++ b/tests/android_androidqf/test_dumpsys_platform_compat.py @@ -17,7 +17,7 @@ class TestDumpsysPlatformCompatModule: 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 1a217d0..b437275 100644 --- a/tests/android_androidqf/test_dumpsysaccessbility.py +++ b/tests/android_androidqf/test_dumpsysaccessbility.py @@ -17,7 +17,7 @@ class TestDumpsysAccessibilityModule: 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 b0649a1..ce74d53 100644 --- a/tests/android_androidqf/test_dumpsysappops.py +++ b/tests/android_androidqf/test_dumpsysappops.py @@ -17,7 +17,7 @@ class TestDumpsysAppOpsModule: 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 798628f..e801b0a 100644 --- a/tests/android_androidqf/test_dumpsyspackages.py +++ b/tests/android_androidqf/test_dumpsyspackages.py @@ -19,7 +19,7 @@ class TestDumpsysPackagesModule: 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 @@ class TestDumpsysPackagesModule: 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 ce4b37a..0a06bab 100644 --- a/tests/android_androidqf/test_dumpsysreceivers.py +++ b/tests/android_androidqf/test_dumpsysreceivers.py @@ -17,7 +17,7 @@ class TestDumpsysReceiversModule: 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 80981a2..de8269c 100644 --- a/tests/android_androidqf/test_files.py +++ b/tests/android_androidqf/test_files.py @@ -18,7 +18,7 @@ class TestAndroidqfFilesAnalysis: 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 4688b73..89fb522 100644 --- a/tests/android_androidqf/test_getprop.py +++ b/tests/android_androidqf/test_getprop.py @@ -20,7 +20,7 @@ class TestAndroidqfGetpropAnalysis: 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 @@ class TestAndroidqfGetpropAnalysis: 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 @@ class TestAndroidqfGetpropAnalysis: 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 a7fce95..d911315 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 8aacb0b..98b5d2a 100644 --- a/tests/android_androidqf/test_processes.py +++ b/tests/android_androidqf/test_processes.py @@ -18,7 +18,7 @@ class TestAndroidqfProcessesAnalysis: 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 c282901..44ee89c 100644 --- a/tests/android_androidqf/test_settings.py +++ b/tests/android_androidqf/test_settings.py @@ -17,7 +17,7 @@ class TestSettingsModule: 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 8bff799..d7433cf 100644 --- a/tests/android_androidqf/test_sms.py +++ b/tests/android_androidqf/test_sms.py @@ -21,7 +21,7 @@ class TestAndroidqfSMSAnalysis: m = SMS(target_path=data_path, log=logging) files = list_files(data_path) parent_path = Path(data_path).absolute().parent.as_posix() - m.from_folder(parent_path, files) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 2 assert len(m.timeline) == 0 @@ -36,7 +36,7 @@ class TestAndroidqfSMSAnalysis: ) files = list_files(data_path) parent_path = Path(data_path).absolute().parent.as_posix() - m.from_folder(parent_path, files) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 1 @@ -52,7 +52,7 @@ class TestAndroidqfSMSAnalysis: ) files = list_files(data_path) parent_path = Path(data_path).absolute().parent.as_posix() - m.from_folder(parent_path, files) + m.from_dir(parent_path, files) run_module(m) assert prompt_mock.call_count == 1 assert len(m.results) == 1 @@ -67,7 +67,7 @@ class TestAndroidqfSMSAnalysis: ) files = list_files(data_path) parent_path = Path(data_path).absolute().parent.as_posix() - m.from_folder(parent_path, files) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 0 assert "Invalid backup password" in caplog.text @@ -82,7 +82,7 @@ class TestAndroidqfSMSAnalysis: ) files = list_files(data_path) parent_path = Path(data_path).absolute().parent.as_posix() - m.from_folder(parent_path, files) + m.from_dir(parent_path, files) run_module(m) assert len(m.results) == 0 assert ( diff --git a/tests/android_bugreport/test_bugreport.py b/tests/android_bugreport/test_bugreport.py index cc9afec..98744a6 100644 --- a/tests/android_bugreport/test_bugreport.py +++ b/tests/android_bugreport/test_bugreport.py @@ -25,7 +25,7 @@ class TestBugreportAnalysis: folder_files.append( os.path.relpath(os.path.join(root, file_name), parent_path) ) - m.from_folder(fpath, folder_files) + m.from_dir(fpath, folder_files) run_module(m) return m diff --git a/tests/test_check_android_androidqf.py b/tests/test_check_android_androidqf.py index 167b5b7..c6e4221 100644 --- a/tests/test_check_android_androidqf.py +++ b/tests/test_check_android_androidqf.py @@ -32,7 +32,8 @@ class TestCheckAndroidqfCommand: path = os.path.join(get_artifact_folder(), "androidqf_encrypted") result = runner.invoke(check_androidqf, [path]) - assert prompt_mock.call_count == 1 + # Called twice, once in AnroidQF SMS module and once in Backup SMS module + assert prompt_mock.call_count == 2 assert result.exit_code == 0 def test_check_encrypted_backup_cli(self, mocker): 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/11] 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 1832134..b2b4368 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 @@ ADB_MODULES = [ 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 a987ae4..0000000 --- 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 5125cbf..0000000 --- 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 0bcd8fd..0000000 --- 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 7a7594e..0000000 --- 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 3a9eee6..0000000 --- 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 aac134c..0000000 --- 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 e6b772b..0000000 --- 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 c4759c4..0000000 --- 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 cdb0af8..c6a3e1f 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 22b832c..90eb3b8 100644 --- a/src/mvt/android/modules/androidqf/files.py +++ b/src/mvt/android/modules/androidqf/aqf_files.py @@ -21,8 +21,13 @@ SUSPICIOUS_PATHS = [ ] -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 e14abd9..35514f8 100644 --- a/src/mvt/android/modules/androidqf/getprop.py +++ b/src/mvt/android/modules/androidqf/aqf_getprop.py @@ -11,7 +11,7 @@ from mvt.android.artifacts.getprop import GetProp as GetPropArtifact 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 b37851d..e5a1410 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 .base import AndroidQFModule 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 1d36777..500b3d4 100644 --- a/src/mvt/android/modules/androidqf/packages.py +++ b/src/mvt/android/modules/androidqf/aqf_packages.py @@ -19,7 +19,7 @@ from mvt.android.utils import ( 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 f2c5e08..3faabb4 100644 --- a/src/mvt/android/modules/androidqf/processes.py +++ b/src/mvt/android/modules/androidqf/aqf_processes.py @@ -11,7 +11,7 @@ from mvt.android.artifacts.processes import Processes as ProcessesArtifact 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 79f55ef..46a70fb 100644 --- a/src/mvt/android/modules/androidqf/settings.py +++ b/src/mvt/android/modules/androidqf/aqf_settings.py @@ -11,7 +11,7 @@ from mvt.android.artifacts.settings import Settings as SettingsArtifact 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 0712ef4..0000000 --- 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 950d0e5..0000000 --- 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 10d8a4d..0000000 --- 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 350b5c8..0000000 --- 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 4a19178..0000000 --- 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 4a4bef3..0000000 --- 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 09c8f6f..0000000 --- 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 8df7144..0000000 --- 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 869c476..0000000 --- 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 9c64d2c..0000000 --- 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 d7e3a9c..893e517 100644 --- a/src/mvt/android/modules/androidqf/sms.py +++ b/src/mvt/android/modules/androidqf/sms.py @@ -19,7 +19,13 @@ from .base import AndroidQFModule 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 b5a1247..1594af9 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 7d30eb0..e141b2f 100644 --- a/src/mvt/android/modules/bugreport/accessibility.py +++ b/src/mvt/android/modules/bugreport/dumpsys_accessibility.py @@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_accessibility import DumpsysAccessibilityArti 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 c2a20dd..a58c6f4 100644 --- a/src/mvt/android/modules/bugreport/activities.py +++ b/src/mvt/android/modules/bugreport/dumpsys_activities.py @@ -13,7 +13,7 @@ from mvt.android.artifacts.dumpsys_package_activities import ( 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 4fb1e7f..96b4796 100644 --- a/src/mvt/android/modules/bugreport/appops.py +++ b/src/mvt/android/modules/bugreport/dumpsys_appops.py @@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_appops import DumpsysAppopsArtifact 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 4fdcf74..7fc8329 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 mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtif 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 968bbbe..729f801 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 mvt.android.artifacts.dumpsys_battery_history import DumpsysBatteryHistoryA 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 780d9fc..73902bb 100644 --- a/src/mvt/android/modules/bugreport/dbinfo.py +++ b/src/mvt/android/modules/bugreport/dumpsys_dbinfo.py @@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_dbinfo import DumpsysDBInfoArtifact 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 106d63c..acec15c 100644 --- a/src/mvt/android/modules/bugreport/getprop.py +++ b/src/mvt/android/modules/bugreport/dumpsys_getprop.py @@ -11,7 +11,7 @@ from mvt.android.artifacts.getprop import GetProp as GetPropArtifact 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 f1b9d63..fccf102 100644 --- a/src/mvt/android/modules/bugreport/packages.py +++ b/src/mvt/android/modules/bugreport/dumpsys_packages.py @@ -12,7 +12,7 @@ from mvt.android.utils import DANGEROUS_PERMISSIONS, DANGEROUS_PERMISSIONS_THRES 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 fadac92..e9d10e6 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.artifacts.dumpsys_platform_compat import DumpsysPlatformCompatA 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 57a87ce..591af2f 100644 --- a/src/mvt/android/modules/bugreport/receivers.py +++ b/src/mvt/android/modules/bugreport/dumpsys_receivers.py @@ -11,7 +11,7 @@ from mvt.android.artifacts.dumpsys_receivers import DumpsysReceiversArtifact 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 a3ac72e..0000000 --- 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 f82f330..0000000 --- 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 fd1d0ae..0000000 --- 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 371a3bc..0000000 --- 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 bddc322..0000000 --- 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 b437275..0000000 --- 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 ce74d53..0000000 --- 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 e801b0a..0000000 --- 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 0a06bab..0000000 --- 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 de8269c..c0d45b5 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 @@ from ..utils import get_android_androidqf, list_files class TestAndroidqfFilesAnalysis: def test_androidqf_files(self): data_path = get_android_androidqf() - m = Files(target_path=data_path, log=logging) + m = AQFFiles(target_path=data_path, log=logging) files = list_files(data_path) parent_path = Path(data_path).absolute().parent.as_posix() m.from_dir(parent_path, files) diff --git a/tests/android_androidqf/test_getprop.py b/tests/android_androidqf/test_getprop.py index 89fb522..3947acd 100644 --- a/tests/android_androidqf/test_getprop.py +++ b/tests/android_androidqf/test_getprop.py @@ -7,7 +7,7 @@ import logging import zipfile from pathlib import Path -from mvt.android.modules.androidqf.getprop import Getprop +from mvt.android.modules.androidqf.aqf_getprop import AQFGetProp from mvt.common.indicators import Indicators from mvt.common.module import run_module @@ -17,7 +17,7 @@ from ..utils import get_android_androidqf, get_artifact, list_files class TestAndroidqfGetpropAnalysis: def test_androidqf_getprop(self): data_path = get_android_androidqf() - m = Getprop(target_path=data_path, log=logging) + m = AQFGetProp(target_path=data_path, log=logging) files = list_files(data_path) parent_path = Path(data_path).absolute().parent.as_posix() m.from_dir(parent_path, files) @@ -30,7 +30,7 @@ class TestAndroidqfGetpropAnalysis: def test_getprop_parsing_zip(self): fpath = get_artifact("androidqf.zip") - m = Getprop(target_path=fpath, log=logging) + m = AQFGetProp(target_path=fpath, log=logging) archive = zipfile.ZipFile(fpath) m.from_zip(archive, archive.namelist()) run_module(m) @@ -42,7 +42,7 @@ class TestAndroidqfGetpropAnalysis: def test_androidqf_getprop_detection(self, indicator_file): data_path = get_android_androidqf() - m = Getprop(target_path=data_path, log=logging) + m = AQFGetProp(target_path=data_path, log=logging) files = list_files(data_path) parent_path = Path(data_path).absolute().parent.as_posix() m.from_dir(parent_path, files) diff --git a/tests/android_androidqf/test_packages.py b/tests/android_androidqf/test_packages.py index d911315..966d8a6 100644 --- a/tests/android_androidqf/test_packages.py +++ b/tests/android_androidqf/test_packages.py @@ -8,7 +8,7 @@ from pathlib import Path import pytest -from mvt.android.modules.androidqf.packages import Packages +from mvt.android.modules.androidqf.aqf_packages import AQFPackages from mvt.common.module import run_module from ..utils import get_android_androidqf, list_files @@ -31,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 98b5d2a..bcd4013 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 @@ from ..utils import get_android_androidqf, list_files class TestAndroidqfProcessesAnalysis: def test_androidqf_processes(self): data_path = get_android_androidqf() - m = Processes(target_path=data_path, log=logging) + m = AQFProcesses(target_path=data_path, log=logging) files = list_files(data_path) parent_path = Path(data_path).absolute().parent.as_posix() m.from_dir(parent_path, files) diff --git a/tests/android_androidqf/test_settings.py b/tests/android_androidqf/test_settings.py index 44ee89c..75527a7 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 @@ from ..utils import get_android_androidqf, list_files class TestSettingsModule: def test_parsing(self): data_path = get_android_androidqf() - m = Settings(target_path=data_path) + m = AQFSettings(target_path=data_path) files = list_files(data_path) parent_path = Path(data_path).absolute().parent.as_posix() m.from_dir(parent_path, files) diff --git a/tests/android_bugreport/test_bugreport.py b/tests/android_bugreport/test_bugreport.py index 98744a6..8abc896 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 @@ class TestBugreportAnalysis: 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 @@ class TestBugreportAnalysis: assert len(detected_by_ioc) == 0 def test_packages_module(self): - m = self.launch_bug_report_module(Packages) + m = self.launch_bug_report_module(DumpsysPackages) assert len(m.results) == 2 assert ( m.results[0]["package_name"] @@ -52,5 +52,5 @@ class TestBugreportAnalysis: assert len(m.results[1]["permissions"]) == 32 def test_getprop_module(self): - m = self.launch_bug_report_module(Getprop) + m = self.launch_bug_report_module(DumpsysGetProp) assert len(m.results) == 0 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/11] 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 628548c..580b5e2 100644 --- a/src/mvt/android/cmd_check_androidqf.py +++ b/src/mvt/android/cmd_check_androidqf.py @@ -161,7 +161,7 @@ class CmdAndroidCheckAndroidQF(Command): 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/11] 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 d5c0660..8e4d070 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. -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: +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 -adb tcpip 5555 -``` +## Reasons for Deprecation -Then you can specify the IP address of the phone with the adb port number to MVT like so: +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. -```bash -mvt-android check-adb --serial 192.168.1.20:5555 --output /path/to/results -``` +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. -Where `192.168.1.20` is the correct IP address of your 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. -## MVT modules requiring root privileges +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. -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! +## Alternative: Using AndroidQF for Forensic Data Collection + +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 e8062c6..958f447 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 -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. +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. -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). +### Why Use 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. +- **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. + +- **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. + +### Workflow for Android Forensic Analysis with AndroidQF + +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 be8631c..d8f4ff8 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 8e9086f..5d23411 100644 --- a/src/mvt/android/cli.py +++ b/src/mvt/android/cli.py @@ -11,20 +11,14 @@ from mvt.common.cmd_check_iocs import CmdCheckIOCS 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.logo import logo 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 + "check-adb", context_settings=CONTEXT_SETTINGS, help=HELP_MSG_CHECK_ADB_REMOVED ) -@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, -) -@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 c1444f4..0000000 --- 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 0cca7ab..3046e36 100644 --- a/src/mvt/common/help.py +++ b/src/mvt/common/help.py @@ -33,19 +33,15 @@ HELP_MSG_CHECK_IOS_BACKUP = "Extract artifacts from an iTunes backup" 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 dd19c1e..b31392c 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 e0749ea..0000000 --- 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/11] 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 eec0824..74e89d8 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 deacb05..0000000 --- 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/11] 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 | 123 +++++++---- 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, 544 insertions(+), 263 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 5d23411..ebdeeff 100644 --- a/src/mvt/android/cli.py +++ b/src/mvt/android/cli.py @@ -37,6 +37,7 @@ from .cmd_check_bugreport import CmdAndroidCheckBugreport 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 580b5e2..e0f49ab 100644 --- a/src/mvt/android/cmd_check_androidqf.py +++ b/src/mvt/android/cmd_check_androidqf.py @@ -67,6 +67,9 @@ class CmdAndroidCheckAndroidQF(Command): 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 @@ class CmdAndroidCheckAndroidQF(Command): 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 @@ class CmdAndroidCheckAndroidQF(Command): 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 0000000..635520d --- /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 b6d7aaf..920f3a0 100644 --- a/src/mvt/common/command.py +++ b/src/mvt/common/command.py @@ -9,16 +9,20 @@ import os 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 @@ class Command: 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 @@ class Command: 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 @@ class Command: 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 @@ class Command: 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 @@ class Command: 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 @@ class Command: 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 @@ class Command: 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 e23a996..adb4483 100644 --- a/src/mvt/common/indicators.py +++ b/src/mvt/common/indicators.py @@ -8,7 +8,8 @@ import json 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 @@ MVT_INDICATORS_FOLDER = os.path.join(MVT_DATA_FOLDER, "indicators") 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: :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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 @@ class Indicators: 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 0000000..498b13d --- /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 1468cf4..e57a9fc 100644 --- a/src/mvt/common/module.py +++ b/src/mvt/common/module.py @@ -8,9 +8,18 @@ import json 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 @@ class MVTModule: 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 @@ class MVTModule: :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 @@ class MVTModule: :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 @@ class MVTModule: 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 @@ class MVTModule: 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 @@ class MVTModule: 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 @@ class MVTModule: """ 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 0000000..f433482 --- /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 3d054f5..500fd85 100644 --- a/src/mvt/common/utils.py +++ b/src/mvt/common/utils.py @@ -12,7 +12,7 @@ import os 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 1d06c96..6ffd106 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 966d8a6..fe6332a 100644 --- a/tests/android_androidqf/test_packages.py +++ b/tests/android_androidqf/test_packages.py @@ -91,7 +91,8 @@ class TestAndroidqfPackages: 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 @@ class TestAndroidqfPackages: 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 @@ class TestAndroidqfPackages: 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/11] 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 72df794..0000000 --- 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/11] 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 | 120 +++++++++--------- 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, 1132 insertions(+), 761 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 fca84df..66b1684 100644 --- a/src/mvt/android/artifacts/dumpsys_accessibility.py +++ b/src/mvt/android/artifacts/dumpsys_accessibility.py @@ -14,10 +14,10 @@ class DumpsysAccessibilityArtifact(AndroidArtifact): 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 8323c8a..36d61c8 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 @@ class DumpsysAppopsArtifact(AndroidArtifact): "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 @@ class DumpsysAppopsArtifact(AndroidArtifact): 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 06b980b..03082f5 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 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact): 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 35e41ec..cd2d6a8 100644 --- a/src/mvt/android/artifacts/dumpsys_battery_history.py +++ b/src/mvt/android/artifacts/dumpsys_battery_history.py @@ -16,10 +16,10 @@ class DumpsysBatteryHistoryArtifact(AndroidArtifact): 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 1064e49..c5f2516 100644 --- a/src/mvt/android/artifacts/dumpsys_dbinfo.py +++ b/src/mvt/android/artifacts/dumpsys_dbinfo.py @@ -20,10 +20,12 @@ class DumpsysDBInfoArtifact(AndroidArtifact): 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 d8d284f..f3b5d2e 100644 --- a/src/mvt/android/artifacts/dumpsys_package_activities.py +++ b/src/mvt/android/artifacts/dumpsys_package_activities.py @@ -12,10 +12,12 @@ class DumpsysPackageActivitiesArtifact(AndroidArtifact): 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 2204180..be59db8 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 e1037f0..012a6e3 100644 --- a/src/mvt/android/artifacts/dumpsys_platform_compat.py +++ b/src/mvt/android/artifacts/dumpsys_platform_compat.py @@ -16,10 +16,10 @@ class DumpsysPlatformCompatArtifact(AndroidArtifact): 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 6ef1c08..75f5afe 100644 --- a/src/mvt/android/artifacts/dumpsys_receivers.py +++ b/src/mvt/android/artifacts/dumpsys_receivers.py @@ -50,10 +50,12 @@ class DumpsysReceiversArtifact(AndroidArtifact): 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 aa2dc25..98b8789 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 6c7030f..053b5fd 100644 --- a/src/mvt/android/artifacts/getprop.py +++ b/src/mvt/android/artifacts/getprop.py @@ -59,13 +59,16 @@ class GetProp(AndroidArtifact): 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 273ac10..7b6f41f 100644 --- a/src/mvt/android/artifacts/processes.py +++ b/src/mvt/android/artifacts/processes.py @@ -58,13 +58,13 @@ class Processes(AndroidArtifact): 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 3827154..76d5506 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 @@ class TombstoneCrashArtifact(AndroidArtifact): 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 @@ class TombstoneCrashArtifact(AndroidArtifact): 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 e366d2b..2c39bae 100644 --- a/src/mvt/android/cmd_check_backup.py +++ b/src/mvt/android/cmd_check_backup.py @@ -11,7 +11,7 @@ import tarfile 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 @@ class CmdAndroidCheckBackup(Command): ) 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 54be2a0..9568e44 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 @@ class ChromeHistory(AndroidExtraction): 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 @@ class ChromeHistory(AndroidExtraction): ) 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 @@ class ChromeHistory(AndroidExtraction): 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 6103357..fa1a6b3 100644 --- a/src/mvt/android/modules/adb/dumpsys_full.py +++ b/src/mvt/android/modules/adb/dumpsys_full.py @@ -8,6 +8,7 @@ import os from typing import Optional from .base import AndroidExtraction +from mvt.common.module_types import ModuleResults class DumpsysFull(AndroidExtraction): @@ -20,7 +21,7 @@ class DumpsysFull(AndroidExtraction): 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 a8a11a2..28b1088 100644 --- a/src/mvt/android/modules/adb/files.py +++ b/src/mvt/android/modules/adb/files.py @@ -9,6 +9,7 @@ import stat 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 @@ class Files(AndroidExtraction): 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 71bface..a9c77be 100644 --- a/src/mvt/android/modules/adb/getprop.py +++ b/src/mvt/android/modules/adb/getprop.py @@ -9,6 +9,7 @@ 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): @@ -21,7 +22,7 @@ class Getprop(GetPropArtifact, AndroidExtraction): 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 bdc8c48..41418a1 100644 --- a/src/mvt/android/modules/adb/logcat.py +++ b/src/mvt/android/modules/adb/logcat.py @@ -8,6 +8,7 @@ import os from typing import Optional from .base import AndroidExtraction +from mvt.common.module_types import ModuleResults class Logcat(AndroidExtraction): @@ -20,7 +21,7 @@ class Logcat(AndroidExtraction): 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 1d9c821..04563ae 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 @@ from mvt.android.utils import ( 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 @@ class Packages(AndroidExtraction): 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 @@ class Packages(AndroidExtraction): ) self._user_needed = False - def serialize(self, record: dict) -> Union[dict, list]: + def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: records = [] timestamps = [ @@ -95,70 +94,71 @@ class Packages(AndroidExtraction): 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) + 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"]) + # @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 = {} + # 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 + # 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 + # if not results: + # continue - positives = results["attributes"]["last_analysis_stats"]["malicious"] - total = len(results["attributes"]["last_analysis_results"]) + # positives = results["attributes"]["last_analysis_stats"]["malicious"] + # total = len(results["attributes"]["last_analysis_results"]) - detections[hashes[i]] = f"{positives}/{total}" + # 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") + # 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"]] + # 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") + # 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) + # table.add_row(*row) - console = Console() - console.print(table) + # 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 1a9f29f..dcdb036 100644 --- a/src/mvt/android/modules/adb/processes.py +++ b/src/mvt/android/modules/adb/processes.py @@ -9,6 +9,7 @@ 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): @@ -21,7 +22,7 @@ class Processes(ProcessesArtifact, AndroidExtraction): 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 6d8350c..0315e23 100644 --- a/src/mvt/android/modules/adb/root_binaries.py +++ b/src/mvt/android/modules/adb/root_binaries.py @@ -7,6 +7,7 @@ import logging from typing import Optional from .base import AndroidExtraction +from mvt.common.module_types import ModuleResults class RootBinaries(AndroidExtraction): @@ -19,7 +20,7 @@ class RootBinaries(AndroidExtraction): 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 a46e362..39925c7 100644 --- a/src/mvt/android/modules/adb/selinux_status.py +++ b/src/mvt/android/modules/adb/selinux_status.py @@ -7,6 +7,7 @@ import logging from typing import Optional from .base import AndroidExtraction +from mvt.common.module_types import ModuleResults class SELinuxStatus(AndroidExtraction): @@ -21,7 +22,7 @@ class SELinuxStatus(AndroidExtraction): 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 416ef7f..dcfc6e5 100644 --- a/src/mvt/android/modules/adb/settings.py +++ b/src/mvt/android/modules/adb/settings.py @@ -7,6 +7,7 @@ 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 @@ -21,7 +22,7 @@ class Settings(SettingsArtifact, AndroidExtraction): 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 673e56a..a69592c 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 @@ class SMS(AndroidExtraction): 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 @@ class SMS(AndroidExtraction): 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 @@ class SMS(AndroidExtraction): 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 28ee170..40f8875 100644 --- a/src/mvt/android/modules/adb/whatsapp.py +++ b/src/mvt/android/modules/adb/whatsapp.py @@ -7,11 +7,16 @@ import base64 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 @@ class Whatsapp(AndroidExtraction): 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 @@ class Whatsapp(AndroidExtraction): 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 90eb3b8..562b2d3 100644 --- a/src/mvt/android/modules/androidqf/aqf_files.py +++ b/src/mvt/android/modules/androidqf/aqf_files.py @@ -10,10 +10,15 @@ import logging 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 @@ class AQFFiles(AndroidQFModule): 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 @@ class AQFFiles(AndroidQFModule): 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 @@ class AQFFiles(AndroidQFModule): 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 @@ class AQFFiles(AndroidQFModule): 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 35514f8..68dca90 100644 --- a/src/mvt/android/modules/androidqf/aqf_getprop.py +++ b/src/mvt/android/modules/androidqf/aqf_getprop.py @@ -9,6 +9,7 @@ from typing import Optional 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 @@ class AQFGetProp(GetPropArtifact, AndroidQFModule): 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 e5a1410..fc46804 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 @@ import logging 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 @@ class AQFLogTimestamps(FileTimestampsArtifact, AndroidQFModule): 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 500b3d4..20b1399 100644 --- a/src/mvt/android/modules/androidqf/aqf_packages.py +++ b/src/mvt/android/modules/androidqf/aqf_packages.py @@ -17,6 +17,7 @@ from mvt.android.utils import ( ) from .base import AndroidQFModule +from mvt.common.module_types import ModuleResults class AQFPackages(AndroidQFModule): @@ -29,7 +30,7 @@ class AQFPackages(AndroidQFModule): 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 @@ class AQFPackages(AndroidQFModule): 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 3faabb4..4c69ca0 100644 --- a/src/mvt/android/modules/androidqf/aqf_processes.py +++ b/src/mvt/android/modules/androidqf/aqf_processes.py @@ -9,6 +9,7 @@ from typing import Optional 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 @@ class AQFProcesses(ProcessesArtifact, AndroidQFModule): 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 46a70fb..1dc3b6e 100644 --- a/src/mvt/android/modules/androidqf/aqf_settings.py +++ b/src/mvt/android/modules/androidqf/aqf_settings.py @@ -9,6 +9,7 @@ from typing import Optional 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 @@ class AQFSettings(SettingsArtifact, AndroidQFModule): 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 43e6210..a784158 100644 --- a/src/mvt/android/modules/androidqf/base.py +++ b/src/mvt/android/modules/androidqf/base.py @@ -7,9 +7,10 @@ import fnmatch 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 @@ class AndroidQFModule(MVTModule): 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 893e517..bc1d361 100644 --- a/src/mvt/android/modules/androidqf/sms.py +++ b/src/mvt/android/modules/androidqf/sms.py @@ -53,8 +53,12 @@ class SMS(AndroidQFModule): 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 29238ba..8da8a20 100644 --- a/src/mvt/android/modules/backup/base.py +++ b/src/mvt/android/modules/backup/base.py @@ -9,10 +9,10 @@ import os 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 @@ class BackupExtraction(MVTModule): 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 a194a1e..3fafcfc 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 @@ class SMS(BackupExtraction): 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 @@ class SMS(BackupExtraction): 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 158bc28..025e2c1 100644 --- a/src/mvt/android/modules/bugreport/base.py +++ b/src/mvt/android/modules/bugreport/base.py @@ -10,7 +10,7 @@ import os 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 @@ class BugReportModule(MVTModule): 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 e141b2f..0c0f294 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_accessibility.py +++ b/src/mvt/android/modules/bugreport/dumpsys_accessibility.py @@ -7,6 +7,7 @@ import logging 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 @@ class DumpsysAccessibility(DumpsysAccessibilityArtifact, BugReportModule): 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 a58c6f4..bfceebf 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_activities.py +++ b/src/mvt/android/modules/bugreport/dumpsys_activities.py @@ -9,6 +9,7 @@ from typing import Optional from mvt.android.artifacts.dumpsys_package_activities import ( DumpsysPackageActivitiesArtifact, ) +from mvt.common.module_types import ModuleResults from .base import BugReportModule @@ -23,7 +24,7 @@ class DumpsysActivities(DumpsysPackageActivitiesArtifact, BugReportModule): 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 ff74368..07d4694 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 @@ import logging 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 @@ class DumpsysADBState(DumpsysADBArtifact, BugReportModule): 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 96b4796..f3ab41c 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_appops.py +++ b/src/mvt/android/modules/bugreport/dumpsys_appops.py @@ -7,6 +7,7 @@ import logging 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 @@ class DumpsysAppops(DumpsysAppopsArtifact, BugReportModule): 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 7fc8329..365d193 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 @@ import logging 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 @@ class DumpsysBatteryDaily(DumpsysBatteryDailyArtifact, BugReportModule): 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 729f801..2e0f468 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 @@ import logging 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 @@ class DumpsysBatteryHistory(DumpsysBatteryHistoryArtifact, BugReportModule): 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 73902bb..96b0bf3 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_dbinfo.py +++ b/src/mvt/android/modules/bugreport/dumpsys_dbinfo.py @@ -7,6 +7,7 @@ import logging 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 @@ class DumpsysDBInfo(DumpsysDBInfoArtifact, BugReportModule): 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 acec15c..2bb5cd6 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_getprop.py +++ b/src/mvt/android/modules/bugreport/dumpsys_getprop.py @@ -7,6 +7,7 @@ import logging 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 @@ class DumpsysGetProp(GetPropArtifact, BugReportModule): 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 fccf102..0fb4713 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 @@ class DumpsysPackages(DumpsysPackagesArtifact, BugReportModule): 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 e9d10e6..29e58f3 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 typing import Optional 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 @@ class DumpsysPlatformCompat(DumpsysPlatformCompatArtifact, BugReportModule): 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 591af2f..000d98c 100644 --- a/src/mvt/android/modules/bugreport/dumpsys_receivers.py +++ b/src/mvt/android/modules/bugreport/dumpsys_receivers.py @@ -7,6 +7,7 @@ import logging 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 @@ class DumpsysReceivers(DumpsysReceiversArtifact, BugReportModule): 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 14e1cd1..000d076 100644 --- a/src/mvt/android/modules/bugreport/fs_timestamps.py +++ b/src/mvt/android/modules/bugreport/fs_timestamps.py @@ -8,6 +8,7 @@ from typing import Optional 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 @@ class BugReportTimestamps(FileTimestampsArtifact, BugReportModule): 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 6447e61..58ef254 100644 --- a/src/mvt/android/modules/bugreport/tombstones.py +++ b/src/mvt/android/modules/bugreport/tombstones.py @@ -7,6 +7,7 @@ import logging 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 @@ class Tombstones(TombstoneCrashArtifact, BugReportModule): 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 2455959..689c048 100644 --- a/src/mvt/android/utils.py +++ b/src/mvt/android/utils.py @@ -6,16 +6,16 @@ from datetime import datetime, timedelta 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 7cc0682..af0ba98 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: +class Artifact(MVTModule): + """Base class for artifacts. + + XXX: Inheriting from MVTModule to have the same signature as other modules. Not sure if this is a good idea. """ - Main artifact class - """ - - def __init__(self, *args, **kwargs): - self.results = [] - self.detected = [] - self.indicators = None - super().__init__(*args, **kwargs) - - 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 diff --git a/src/mvt/common/cmd_check_iocs.py b/src/mvt/common/cmd_check_iocs.py index 1f8bde5..11e5658 100644 --- a/src/mvt/common/cmd_check_iocs.py +++ b/src/mvt/common/cmd_check_iocs.py @@ -78,7 +78,7 @@ class CmdCheckIOCS(Command): 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 920f3a0..00bf114 100644 --- a/src/mvt/common/command.py +++ b/src/mvt/common/command.py @@ -58,11 +58,9 @@ class Command: # 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 adb4483..877429e 100644 --- a/src/mvt/common/indicators.py +++ b/src/mvt/common/indicators.py @@ -590,9 +590,9 @@ class Indicators: 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 e57a9fc..775717d 100644 --- a/src/mvt/common/module.py +++ b/src/mvt/common/module.py @@ -76,7 +76,6 @@ class MVTModule: 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 @@ class MVTModule: 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 @@ class MVTModule: 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 c8f55f6..07baa2f 100644 --- a/src/mvt/ios/modules/backup/backup_info.py +++ b/src/mvt/ios/modules/backup/backup_info.py @@ -9,6 +9,7 @@ import plistlib 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 @@ class BackupInfo(IOSExtraction): 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 3866971..306ea4f 100644 --- a/src/mvt/ios/modules/backup/configuration_profiles.py +++ b/src/mvt/ios/modules/backup/configuration_profiles.py @@ -7,9 +7,14 @@ import logging 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 @@ class ConfigurationProfiles(IOSExtraction): 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 @@ class ConfigurationProfiles(IOSExtraction): 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 @@ class ConfigurationProfiles(IOSExtraction): # 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 - self.detected.append(result) + result["matched_indicator"] = ioc_match.ioc + self.alertstore.critical( + self.get_slug(), warning_message, "", 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 ccbc459..099a682 100644 --- a/src/mvt/ios/modules/backup/manifest.py +++ b/src/mvt/ios/modules/backup/manifest.py @@ -13,6 +13,11 @@ 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, + ModuleSerializedResult, +) from ..base import IOSExtraction @@ -27,7 +32,7 @@ class Manifest(IOSExtraction): 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 @@ class Manifest(IOSExtraction): 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 @@ class Manifest(IOSExtraction): 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 @@ class Manifest(IOSExtraction): 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 eeb91e2..0648b9e 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 @@ class ProfileEvents(IOSExtraction): 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 @@ class ProfileEvents(IOSExtraction): 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 @@ class ProfileEvents(IOSExtraction): } 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 @@ class ProfileEvents(IOSExtraction): 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 f96d99a..1a4861b 100644 --- a/src/mvt/ios/modules/base.py +++ b/src/mvt/ios/modules/base.py @@ -11,7 +11,8 @@ import sqlite3 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 @@ class IOSExtraction(MVTModule): 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 fecab57..b5cba0d 100644 --- a/src/mvt/ios/modules/fs/analytics.py +++ b/src/mvt/ios/modules/fs/analytics.py @@ -7,9 +7,14 @@ import copy 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 @@ class Analytics(IOSExtraction): 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 @@ class Analytics(IOSExtraction): 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 @@ class Analytics(IOSExtraction): 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 16ac9fc..5fb300e 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 @@ class AnalyticsIOSVersions(IOSExtraction): 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 @@ class AnalyticsIOSVersions(IOSExtraction): 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 120ed1d..fa34b08 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 @@ class CacheFiles(IOSExtraction): 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 @@ class CacheFiles(IOSExtraction): 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 @@ class CacheFiles(IOSExtraction): 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 87c5a0b..980857a 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 @@ class Filesystem(IOSExtraction): 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 @@ class Filesystem(IOSExtraction): 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 @@ class Filesystem(IOSExtraction): 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 ac36a79..23b97f3 100644 --- a/src/mvt/ios/modules/fs/net_netusage.py +++ b/src/mvt/ios/modules/fs/net_netusage.py @@ -7,6 +7,7 @@ import logging 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 @@ class Netusage(NetBase): 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 72bcc9b..c7579fb 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 @@ class SafariFavicon(IOSExtraction): 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 @@ class SafariFavicon(IOSExtraction): 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 @@ class SafariFavicon(IOSExtraction): 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 3d2be78..ad8fcd9 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 @@ class ShutdownLog(IOSExtraction): 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 @@ class ShutdownLog(IOSExtraction): 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 @@ class ShutdownLog(IOSExtraction): 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 44b9b13..8c0af0f 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 @@ class IOSVersionHistory(IOSExtraction): 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 @@ class IOSVersionHistory(IOSExtraction): 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 7e4b245..00d4c19 100644 --- a/src/mvt/ios/modules/fs/webkit_base.py +++ b/src/mvt/ios/modules/fs/webkit_base.py @@ -18,10 +18,11 @@ class WebkitBase(IOSExtraction): 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 aba91c3..58cea42 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 @@ class WebkitIndexedDB(WebkitBase): 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 @@ class WebkitIndexedDB(WebkitBase): 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 dfb117f..2b94fd1 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 @@ class WebkitLocalStorage(WebkitBase): 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 @@ class WebkitLocalStorage(WebkitBase): 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 9e18c93..caa7eef 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 @@ class WebkitSafariViewService(WebkitBase): 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 8c15130..558430a 100644 --- a/src/mvt/ios/modules/mixed/applications.py +++ b/src/mvt/ios/modules/mixed/applications.py @@ -8,11 +8,13 @@ import logging 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 @@ class Applications(IOSExtraction): 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 @@ class Applications(IOSExtraction): 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 @@ class Applications(IOSExtraction): 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 bfd1fc7..6c11186 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 @@ class Calendar(IOSExtraction): 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 @@ class Calendar(IOSExtraction): "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 @@ class Calendar(IOSExtraction): 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 e29be35..82debba 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 @@ class Calls(IOSExtraction): 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 f50ee29..00d0b6c 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 @@ class ChromeFavicon(IOSExtraction): 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 @@ class ChromeFavicon(IOSExtraction): 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 @@ class ChromeFavicon(IOSExtraction): 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 e59ea9f..012b703 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 @@ class ChromeHistory(IOSExtraction): 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 @@ class ChromeHistory(IOSExtraction): 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 @@ class ChromeHistory(IOSExtraction): 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 5f842c6..2bd5bee 100644 --- a/src/mvt/ios/modules/mixed/contacts.py +++ b/src/mvt/ios/modules/mixed/contacts.py @@ -7,6 +7,7 @@ import logging import sqlite3 from typing import Optional +from mvt.common.module_types import ModuleResults from ..base import IOSExtraction CONTACTS_BACKUP_IDS = [ @@ -27,7 +28,7 @@ class Contacts(IOSExtraction): 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 8c88e4a..0489114 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 @@ class FirefoxFavicon(IOSExtraction): 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 @@ class FirefoxFavicon(IOSExtraction): 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 @@ class FirefoxFavicon(IOSExtraction): 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 69bc034..67adda5 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 @@ class FirefoxHistory(IOSExtraction): 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 @@ class FirefoxHistory(IOSExtraction): 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 @@ class FirefoxHistory(IOSExtraction): 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 7b159b1..a1aa9c2 100644 --- a/src/mvt/ios/modules/mixed/global_preferences.py +++ b/src/mvt/ios/modules/mixed/global_preferences.py @@ -7,6 +7,7 @@ import logging 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 @@ class GlobalPreferences(IOSExtraction): 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 @@ class GlobalPreferences(IOSExtraction): 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 e8f4157..1d836fc 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 @@ class IDStatusCache(IOSExtraction): 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 @@ class IDStatusCache(IOSExtraction): 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 @@ class IDStatusCache(IOSExtraction): 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 744decd..8eadda4 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 @@ class InteractionC(IOSExtraction): 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 @@ class InteractionC(IOSExtraction): "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 c190589..35a1c49 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 @@ class LocationdClients(IOSExtraction): 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 @@ class LocationdClients(IOSExtraction): "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 @@ class LocationdClients(IOSExtraction): 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 ce52179..713a7f9 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 @@ class Datausage(NetBase): 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 aea5dbf..1238d3c 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 @@ class OSAnalyticsADDaily(IOSExtraction): 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 @@ class OSAnalyticsADDaily(IOSExtraction): 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 @@ class OSAnalyticsADDaily(IOSExtraction): 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 616ea20..2e9f834 100644 --- a/src/mvt/ios/modules/mixed/safari_browserstate.py +++ b/src/mvt/ios/modules/mixed/safari_browserstate.py @@ -8,9 +8,14 @@ import logging 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 @@ class SafariBrowserState(IOSExtraction): 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 @@ class SafariBrowserState(IOSExtraction): 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 @@ class SafariBrowserState(IOSExtraction): 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 @@ class SafariBrowserState(IOSExtraction): 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 56bc9d0..213cab6 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 @@ class SafariHistory(IOSExtraction): 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 @@ class SafariHistory(IOSExtraction): 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 @@ class SafariHistory(IOSExtraction): 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 @@ class SafariHistory(IOSExtraction): 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 f61168b..38735a0 100644 --- a/src/mvt/ios/modules/mixed/shortcuts.py +++ b/src/mvt/ios/modules/mixed/shortcuts.py @@ -8,9 +8,14 @@ import itertools 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 @@ class Shortcuts(IOSExtraction): 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 @@ class Shortcuts(IOSExtraction): 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 @@ class Shortcuts(IOSExtraction): 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 34c064b..5f64941 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 @@ class SMS(IOSExtraction): 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 @@ class SMS(IOSExtraction): 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 @@ class SMS(IOSExtraction): 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 @@ class SMS(IOSExtraction): # 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 ea9b477..0b9a6e3 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 @@ class SMSAttachments(IOSExtraction): 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 @@ class SMSAttachments(IOSExtraction): 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 @@ class SMSAttachments(IOSExtraction): 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 461e5b3..9be706e 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 @@ class TCC(IOSExtraction): 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 @@ class TCC(IOSExtraction): 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 @@ class TCC(IOSExtraction): 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 e0c2833..4de6df6 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 @@ class WebkitResourceLoadStatistics(IOSExtraction): 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 @@ class WebkitResourceLoadStatistics(IOSExtraction): 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 @@ class WebkitResourceLoadStatistics(IOSExtraction): 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 0ae2545..be59a80 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 @@ import plistlib 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 @@ class WebkitSessionResourceLog(IOSExtraction): 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 @@ class WebkitSessionResourceLog(IOSExtraction): [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 @@ class WebkitSessionResourceLog(IOSExtraction): 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 4e5d8db..4fdeba3 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 @@ class Whatsapp(IOSExtraction): 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 @@ class Whatsapp(IOSExtraction): 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 @@ class Whatsapp(IOSExtraction): 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 1773e29..6de5bd1 100644 --- a/src/mvt/ios/modules/net_base.py +++ b/src/mvt/ios/modules/net_base.py @@ -7,11 +7,16 @@ import logging 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 @@ class NetBase(IOSExtraction): 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 @@ class NetBase(IOSExtraction): 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 @@ class NetBase(IOSExtraction): "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 @@ class NetBase(IOSExtraction): # 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 @@ class NetBase(IOSExtraction): 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 @@ class NetBase(IOSExtraction): 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 2eca8fa..0c9d8d1 100644 --- a/tests/android/test_artifact_dumpsys_accessibility.py +++ b/tests/android/test_artifact_dumpsys_accessibility.py @@ -49,6 +49,6 @@ class TestDumpsysAccessibilityArtifact: 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 7c2edc2..8d59365 100644 --- a/tests/android/test_artifact_dumpsys_appops.py +++ b/tests/android/test_artifact_dumpsys_appops.py @@ -42,22 +42,24 @@ class TestDumpsysAppopsArtifact: 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 e93b050..26917f9 100644 --- a/tests/android/test_artifact_dumpsys_battery_daily.py +++ b/tests/android/test_artifact_dumpsys_battery_daily.py @@ -32,6 +32,6 @@ class TestDumpsysBatteryDailyArtifact: 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 9a09e88..03d7d2e 100644 --- a/tests/android/test_artifact_dumpsys_battery_history.py +++ b/tests/android/test_artifact_dumpsys_battery_history.py @@ -39,6 +39,6 @@ class TestDumpsysBatteryHistoryArtifact: 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 23df5f2..2becf65 100644 --- a/tests/android/test_artifact_dumpsys_dbinfo.py +++ b/tests/android/test_artifact_dumpsys_dbinfo.py @@ -37,6 +37,6 @@ class TestDumpsysDBinfoArtifact: 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 da7c0ab..5eab63d 100644 --- a/tests/android/test_artifact_dumpsys_package_activities.py +++ b/tests/android/test_artifact_dumpsys_package_activities.py @@ -39,6 +39,6 @@ class TestDumpsysPackageActivitiesArtifact: 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 6300f17..7b2ec0f 100644 --- a/tests/android/test_artifact_dumpsys_packages.py +++ b/tests/android/test_artifact_dumpsys_packages.py @@ -37,6 +37,6 @@ class TestDumpsysPackagesArtifact: 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 e2321a4..c8c59b3 100644 --- a/tests/android/test_artifact_dumpsys_platform_compat.py +++ b/tests/android/test_artifact_dumpsys_platform_compat.py @@ -35,6 +35,6 @@ class TestDumpsysPlatformCompatArtifact: 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 f236aa9..e4bed62 100644 --- a/tests/android/test_artifact_dumpsys_receivers.py +++ b/tests/android/test_artifact_dumpsys_receivers.py @@ -42,6 +42,6 @@ class TestDumpsysReceiversArtifact: 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 4ae9036..d9fec6b 100644 --- a/tests/android/test_artifact_getprop.py +++ b/tests/android/test_artifact_getprop.py @@ -36,6 +36,6 @@ class TestGetPropArtifact: "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 54bc36c..2806472 100644 --- a/tests/android/test_artifact_processes.py +++ b/tests/android/test_artifact_processes.py @@ -33,6 +33,6 @@ class TestProcessesArtifact: 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 4da9024..5bd5b99 100644 --- a/tests/android/test_backup_parser.py +++ b/tests/android/test_backup_parser.py @@ -60,7 +60,6 @@ class TestBackupParsing: == "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 c0d45b5..c854473 100644 --- a/tests/android_androidqf/test_files.py +++ b/tests/android_androidqf/test_files.py @@ -22,4 +22,4 @@ class TestAndroidqfFilesAnalysis: 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 3947acd..9a938cc 100644 --- a/tests/android_androidqf/test_getprop.py +++ b/tests/android_androidqf/test_getprop.py @@ -26,7 +26,7 @@ class TestAndroidqfGetpropAnalysis: 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 @@ class TestAndroidqfGetpropAnalysis: 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 @@ class TestAndroidqfGetpropAnalysis: 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 fe6332a..129cb35 100644 --- a/tests/android_androidqf/test_packages.py +++ b/tests/android_androidqf/test_packages.py @@ -47,38 +47,35 @@ class TestAndroidqfPackages: 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 @@ class TestAndroidqfPackages: 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 @@ class TestAndroidqfPackages: 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 @@ class TestAndroidqfPackages: 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 bcd4013..da75fa5 100644 --- a/tests/android_androidqf/test_processes.py +++ b/tests/android_androidqf/test_processes.py @@ -22,4 +22,4 @@ class TestAndroidqfProcessesAnalysis: 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 75527a7..ef7386a 100644 --- a/tests/android_androidqf/test_settings.py +++ b/tests/android_androidqf/test_settings.py @@ -21,4 +21,4 @@ class TestSettingsModule: 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 d7433cf..116d5b2 100644 --- a/tests/android_androidqf/test_sms.py +++ b/tests/android_androidqf/test_sms.py @@ -25,7 +25,7 @@ class TestAndroidqfSMSAnalysis: 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 0000000..d1bf073 --- /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 8abc896..cb156e5 100644 --- a/tests/android_bugreport/test_bugreport.py +++ b/tests/android_bugreport/test_bugreport.py @@ -35,9 +35,13 @@ class TestBugreportAnalysis: 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 de3f32c..bf931f4 100644 --- a/tests/ios_backup/test_calendar.py +++ b/tests/ios_backup/test_calendar.py @@ -18,7 +18,7 @@ class TestCalendarModule: 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 @@ class TestCalendarModule: 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 5e12727..8ab25f8 100644 --- a/tests/ios_backup/test_datausage.py +++ b/tests/ios_backup/test_datausage.py @@ -7,6 +7,7 @@ import logging 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 @@ class TestDatausageModule: 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 @@ class TestDatausageModule: 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 705ca8d..7c2f766 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 @@ class TestGlobalPreferencesModule: 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 9b9882f..44df42d 100644 --- a/tests/ios_backup/test_manifest.py +++ b/tests/ios_backup/test_manifest.py @@ -18,7 +18,7 @@ class TestManifestModule: 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 @@ class TestManifestModule: 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 e877ded..fd7e62e 100644 --- a/tests/ios_backup/test_safari_browserstate.py +++ b/tests/ios_backup/test_safari_browserstate.py @@ -19,7 +19,7 @@ class TestSafariBrowserStateModule: 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 @@ class TestSafariBrowserStateModule: 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 243f630..03e4606 100644 --- a/tests/ios_backup/test_sms.py +++ b/tests/ios_backup/test_sms.py @@ -18,7 +18,7 @@ class TestSMSModule: 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 @@ class TestSMSModule: 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 23a1ab7..d1bf073 100644 --- a/tests/ios_backup/test_tcc.py +++ b/tests/ios_backup/test_tcc.py @@ -18,7 +18,7 @@ class TestTCCModule: 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 @@ class TestTCCModule: 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 0e59ebb..f0be231 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 @@ class TestWebkitResourceLoadStatisticsModule: 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 062713f..9fa664f 100644 --- a/tests/ios_fs/test_filesystem.py +++ b/tests/ios_fs/test_filesystem.py @@ -17,7 +17,7 @@ class TestFilesystem: 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 @@ class TestFilesystem: 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/11] 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 877429e..c34418d 100644 --- a/src/mvt/common/indicators.py +++ b/src/mvt/common/indicators.py @@ -73,7 +73,7 @@ class Indicators: 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 @@ class Indicators: :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/11] 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 0648b9e..98c87b1 100644 --- a/src/mvt/ios/modules/backup/profile_events.py +++ b/src/mvt/ios/modules/backup/profile_events.py @@ -57,7 +57,7 @@ class ProfileEvents(IOSExtraction): 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/11] 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 e0f49ab..013c622 100644 --- a/src/mvt/android/cmd_check_androidqf.py +++ b/src/mvt/android/cmd_check_androidqf.py @@ -159,6 +159,9 @@ class CmdAndroidCheckAndroidQF(Command): 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 @@ class CmdAndroidCheckAndroidQF(Command): 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 2c39bae..15711f3 100644 --- a/src/mvt/android/cmd_check_backup.py +++ b/src/mvt/android/cmd_check_backup.py @@ -56,12 +56,12 @@ class CmdAndroidCheckBackup(Command): 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 @@ class CmdAndroidCheckBackup(Command): 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 @@ class CmdAndroidCheckBackup(Command): 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()