diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c20dc71..5acb3b51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..cc7f0b97 --- /dev/null +++ b/MANIFEST.in @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 9f423d2e..4154662b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/src/mvt/android/artifacts/mounts.py b/src/mvt/android/artifacts/mounts.py index 5a5537b7..0380a785 100644 --- a/src/mvt/android/artifacts/mounts.py +++ b/src/mvt/android/artifacts/mounts.py @@ -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"] diff --git a/src/mvt/ios/data/ios_versions.json b/src/mvt/ios/data/ios_versions.json index d8a93c91..8cea66c2 100644 --- a/src/mvt/ios/data/ios_versions.json +++ b/src/mvt/ios/data/ios_versions.json @@ -1275,5 +1275,9 @@ { "version": "26.6.2", "build": "23G90" + }, + { + "version": "27.0", + "build": "24A437" } ] \ No newline at end of file diff --git a/src/mvt/ios/decrypt.py b/src/mvt/ios/decrypt.py index dc30d9c9..ffc7bfa0 100644 --- a/src/mvt/ios/decrypt.py +++ b/src/mvt/ios/decrypt.py @@ -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): diff --git a/tests/android/test_artifact_mounts.py b/tests/android/test_artifact_mounts.py new file mode 100644 index 00000000..6b7d1faf --- /dev/null +++ b/tests/android/test_artifact_mounts.py @@ -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 diff --git a/uv.lock b/uv.lock index a1d370e7..085b1a35 100644 --- a/uv.lock +++ b/uv.lock @@ -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"