Compare commits

..
Author SHA1 Message Date
besendorf a0bc233eec Merge branch 'main' into besendorf-patch-5 2026-09-22 15:34:36 +02:00
Janik Besendorf 1db5d239d0 Merge main into PR 926 2026-09-22 15:33:17 +02:00
Donncha Ó Cearbhaill 2426178baa Enable automated PyPI deployments for tagged releases (#943)
* Limit PyPi deploys to tag relaeses

* Remove unnecessary tests and docs from sdist build
2026-09-22 13:02:49 +02:00
besendorf e0697a58e9 Merge pull request #937 from va-resident/fix/mountinfo-rw-both-layers
Fix mountinfo read-write detection across both option layers
2026-09-18 17:48:33 +02:00
va-residentandClaude Opus 5 d0fc0379b9 Fix mountinfo read-write detection across both option layers
/proc/PID/mountinfo carries two option sets with different meaning: fields[5]
are the per-mount (VFS) flags, the field after the "-" separator belongs to the
superblock. A mount is writable only if both allow it.

parse_mountinfo() merged both into one list and set
is_read_write = "rw" in options, so a "rw" VFS mount over a read-only
superblock was reported as writable. On stock Xiaomi-family builds that made
every read-only mi_ext customisation overlay a HIGH "system partition is
mounted as read-write".

Require "rw" in both layers, and let "rw" count as a suspicious mount option
only when the mount is actually writable; remount, noatime and nodiratime keep
their current meaning in either layer. mount_options and options_list still
carry both layers, so nothing downstream loses data.

Measured on 50 bug reports carrying mountinfo - the 28 where the rule changes
the output plus 22 controls, 14 brands, Android 10-16: HIGH 94 -> 0,
MEDIUM 119 -> 25, the 22 controls identical, and 36233 mount entries parsed
either way. Every removed alert is a read-only superblock under a "rw" VFS
mount; no report gains an alert. A partition that really is writable still
raises the HIGH, which the new test asserts explicitly.

Fixes #936

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 18:43:06 +03:00
besendorf 7973e90589 Merge pull request #934 from mvt-project/chore/update-iphone-backup-decrypt-0.10.0
Bump iphone_backup_decrypt to 0.10.0
2026-09-18 10:36:59 +02:00
Janik Besendorf 935747519f Bump iphone_backup_decrypt to 0.10.0 2026-09-18 10:22:21 +02:00
besendorf 178ac52aab Merge pull request #927 from mvt-project/auto/add-new-ios-releases
[auto] Update iOS releases and versions
2026-09-15 03:37:45 +02:00
DonnchaC f20fd14075 Add new iOS versions and build numbers 2026-09-15 00:37:07 +00:00
Janik Besendorf a9d787c446 Remove unused libusb1 and refresh dependency lockfile 2026-09-14 00:30:48 +02:00
besendorf 518becb825 Remove adb-shell dependency from pyproject.toml
not used anymore
2026-09-13 20:06:15 +02:00
44 changed files with 127 additions and 255 deletions

No files matched your search

+1
View File
@@ -34,6 +34,7 @@ jobs:
publish:
if: github.ref_type == 'tag'
runs-on: ubuntu-latest
environment: pypi # only tag refs may deploy; the PyPI trusted publisher requires it
permissions:
id-token: write # PyPI trusted publishing
steps:
+4
View File
@@ -0,0 +1,4 @@
prune tests
prune docs
prune .github
exclude uv.lock Dockerfile* mkdocs.yml .readthedocs.yaml .safety-policy.yml .python-version .gitignore Makefile
+1 -3
View File
@@ -24,10 +24,8 @@ dependencies = [
"simplejson==4.1.1",
"packaging==26.3",
"appdirs==1.4.4",
"iphone_backup_decrypt==0.9.0",
"iphone_backup_decrypt==0.10.0",
"pycryptodome>=3.20.0",
"adb-shell[usb]==0.4.4",
"libusb1==3.4.0",
"cryptography==50.0.0",
"PyYAML>=6.0.2",
"pyahocorasick==2.3.1",
+1 -15
View File
@@ -43,25 +43,11 @@ class DumpsysAppopsArtifact(AndroidArtifact):
def check_indicators(self) -> None:
for result in self.results:
# A package IOC match is not tied to one AppOps entry. Use the latest
# recorded access or rejection to place the package-level alert in time.
event_time = max(
(
entry["timestamp"]
for permission in result["permissions"]
for entry in permission.get("entries", [])
if entry.get("timestamp")
),
default="",
)
if self.indicators:
ioc_match = self.indicators.check_app_id(result.get("package_name"))
if ioc_match:
self.alertstore.critical(
ioc_match.message,
event_time,
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -43,10 +43,7 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact):
ioc_match = self.indicators.check_app_id(result["package_name"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("period_start") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -24,10 +24,7 @@ class DumpsysBatteryHistoryArtifact(AndroidArtifact):
ioc_match = self.indicators.check_app_id(package_name)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
@staticmethod
+1 -4
View File
@@ -23,10 +23,7 @@ class DumpsysDBInfoArtifact(AndroidArtifact):
ioc_match = self.indicators.check_app_id(part)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -35,7 +35,7 @@ class DumpsysPackagesArtifact(AndroidArtifact):
alerted_root_packages.add(package_name)
self.alertstore.medium(
f'Found an installed package related to rooting/jailbreaking: "{package_name}"',
result.get("timestamp") or "",
"",
result,
)
if not self.indicators:
@@ -43,10 +43,7 @@ class DumpsysPackagesArtifact(AndroidArtifact):
ioc_match = self.indicators.check_app_id(package_name)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
+13 -2
View File
@@ -136,6 +136,10 @@ class Mounts(AndroidArtifact):
mount_options = fields[5].split(",")
super_options = fields[separator + 3].split(",")
options = list(dict.fromkeys(mount_options + super_options))
# fields[5] are the per-mount (VFS) flags, the field after the
# separator the superblock's; a mount is writable only if both
# allow it. The merged list made every read-only overlay a HIGH.
is_read_write = "rw" in mount_options and "rw" in super_options
mount_point = fields[4].replace("\\040", " ")
device = fields[separator + 2].replace("\\040", " ")
filesystem_type = fields[separator + 1]
@@ -156,7 +160,7 @@ class Mounts(AndroidArtifact):
"options_list": options,
"optional_fields": fields[6:separator],
"is_system_partition": is_system,
"is_read_write": "rw" in options,
"is_read_write": is_read_write,
"process_ids": [process_id],
}
)
@@ -191,7 +195,14 @@ class Mounts(AndroidArtifact):
)
# Check for other suspicious mount options
suspicious_opts = [opt for opt in options if opt in SUSPICIOUS_OPTIONS]
# `rw` sitting in the merged option list is not evidence of
# writability (see is_read_write in parse_mountinfo); every other
# suspicious option is meaningful in either list.
suspicious_opts = [
opt
for opt in options
if opt in SUSPICIOUS_OPTIONS and (opt != "rw" or mount["is_read_write"])
]
if suspicious_opts and mount["is_system_partition"]:
if (
"noatime" in mount["mount_options"]
@@ -99,10 +99,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
ioc_match = self.indicators.check_process(result["process_name"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -111,10 +108,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
ioc_match = self.indicators.check_process(command_name)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -129,7 +123,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
f"Potentially suspicious crash in process '{result['process_name']}' "
f"running as UID '{result['uid']}' in tombstone '{result['file_name']}' at {result['timestamp']}"
),
result.get("timestamp") or "",
"",
result,
)
@@ -90,10 +90,7 @@ class AQFFiles(AndroidQFModule):
ioc_match = self.indicators.check_file_path(result["path"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("modified_time") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -108,7 +105,7 @@ class AQFFiles(AndroidQFModule):
file_type = "executable "
msg = f'Found {file_type}file at suspicious path "{result["path"]}"'
self.alertstore.high(msg, result.get("modified_time") or "", result)
self.alertstore.high(msg, "", result)
for hash_key in ("sha256", "sha1", "md5"):
file_hash = result.get(hash_key, "")
@@ -117,10 +114,7 @@ class AQFFiles(AndroidQFModule):
ioc_match = self.indicators.check_file_hash(file_hash)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("modified_time") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
break
+1 -4
View File
@@ -54,10 +54,7 @@ class SMS(BackupModule):
):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
message.get("isodate") or "",
message,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", message, matched_indicator=ioc_match.ioc
)
def collect_url_results(self) -> None:
+4
View File
@@ -1275,5 +1275,9 @@
{
"version": "26.6.2",
"build": "23G90"
},
{
"version": "27.0",
"build": "24A437"
}
]
+1 -1
View File
@@ -52,7 +52,7 @@ class MVTEncryptedBackup(EncryptedBackup):
NOTE: This subclass relies on internal APIs of iphone_backup_decrypt
(specifically _read_and_unlock_keybag, _keybag, and the Keybag class
internals). Pinned to iphone_backup_decrypt==0.9.0.
internals). Pinned to iphone_backup_decrypt==0.10.0.
"""
def __init__(self, *, backup_directory, passphrase=None, derived_key=None):
@@ -74,10 +74,7 @@ class ConfigurationProfiles(IOSExtraction):
if ioc_match:
warning_message = f'Found a known malicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with UUID "{result["plist"]["PayloadUUID"]}"'
self.alertstore.critical(
warning_message,
result.get("install_date") or "",
result,
matched_indicator=ioc_match.ioc,
warning_message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -85,9 +82,7 @@ class ConfigurationProfiles(IOSExtraction):
# to hide notifications.
if payload_content["PayloadType"] in ["com.apple.notificationsettings"]:
warning_message = f'Found a potentially suspicious configuration profile "{result["plist"]["PayloadDisplayName"]}" with payload type {payload_content["PayloadType"]}'
self.alertstore.medium(
warning_message, result.get("install_date") or "", result
)
self.alertstore.medium(warning_message, "", result)
continue
@staticmethod
+2 -5
View File
@@ -104,10 +104,7 @@ class Manifest(IOSExtraction):
ioc_match = self.indicators.check_file_path("/" + result["relative_path"])
if ioc_match:
self.alertstore.high(
ioc_match.message,
result.get("modified") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -123,7 +120,7 @@ class Manifest(IOSExtraction):
if ioc_match:
self.alertstore.high(
f'Found mention of domain "{ioc_match.ioc.value}" in a backup file with path: {rel_path}',
result.get("modified") or "",
"",
result,
matched_indicator=ioc_match.ioc,
)
+2 -8
View File
@@ -67,20 +67,14 @@ class ProfileEvents(IOSExtraction):
ioc_match = self.indicators.check_process(result.get("process") or "")
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
ioc_match = self.indicators.check_profile(result.get("profile_id") or "")
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
@staticmethod
+2 -5
View File
@@ -68,10 +68,7 @@ class Analytics(IOSExtraction):
warning_message = f'Found mention of a malicious process "{value}" in {result["artifact"]} file at {result["isodate"]}'
new_result = copy.copy(result)
self.alertstore.critical(
warning_message,
result.get("isodate") or "",
new_result,
matched_indicator=ioc_match.ioc,
warning_message, "", new_result, matched_indicator=ioc_match.ioc
)
continue
@@ -80,7 +77,7 @@ class Analytics(IOSExtraction):
new_result = copy.copy(result)
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
"",
new_result,
matched_indicator=ioc_match.ioc,
)
+2 -8
View File
@@ -59,10 +59,7 @@ class Filesystem(IOSExtraction):
ioc_match = self.indicators.check_file_path(result["path"])
if ioc_match:
self.alertstore.high(
ioc_match.message,
result.get("modified") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
# If we are instructed to run fast, we skip the rest.
@@ -72,10 +69,7 @@ class Filesystem(IOSExtraction):
ioc_match = self.indicators.check_file_path_process(result["path"])
if ioc_match:
self.alertstore.high(
ioc_match.message,
result.get("modified") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def run(self) -> None:
+1 -4
View File
@@ -63,10 +63,7 @@ class SafariFavicon(IOSExtraction):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def _process_favicon_db(self, file_path):
+2 -5
View File
@@ -60,10 +60,7 @@ class ShutdownLog(IOSExtraction):
ioc_match = self.indicators.check_file_path(result["client"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -72,7 +69,7 @@ class ShutdownLog(IOSExtraction):
if ioc.value in parts:
self.alertstore.critical(
f'Found mention of a known malicious process "{ioc.value}" in shutdown.log',
result.get("isodate") or "",
"",
result,
matched_indicator=ioc,
)
+1 -4
View File
@@ -21,10 +21,7 @@ class WebkitBase(IOSExtraction):
ioc_match = self.indicators.check_url(result["url"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
+4 -4
View File
@@ -68,7 +68,7 @@ class Applications(IOSExtraction):
if "softwareVersionBundleId" not in result:
self.alertstore.medium(
"Suspicious application identified without softwareVersionBundleId",
result.get("isodate") or "",
"",
result,
)
continue
@@ -79,7 +79,7 @@ class Applications(IOSExtraction):
if ioc_match:
self.alertstore.critical(
f"Malicious application {result['softwareVersionBundleId']} identified",
result.get("isodate") or "",
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -91,7 +91,7 @@ class Applications(IOSExtraction):
if ioc_match:
self.alertstore.critical(
f"Malicious application {result['softwareVersionBundleId']} identified",
result.get("isodate") or "",
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -104,7 +104,7 @@ class Applications(IOSExtraction):
):
self.alertstore.medium(
f"Suspicious app not installed from the App Store or MDM: {result['softwareVersionBundleId']}",
result.get("isodate") or "",
"",
result,
)
+2 -5
View File
@@ -72,10 +72,7 @@ class Calendar(IOSExtraction):
ioc_match = self.indicators.check_email(result["participant_email"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("start_date") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -83,7 +80,7 @@ class Calendar(IOSExtraction):
if result["summary"] == "Meeting" and result["description"] == "Notes":
self.alertstore.high(
f"Potential Quadream exploit event identified: {result['uuid']}",
result.get("start_date") or "",
"",
result,
)
+1 -4
View File
@@ -61,10 +61,7 @@ class ChromeFavicon(IOSExtraction):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
+1 -4
View File
@@ -62,10 +62,7 @@ class ChromeHistory(IOSExtraction):
ioc_match = self.indicators.check_url(result["url"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def run(self) -> None:
+1 -4
View File
@@ -64,10 +64,7 @@ class FirefoxFavicon(IOSExtraction):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def run(self) -> None:
+1 -4
View File
@@ -64,10 +64,7 @@ class FirefoxHistory(IOSExtraction):
ioc_match = self.indicators.check_url(result["url"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def run(self) -> None:
+2 -5
View File
@@ -66,17 +66,14 @@ class IDStatusCache(IOSExtraction):
ioc_match = self.indicators.check_email(email)
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
if "\\x00\\x00" in result.get("user", ""):
self.alertstore.high(
f"Found an ID Status Cache entry with suspicious patterns: {result.get('user')}",
result.get("isodate") or "",
"",
result,
)
+5 -13
View File
@@ -79,14 +79,6 @@ class LocationdClients(IOSExtraction):
return
for result in self.results:
event_time = next(
(
result[timestamp]
for timestamp in self.timestamps
if result.get(timestamp)
),
"",
)
parts = result["package"].split("/")
proc_name = parts[len(parts) - 1]
@@ -94,7 +86,7 @@ class LocationdClients(IOSExtraction):
if ioc_match:
self.alertstore.high(
f"Found a suspicious process name in LocationD entry {result['package']}",
event_time,
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -105,7 +97,7 @@ class LocationdClients(IOSExtraction):
if ioc_match:
self.alertstore.high(
f"Found a suspicious process name in LocationD entry {result['package']}",
event_time,
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -115,7 +107,7 @@ class LocationdClients(IOSExtraction):
if ioc_match:
self.alertstore.high(
f"Found a known malicious domain in LocationD entry {result['package']}",
event_time,
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -126,7 +118,7 @@ class LocationdClients(IOSExtraction):
if ioc_match:
self.alertstore.high(
f"Found a suspicious file path in LocationD entry {result['Executable']}",
event_time,
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -141,7 +133,7 @@ class LocationdClients(IOSExtraction):
if ioc_match:
self.alertstore.high(
f"Found a suspicious file path in LocationD entry {result['Registered']}",
event_time,
"",
result,
matched_indicator=ioc_match.ioc,
)
@@ -65,10 +65,7 @@ class OSAnalyticsADDaily(IOSExtraction):
ioc_match = self.indicators.check_process(result["package"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("ts") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def run(self) -> None:
@@ -73,10 +73,7 @@ class SafariBrowserState(IOSExtraction):
ioc_match = self.indicators.check_url(result["tab_url"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("last_viewed_timestamp") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
continue
@@ -89,7 +86,7 @@ class SafariBrowserState(IOSExtraction):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("last_viewed_timestamp") or "",
"",
result,
matched_indicator=ioc_match.ioc,
)
+1 -4
View File
@@ -126,10 +126,7 @@ class SafariHistory(IOSExtraction):
ioc_match = self.indicators.check_url(result["url"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def _process_history_db(self, history_path):
+1 -4
View File
@@ -82,10 +82,7 @@ class Shortcuts(IOSExtraction):
):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def run(self) -> None:
+1 -4
View File
@@ -98,10 +98,7 @@ class SMS(IOSExtraction):
):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def collect_url_results(self) -> None:
+1 -1
View File
@@ -67,7 +67,7 @@ class SMSAttachments(IOSExtraction):
if ioc_match:
self.alertstore.high(
ioc_match.message,
attachment.get("isodate") or "",
"",
attachment,
matched_indicator=ioc_match.ioc,
)
+1 -4
View File
@@ -97,10 +97,7 @@ class TCC(IOSExtraction):
ioc_match = self.indicators.check_process(result["client"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("last_modified") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def process_db(self, file_path):
@@ -69,10 +69,7 @@ class WebkitResourceLoadStatistics(IOSExtraction):
ioc_match = self.indicators.check_url(result["registrable_domain"])
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("last_seen_isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def _process_observations_db(self, db_path: str, domain: str, path: str) -> None:
@@ -105,12 +105,7 @@ class WebkitSessionResourceLog(IOSExtraction):
if ioc_match:
entry, source_domains, destination_domains = record
self.alertstore.critical(
ioc_match.message,
entry.get("last_seen")
or entry.get("most_recent_interaction")
or "",
entry,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", entry, matched_indicator=ioc_match.ioc
)
redirect_path = ""
@@ -134,9 +129,7 @@ class WebkitSessionResourceLog(IOSExtraction):
self.alertstore.high(
f"Found HTTP redirect between suspicious domains: {redirect_path}",
entry.get("last_seen")
or entry.get("most_recent_interaction")
or "",
"",
entry,
)
+1 -4
View File
@@ -189,10 +189,7 @@ class Whatsapp(IOSExtraction):
):
if ioc_match:
self.alertstore.critical(
ioc_match.message,
result.get("isodate") or "",
result,
matched_indicator=ioc_match.ioc,
ioc_match.message, "", result, matched_indicator=ioc_match.ioc
)
def collect_url_results(self) -> None:
@@ -90,10 +90,4 @@ class TestDumpsysAppopsArtifact:
assert len(da.alertstore.alerts) == 3
assert len(detected_by_ioc) == 1
assert detected_by_ioc[0].matched_indicator is not None
assert detected_by_ioc[0].event_time == max(
entry["timestamp"]
for permission in detected_by_ioc[0].event["permissions"]
for entry in permission.get("entries", [])
if entry.get("timestamp")
)
assert len(detected_by_permission_heuristic) == 2
+43
View File
@@ -0,0 +1,43 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-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/
"""A mount is writable only if both mountinfo option layers allow it."""
from mvt.android.artifacts.mounts import Mounts
from mvt.common.alerts import AlertLevel
# fields[5] is the per-mount (VFS) layer, the field after "-" the superblock.
RO_OVERLAY = (
"304 166 0:130 / /product/usr rw,relatime shared:63 - overlay overlay "
"ro,seclabel,lowerdir=/mnt/vendor/ext/product/usr:/product/usr"
)
RW_SYSTEM = (
"305 166 0:131 / /system rw,relatime shared:64 - ext4 /dev/block/dm-1 "
"rw,seclabel,errors=panic"
)
def _run(line):
mounts = Mounts()
mounts.results = Mounts.parse_mountinfo(line, 1)
mounts.check_indicators()
return mounts
class TestMountsReadWriteLayers:
def test_rw_vfs_over_ro_superblock_is_not_writable(self):
mounts = _run(RO_OVERLAY)
assert mounts.results[0]["is_read_write"] is False
assert mounts.alertstore.count(AlertLevel.HIGH) == 0
assert mounts.alertstore.count(AlertLevel.MEDIUM) == 0
def test_rw_in_both_layers_still_alerts(self):
mounts = _run(RW_SYSTEM)
assert mounts.results[0]["is_read_write"] is True
assert mounts.alertstore.count(AlertLevel.HIGH) == 1
def test_both_option_layers_are_still_reported(self):
mounts = _run(RO_OVERLAY)
options = mounts.results[0]["options_list"]
assert "rw" in options and "ro" in options
-1
View File
@@ -44,4 +44,3 @@ class TestCalendarModule:
assert len(m.results) == 1
assert len(m.timeline) == 4
assert len(m.alertstore.alerts) == 1
assert m.alertstore.alerts[0].event_time == m.results[0]["start_date"]
Generated
+4 -56
View File
@@ -6,22 +6,6 @@ resolution-markers = [
"python_full_version < '3.15'",
]
[[package]]
name = "adb-shell"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "pyasn1" },
{ name = "rsa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8f/73/d246034db6f3e374dad9a35ee3f61345a6b239d4febd2a41ab69df9936fe/adb_shell-0.4.4.tar.gz", hash = "sha256:04c305f30a2ca25d5c54b3cd6ce9bb64c36e5f07967b23b3fb6aaecc851b90b6", size = 61822, upload-time = "2023-09-01T03:48:40.348Z" }
[package.optional-dependencies]
usb = [
{ name = "libusb1" },
]
[[package]]
name = "annotated-types"
version = "0.8.0"
@@ -625,14 +609,14 @@ wheels = [
[[package]]
name = "iphone-backup-decrypt"
version = "0.9.0"
version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycryptodome" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6f/e7/bcdacdec21d628122ba240e7f742ab2175149e58672be63af55ff37a0f28/iphone_backup_decrypt-0.9.0.tar.gz", hash = "sha256:13b18fef3c8e3af627914f8c1a429bbc5555dfb0505239ba49efe99984cc0c96", size = 16125, upload-time = "2024-09-18T15:50:12.179Z" }
sdist = { url = "https://files.pythonhosted.org/packages/01/b8/a2ec2b10d792403d36bb6661fea6357179c31ece1ed3285c442e636d6335/iphone_backup_decrypt-0.10.0.tar.gz", hash = "sha256:5f2799e58f908cce28d7929dd82231abc8acd57c25b37bce6a2d6a82e018d343", size = 20000, upload-time = "2026-09-17T21:46:44Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/94/64a31be93f72e0a254bde68e4cf7d24aef37a0a985754a197fa1b028a665/iphone_backup_decrypt-0.9.0-py3-none-any.whl", hash = "sha256:55b5adfafac757f58aa6444b83a4cc2c20cdd699c6ff1d2f4b549936a5dad92c", size = 15767, upload-time = "2024-09-18T15:50:10.537Z" },
{ url = "https://files.pythonhosted.org/packages/89/10/349b2a720e012921d1457e1d1c2b921b16300a5f3570c4c64d0d3787539f/iphone_backup_decrypt-0.10.0-py3-none-any.whl", hash = "sha256:1ac4345a07b104a3eacab20932b8621cd1e0164e0f03d913e66eed6ea5ee173e", size = 17400, upload-time = "2026-09-17T21:46:42Z" },
]
[[package]]
@@ -777,17 +761,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" },
]
[[package]]
name = "libusb1"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/89/35/f9d2a990d092d647b47540cd229e1d68432c0f51183484ca189612a4824c/libusb1-3.4.0.tar.gz", hash = "sha256:9cf5638506d54f21bf36550d97ea63189111a23c4d8078f630103a2052135f45", size = 91206, upload-time = "2026-05-16T20:59:19.315Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/64/d4b59444e4d3b6979aa5eb58840634465a24b41a9ab03dcf8434c9b89551/libusb1-3.4.0-py3-none-any.whl", hash = "sha256:e83d034e44c3efe1c4599c6281d34bca50a38c12cab3b7b6217d583161a01ffd", size = 67373, upload-time = "2026-05-16T20:59:13.142Z" },
{ url = "https://files.pythonhosted.org/packages/7e/55/a838a4278fac4ee49bf670ddc59d621999a1cb2a3188fb13a23609ea4f06/libusb1-3.4.0-py3-none-win32.whl", hash = "sha256:0a1aa1416034690eb9dc9a895eda0fef44d853bca1053eb1de50a5906684846d", size = 129704, upload-time = "2026-05-16T20:59:15.243Z" },
{ url = "https://files.pythonhosted.org/packages/5f/08/02aecf6dad627534a5835244ece14d7f187ef430354d9b8551199934d059/libusb1-3.4.0-py3-none-win_amd64.whl", hash = "sha256:b7dcc1f324a895af6aac708bc5513a17373f97349cc2ab8a277519c788bd18ca", size = 141212, upload-time = "2026-05-16T20:59:17.286Z" },
]
[[package]]
name = "markdown"
version = "3.10.3"
@@ -1016,13 +989,11 @@ wheels = [
name = "mvt"
source = { editable = "." }
dependencies = [
{ name = "adb-shell", extra = ["usb"] },
{ name = "appdirs" },
{ name = "betterproto2" },
{ name = "click" },
{ name = "cryptography" },
{ name = "iphone-backup-decrypt" },
{ name = "libusb1" },
{ name = "nskeyedunarchiver" },
{ name = "packaging" },
{ name = "pyahocorasick" },
@@ -1063,13 +1034,11 @@ docs = [
[package.metadata]
requires-dist = [
{ name = "adb-shell", extras = ["usb"], specifier = "==0.4.4" },
{ name = "appdirs", specifier = "==1.4.4" },
{ name = "betterproto2", specifier = "==0.10.0" },
{ name = "click", specifier = "==8.4.2" },
{ name = "cryptography", specifier = "==50.0.0" },
{ name = "iphone-backup-decrypt", specifier = "==0.9.0" },
{ name = "libusb1", specifier = "==3.4.0" },
{ name = "iphone-backup-decrypt", specifier = "==0.10.0" },
{ name = "nskeyedunarchiver", specifier = "==1.5.2" },
{ name = "packaging", specifier = "==26.3" },
{ name = "pyahocorasick", specifier = "==2.3.1" },
@@ -1274,15 +1243,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c5/96/37c50ac951bb0260ec38d8d12e5b51587ef1ef4035c279088f2771544b28/pyahocorasick-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4acb11a0a2ff10519465749d22ad70789e9fe7f81dc8fe9957a8868e499e18ab", size = 35987, upload-time = "2026-04-27T16:32:07.08Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
@@ -1684,18 +1644,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
name = "rsa"
version = "4.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
]
[[package]]
name = "ruff"
version = "0.16.2"