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] 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