mirror of
https://github.com/mvt-project/mvt.git
synced 2026-08-17 08:30:38 +02:00
V3 (#716)
* Run bugreport and backup modules during check-androidqf Adding support to automatically run ADB backup and bugreport modules automatically when running the check-androidqf command. This is a first step to deduplicate the code for Android modules. * Deduplicate modules which are run by the sub-commands. * Raise the proper NoAndroidQFBackup exception when a back-up isn't found * Remove check-adb command and update docs * Remove check-apk code and old dependencies * Major refactor to add structured alerting and typed indicators This commit makes a structural change to MVT by changing binary detected/not detected logic into a structured multi-level system of alerts. This gives far more power to extend MVT and manage alerts. This commit also begins the process of adding proper typing for key objects used in MVT including Indicators, IndicatorMatches, and ModuleResults. This will also be keep to programmatically using the output of MVT. * Fix up, remove ADB module base * Rework old detections tracking into stuctured alert levels * Quote STIX path in log line * Fix profile events log line * Close open archive (zip/tar) file handles * Fix root_binaries and mounts modules to use alertstore * Update tests to use alertstore instead of detected attribute * Fix alertstore method calls - use high() instead of warning() * Fix remaining test errors - Add log_latest() call in root_binaries to log each alert - Fix UnboundLocalError in cmd_check_androidqf by initializing bugreport variable - Remove incorrect backup.close() call since load_backup() returns bytes - Remove duplicate from_ab method in cmd_check_backup that was using old attributes * Log alerts on add * Remove slug from alertstore calls * update alerts.py * update alerts.py * move indicator_match to alert object * . * - Remove timeline_detected and route to alertstore * fix typing for mypy * Remove unused type imports * Fix check_receiver_prefix and check_android_property_name - check_receiver_prefix() used dict syntax (ioc["value"]) on Indicator dataclass objects from get_iocs(). Changed to ioc.value/ioc.name. - check_receiver_prefix() returned raw ioc instead of IndicatorMatch. Now returns IndicatorMatch with descriptive message. - Fixed return type annotations on both methods to Optional[IndicatorMatch]. - Removed unused Union import. * Fix residual self.detected usage in packages and dumpsys_receivers These modules still used self.detected.append() which no longer exists after the alertstore migration. Converted to alertstore calls: - packages.py: ROOT_PACKAGES detection → alertstore.high() - dumpsys_receivers.py: receiver IOC match → alertstore.critical() * Fix SMS module alertstore.high() call passing slug as message The first argument was self.get_slug() (module slug) instead of a human-readable message. The module is already auto-detected via AlertStore._get_calling_module(). Also removed redundant log_latest(). * Apply suggestions from code review Fix JSON serialization in `module.save_to_json` and fix argument order in iOS alertstore calls. Co-authored-by: tes <tesitura@users.noreply.github.com> * Remove unsupported ADB modules * Fail removed check-adb command * Fix alert serialization and logging * Close sqlite connections in iOS modules * Fix DEBUG messages not reaching handlers, save_to_json for dictionary results and TypeError on mixed event_time types in safary_history * add matched_indicator via alertstore instead of directly modifying json objects * Alert on battery daily uninstall and downgrade * Lower alert severity to medium for suspicious items * Switch version to 2026.4.28 CalVer --------- Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org> Co-authored-by: tes <tesitura@users.noreply.github.com> Co-authored-by: Janik Besendorf <janik.besendorf@reporter-ohne-grenzen.de>
This commit is contained in:
co-authored by
tes
Donncha Ó Cearbhaill
Janik Besendorf
parent
dad2a7a928
commit
c782d79974
@@ -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
|
||||
|
||||
@@ -42,22 +42,25 @@ 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 alert.matched_indicator is not None
|
||||
]
|
||||
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 detected_by_ioc[0].matched_indicator is not None
|
||||
assert len(detected_by_permission_heuristic) == 2
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import logging
|
||||
|
||||
from mvt.android.artifacts.dumpsys_battery_daily import DumpsysBatteryDailyArtifact
|
||||
from mvt.common.alerts import AlertLevel
|
||||
from mvt.common.indicators import Indicators
|
||||
|
||||
from ..utils import get_artifact
|
||||
@@ -32,6 +33,39 @@ 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
|
||||
|
||||
def test_uninstall_and_downgrade_create_medium_alerts(self):
|
||||
dba = DumpsysBatteryDailyArtifact()
|
||||
dba.parse(
|
||||
"""
|
||||
Daily from 2022-08-16-15-56-39 to 2022-08-17-01-15-45:
|
||||
Update com.example.app vers=10
|
||||
Update com.example.removed vers=0
|
||||
Daily from 2022-08-17-15-56-39 to 2022-08-18-01-15-45:
|
||||
Update com.example.app vers=9
|
||||
"""
|
||||
)
|
||||
|
||||
assert len(dba.results) == 3
|
||||
assert len(dba.alertstore.alerts) == 2
|
||||
|
||||
uninstall_alert, downgrade_alert = dba.alertstore.alerts
|
||||
assert uninstall_alert.level == AlertLevel.MEDIUM
|
||||
assert uninstall_alert.message == (
|
||||
"Detected uninstall of package com.example.removed (vers 0)"
|
||||
)
|
||||
assert uninstall_alert.event_time == "2022-08-16"
|
||||
assert uninstall_alert.event["package_name"] == "com.example.removed"
|
||||
assert uninstall_alert.event["vers"] == "0"
|
||||
|
||||
assert downgrade_alert.level == AlertLevel.MEDIUM
|
||||
assert downgrade_alert.message == (
|
||||
"Detected downgrade of package com.example.app from vers 10 to vers 9"
|
||||
)
|
||||
assert downgrade_alert.event_time == "2022-08-17"
|
||||
assert downgrade_alert.event["package_name"] == "com.example.app"
|
||||
assert downgrade_alert.event["action"] == "downgrade"
|
||||
assert downgrade_alert.event["previous_vers"] == "10"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from mvt.common.indicators import Indicator, IndicatorMatch
|
||||
from mvt.common.module import run_module
|
||||
|
||||
from ..utils import get_android_androidqf, list_files
|
||||
@@ -72,6 +73,38 @@ class TestAndroidqfMountsArtifact:
|
||||
(("by-name/data" in s or "/data" in s) and "rw" in s) for s in concatenated
|
||||
), f"No data-like tokens (data + rw) found in parsed results: {concatenated}"
|
||||
|
||||
def test_mount_ioc_alert_uses_indicator(self):
|
||||
from mvt.android.artifacts.mounts import Mounts as MountsArtifact
|
||||
|
||||
indicator = Indicator(
|
||||
value="/system",
|
||||
type="file_path",
|
||||
name="TestMalware",
|
||||
stix2_file_name="indicators.stix2",
|
||||
)
|
||||
m = MountsArtifact()
|
||||
m.indicators = type(
|
||||
"MountIndicators",
|
||||
(),
|
||||
{
|
||||
"check_file_path": lambda self, path: IndicatorMatch(
|
||||
ioc=indicator, message="matched file path"
|
||||
)
|
||||
if path == "/system"
|
||||
else None
|
||||
},
|
||||
)()
|
||||
|
||||
m.parse("/dev/block/by-name/system on /system type ext4 (rw,seclabel)")
|
||||
m.check_indicators()
|
||||
|
||||
indicator_alerts = [
|
||||
alert for alert in m.alertstore.alerts if alert.matched_indicator
|
||||
]
|
||||
assert len(indicator_alerts) == 1
|
||||
assert indicator_alerts[0].matched_indicator == indicator
|
||||
assert indicator_alerts[0].message == "matched file path"
|
||||
|
||||
|
||||
class TestAndroidqfMountsModule:
|
||||
def test_androidqf_module_no_mounts_file(self):
|
||||
@@ -94,4 +127,6 @@ class TestAndroidqfMountsModule:
|
||||
assert len(m.results) == 0, (
|
||||
f"Expected no results when mounts.json is absent, got: {m.results}"
|
||||
)
|
||||
assert len(m.detected) == 0, f"Expected no detections, got: {m.detected}"
|
||||
assert len(m.alertstore.alerts) == 0, (
|
||||
f"Expected no detections, got: {m.alertstore.alerts}"
|
||||
)
|
||||
|
||||
@@ -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,13 +83,13 @@ 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]["matched_indicator"]["value"] == "com.malware.blah"
|
||||
)
|
||||
assert possible_detected_app[0].event["name"] == "com.malware.blah"
|
||||
assert possible_detected_app[0].matched_indicator.value == "com.malware.blah"
|
||||
|
||||
def test_packages_ioc_sha256(self, module, indicators_factory):
|
||||
module.indicators = indicators_factory(
|
||||
@@ -104,12 +101,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"]["value"]
|
||||
possible_detected_app[0].matched_indicator.value
|
||||
== "31037a27af59d4914906c01ad14a318eee2f3e31d48da8954dca62a99174e3fa"
|
||||
)
|
||||
|
||||
@@ -123,11 +122,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"]["value"]
|
||||
possible_detected_app[0].matched_indicator.value
|
||||
== "c7e56178748be1441370416d4c10e34817ea0c961eb636c8e9d98e0fd79bf730"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -42,7 +42,7 @@ class TestAndroidqfRootBinaries:
|
||||
|
||||
# Should find 4 root binaries from the test file
|
||||
assert len(module.results) == 4
|
||||
assert len(module.detected) == 4
|
||||
assert len(module.alertstore.alerts) == 4
|
||||
|
||||
# Check that all results are detected as indicators
|
||||
binary_paths = [result["path"] for result in module.results]
|
||||
@@ -113,4 +113,4 @@ class TestAndroidqfRootBinaries:
|
||||
run_module(m)
|
||||
|
||||
assert len(m.results) == 0
|
||||
assert len(m.detected) == 0
|
||||
assert len(m.alertstore.alerts) == 0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
@@ -36,9 +36,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):
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2026 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
from mvt.common.alerts import Alert, AlertLevel, AlertStore
|
||||
|
||||
|
||||
def test_as_json_promotes_nested_matched_indicator():
|
||||
indicator = {"value": "com.apple.weather", "type": "processes"}
|
||||
alertstore = AlertStore()
|
||||
alertstore.add(
|
||||
Alert(
|
||||
level=AlertLevel.CRITICAL,
|
||||
module="datausage",
|
||||
message="Matched indicator",
|
||||
event_time="2026-01-01 00:00:00",
|
||||
event={
|
||||
"proc_name": "WeatherWidget/com.apple.weather",
|
||||
"matched_indicator": indicator,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
alert = alertstore.as_json()[0]
|
||||
|
||||
assert alert["matched_indicator"] == indicator
|
||||
assert "matched_indicator" not in alert["event"]
|
||||
|
||||
|
||||
def test_as_json_removes_nested_matched_indicator_when_parent_exists():
|
||||
event_indicator = {"value": "nested", "type": "processes"}
|
||||
alert_indicator = {"value": "parent", "type": "processes"}
|
||||
alertstore = AlertStore()
|
||||
alertstore.add(
|
||||
Alert(
|
||||
level=AlertLevel.CRITICAL,
|
||||
module="manifest",
|
||||
message="Matched indicator",
|
||||
event_time="2026-01-01 00:00:00",
|
||||
event={"path": "/tmp/example", "matched_indicator": event_indicator},
|
||||
matched_indicator=alert_indicator,
|
||||
)
|
||||
)
|
||||
|
||||
alert = alertstore.as_json()[0]
|
||||
|
||||
assert alert["matched_indicator"] == alert_indicator
|
||||
assert "matched_indicator" not in alert["event"]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import json
|
||||
|
||||
from mvt.common.command import Command
|
||||
|
||||
|
||||
class TestCommand:
|
||||
def test_store_alerts_handles_bytes(self, tmp_path):
|
||||
cmd = Command(results_path=str(tmp_path))
|
||||
cmd.alertstore.medium(
|
||||
"bytes event",
|
||||
"",
|
||||
{"payload": b"\xa8\xa9"},
|
||||
)
|
||||
|
||||
cmd._store_alerts()
|
||||
|
||||
alerts = json.loads((tmp_path / "alerts.json").read_text())
|
||||
assert alerts[0]["event"]["payload"] == "\\xa8\\xa9"
|
||||
@@ -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
|
||||
|
||||
@@ -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,19 @@ 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
|
||||
assert all(
|
||||
"matched_indicator" not in alert.event for alert in critical_alerts
|
||||
)
|
||||
serialized_alerts = [
|
||||
alert
|
||||
for alert in m.alertstore.as_json()
|
||||
if alert["matched_indicator"] is not None
|
||||
]
|
||||
assert len(serialized_alerts) == 2
|
||||
assert all(
|
||||
"matched_indicator" not in alert["event"] for alert in serialized_alerts
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,22 +3,43 @@
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
import gc
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from mvt.common.indicators import Indicators
|
||||
from mvt.common.module import run_module
|
||||
from mvt.ios.modules.base import IOSExtraction
|
||||
from mvt.ios.modules.backup.manifest import Manifest
|
||||
|
||||
from ..utils import get_ios_backup_folder
|
||||
|
||||
|
||||
class TestIOSExtraction:
|
||||
def test_get_backup_files_from_manifest_closes_connection(self):
|
||||
m = IOSExtraction(target_path=get_ios_backup_folder())
|
||||
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always", ResourceWarning)
|
||||
files = list(m._get_backup_files_from_manifest(domain="CameraRollDomain"))
|
||||
gc.collect()
|
||||
|
||||
assert files
|
||||
assert not [
|
||||
warning
|
||||
for warning in caught
|
||||
if issubclass(warning.category, ResourceWarning)
|
||||
and "unclosed database" in str(warning.message)
|
||||
]
|
||||
|
||||
|
||||
class TestManifestModule:
|
||||
def test_manifest(self):
|
||||
m = Manifest(target_path=get_ios_backup_folder())
|
||||
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 +48,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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
import logging
|
||||
|
||||
from mvt.common.indicators import Indicators
|
||||
from mvt.ios.modules.fs.cache_files import CacheFiles
|
||||
|
||||
|
||||
class TestCacheFiles:
|
||||
def test_detection(self, indicator_file):
|
||||
m = CacheFiles(
|
||||
results={
|
||||
"Library/Caches/example/Cache.db": [
|
||||
{
|
||||
"entry_id": 1,
|
||||
"version": 1,
|
||||
"hash_value": 123,
|
||||
"storage_policy": 0,
|
||||
"url": "http://example.com/thisisbad",
|
||||
"isodate": "2026-01-01 00:00:00.000000",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
ind = Indicators(log=logging.getLogger())
|
||||
ind.parse_stix2(indicator_file)
|
||||
m.indicators = ind
|
||||
|
||||
m.check_indicators()
|
||||
|
||||
assert len(m.alertstore.alerts) == 1
|
||||
alert = m.alertstore.alerts[0]
|
||||
assert alert.event["cache_file"] == "Library/Caches/example/Cache.db"
|
||||
assert alert.event["url"] == "http://example.com/thisisbad"
|
||||
assert alert.matched_indicator is not None
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Mobile Verification Toolkit (MVT)
|
||||
# Copyright (c) 2021-2023 The MVT Authors.
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from mvt.android.cli import check_adb
|
||||
|
||||
|
||||
class TestCheckAndroidADBRemovedCommand:
|
||||
def test_check_adb_exits_nonzero(self):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(check_adb)
|
||||
|
||||
assert result.exit_code == 1
|
||||
Reference in New Issue
Block a user