Compare commits

..

15 Commits

Author SHA1 Message Date
Tek
61f51caf31 Freeze versions and bump version (#632)
* Freeze versions and bump version
* Drops support for python below 3.10
2025-06-12 16:33:15 +02:00
besendorf
511063fd0e Update pyproject.toml (#630) 2025-06-04 13:00:04 +02:00
scribblemaniac
88bc5672cb Upgrade main dockerfile runtime to ubuntu:24.04 (#619)
Co-authored-by: Tek <tek@randhome.io>
2025-05-14 11:34:40 +02:00
github-actions[bot]
0fce0acf7a Add new iOS versions and build numbers (#626)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-05-14 11:12:13 +02:00
github-actions[bot]
61f95d07d3 Add new iOS versions and build numbers (#625)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-05-12 22:37:46 +02:00
ping2A
3dedd169c4 Fix issue #574 for a module without IOCs output (#620)
* Fix issue #574 for a module without IOCs output
2025-04-30 10:30:39 +02:00
Tek
e34e03d3a3 Fixes Android Dumpsys ADB parsing issue 2025-04-18 17:43:08 +02:00
github-actions[bot]
34374699ce Add new iOS versions and build numbers (#622)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-04-17 09:46:17 +02:00
github-actions[bot]
cf5aa7c89f Add new iOS versions and build numbers (#618)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-04-01 16:04:06 +02:00
Donncha Ó Cearbhaill
2766739512 Fix bug where default values were dropped when parsing protobuf tombstones (#617) 2025-03-11 14:10:34 +01:00
cacu
9c84afb4b0 Update logo.py (#615)
add instructions to update mvt via pipx
2025-03-11 13:46:59 +01:00
Donncha Ó Cearbhaill
80fc8bd879 Fix YAML format (#611) 2025-02-21 15:48:00 +01:00
Donncha Ó Cearbhaill
ca41f7f106 Always open automatic PRs as drafts (#609) 2025-02-21 15:35:06 +01:00
github-actions[bot]
55ddd86ad5 Add new iOS versions and build numbers (#607)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2025-02-21 15:24:27 +01:00
Donncha Ó Cearbhaill
b184eeedf4 Handle XML encoded ADB keystore and fix parsing bugs (#605) 2025-02-07 02:00:24 +01:00
14 changed files with 161 additions and 79 deletions

View File

@@ -12,7 +12,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ['3.8', '3.9', '3.10'] # , '3.11'] python-version: ['3.10', '3.11', '3.12', '3.13']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -35,4 +35,4 @@ jobs:
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
with: with:
pytest-coverage-path: ./pytest-coverage.txt pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml junitxml-path: ./pytest.xml

View File

@@ -21,6 +21,7 @@ jobs:
title: '[auto] Update iOS releases and versions' title: '[auto] Update iOS releases and versions'
commit-message: Add new iOS versions and build numbers commit-message: Add new iOS versions and build numbers
branch: auto/add-new-ios-releases branch: auto/add-new-ios-releases
draft: true
body: | body: |
This is an automated pull request to update the iOS releases and version numbers. This is an automated pull request to update the iOS releases and version numbers.
add-paths: | add-paths: |

View File

@@ -103,7 +103,7 @@ RUN git clone https://github.com/libimobiledevice/usbmuxd && cd usbmuxd \
# Create main image # Create main image
FROM ubuntu:22.04 as main FROM ubuntu:24.04 as main
LABEL org.opencontainers.image.url="https://mvt.re" LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re" LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
@@ -135,8 +135,7 @@ COPY --from=build-usbmuxd /build /
COPY . mvt/ COPY . mvt/
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y git python3-pip \ && apt-get install -y git python3-pip \
&& PIP_NO_CACHE_DIR=1 pip3 install --upgrade pip \ && PIP_NO_CACHE_DIR=1 pip3 install --break-system-packages ./mvt \
&& PIP_NO_CACHE_DIR=1 pip3 install ./mvt \
&& apt-get remove -y python3-pip git && apt-get autoremove -y \ && apt-get remove -y python3-pip git && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& rm -rf mvt && rm -rf mvt

View File

@@ -19,25 +19,25 @@ classifiers = [
"Programming Language :: Python" "Programming Language :: Python"
] ]
dependencies = [ dependencies = [
"click >=8.1.3", "click==8.2.1",
"rich >=12.6.0", "rich==14.0.0",
"tld >=0.12.6", "tld==0.13.1",
"requests >=2.28.1", "requests==2.32.2",
"simplejson >=3.17.6", "simplejson==3.20.1",
"packaging >=21.3", "packaging==25.0",
"appdirs >=1.4.4", "appdirs==1.4.4",
"iOSbackup >=0.9.923", "iOSbackup==0.9.925",
"adb-shell[usb] >=0.4.3", "adb-shell[usb]==0.4.4",
"libusb1 >=3.0.0", "libusb1==3.3.1",
"cryptography >=42.0.5", "cryptography==45.0.3",
"pyyaml >=6.0", "PyYAML>=6.0.2",
"pyahocorasick >= 2.0.0", "pyahocorasick==2.1.0",
"betterproto >=1.2.0", "betterproto==1.2.5",
"pydantic >= 2.10.0", "pydantic==2.11.5",
"pydantic-settings >= 2.7.0", "pydantic-settings==2.9.1",
'backports.zoneinfo; python_version < "3.9"', "NSKeyedUnArchiver==1.5",
] ]
requires-python = ">= 3.8" requires-python = ">= 3.10"
[project.urls] [project.urls]
homepage = "https://docs.mvt.re/en/latest/" homepage = "https://docs.mvt.re/en/latest/"
@@ -103,4 +103,4 @@ where = ["src"]
mvt = ["ios/data/*.json"] mvt = ["ios/data/*.json"]
[tool.setuptools.dynamic] [tool.setuptools.dynamic]
version = {attr = "mvt.common.version.MVT_VERSION"} version = {attr = "mvt.common.version.MVT_VERSION"}

View File

@@ -4,13 +4,14 @@
# https://license.mvt.re/1.1/ # https://license.mvt.re/1.1/
import base64 import base64
import binascii
import hashlib import hashlib
from .artifact import AndroidArtifact from .artifact import AndroidArtifact
class DumpsysADBArtifact(AndroidArtifact): class DumpsysADBArtifact(AndroidArtifact):
multiline_fields = ["user_keys"] multiline_fields = ["user_keys", "keystore"]
def indented_dump_parser(self, dump_data): def indented_dump_parser(self, dump_data):
""" """
@@ -67,14 +68,38 @@ class DumpsysADBArtifact(AndroidArtifact):
return res return res
def parse_xml(self, xml_data):
"""
Parse XML data from dumpsys ADB output
"""
import xml.etree.ElementTree as ET
keystore = []
keystore_root = ET.fromstring(xml_data)
for adb_key in keystore_root.findall("adbKey"):
key_info = self.calculate_key_info(adb_key.get("key").encode("utf-8"))
key_info["last_connected"] = adb_key.get("lastConnection")
keystore.append(key_info)
return keystore
@staticmethod @staticmethod
def calculate_key_info(user_key: bytes) -> str: def calculate_key_info(user_key: bytes) -> str:
key_base64, user = user_key.split(b" ", 1) if b" " in user_key:
key_raw = base64.b64decode(key_base64) key_base64, user = user_key.split(b" ", 1)
key_fingerprint = hashlib.md5(key_raw).hexdigest().upper() else:
key_fingerprint_colon = ":".join( key_base64, user = user_key, b""
[key_fingerprint[i : i + 2] for i in range(0, len(key_fingerprint), 2)]
) try:
key_raw = base64.b64decode(key_base64)
key_fingerprint = hashlib.md5(key_raw).hexdigest().upper()
key_fingerprint_colon = ":".join(
[key_fingerprint[i : i + 2] for i in range(0, len(key_fingerprint), 2)]
)
except binascii.Error:
# Impossible to parse base64
key_fingerprint_colon = ""
return { return {
"user": user.decode("utf-8"), "user": user.decode("utf-8"),
"fingerprint": key_fingerprint_colon, "fingerprint": key_fingerprint_colon,
@@ -115,8 +140,24 @@ class DumpsysADBArtifact(AndroidArtifact):
if parsed.get("debugging_manager") is None: if parsed.get("debugging_manager") is None:
self.log.error("Unable to find expected ADB entries in dumpsys output") # noqa self.log.error("Unable to find expected ADB entries in dumpsys output") # noqa
return return
# Keystore can be in different levels, as the basic parser
# is not always consistent due to different dumpsys formats.
if parsed.get("keystore"):
keystore_data = b"\n".join(parsed["keystore"])
elif parsed["debugging_manager"].get("keystore"):
keystore_data = b"\n".join(parsed["debugging_manager"]["keystore"])
else: else:
parsed = parsed["debugging_manager"] keystore_data = None
# Keystore is in XML format on some devices and we need to parse it
if keystore_data and keystore_data.startswith(b"<?xml"):
parsed["debugging_manager"]["keystore"] = self.parse_xml(keystore_data)
else:
# Keystore is not XML format
parsed["debugging_manager"]["keystore"] = keystore_data
parsed = parsed["debugging_manager"]
# Calculate key fingerprints for better readability # Calculate key fingerprints for better readability
key_info = [] key_info = []

View File

@@ -62,7 +62,7 @@ class TombstoneCrashResult(pydantic.BaseModel):
process_name: Optional[str] = None process_name: Optional[str] = None
binary_path: Optional[str] = None binary_path: Optional[str] = None
selinux_label: Optional[str] = None selinux_label: Optional[str] = None
uid: Optional[int] = None uid: int
signal_info: SignalInfo signal_info: SignalInfo
cause: Optional[str] = None cause: Optional[str] = None
extra: Optional[str] = None extra: Optional[str] = None
@@ -124,7 +124,9 @@ class TombstoneCrashArtifact(AndroidArtifact):
Parse Android tombstone crash files from a protobuf object. Parse Android tombstone crash files from a protobuf object.
""" """
tombstone_pb = Tombstone().parse(data) tombstone_pb = Tombstone().parse(data)
tombstone_dict = tombstone_pb.to_dict(betterproto.Casing.SNAKE) tombstone_dict = tombstone_pb.to_dict(
betterproto.Casing.SNAKE, include_default_values=True
)
# Add some extra metadata # Add some extra metadata
tombstone_dict["timestamp"] = self._parse_timestamp_string( tombstone_dict["timestamp"] = self._parse_timestamp_string(

View File

@@ -12,8 +12,6 @@ from typing import List, Optional
from mvt.common.command import Command from mvt.common.command import Command
from .modules.androidqf import ANDROIDQF_MODULES from .modules.androidqf import ANDROIDQF_MODULES
from .modules.bugreport import BUGREPORT_MODULES
from .modules.bugreport.base import BugReportModule
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -41,11 +39,7 @@ class CmdAndroidCheckAndroidQF(Command):
) )
self.name = "check-androidqf" self.name = "check-androidqf"
self.modules = ANDROIDQF_MODULES
# We can load AndroidQF and bugreport modules here, as
# AndroidQF dump will contain a bugreport.
self.modules = ANDROIDQF_MODULES + BUGREPORT_MODULES
# TODO: Check how to namespace and deduplicate modules.
self.format: Optional[str] = None self.format: Optional[str] = None
self.archive: Optional[zipfile.ZipFile] = None self.archive: Optional[zipfile.ZipFile] = None
@@ -60,44 +54,12 @@ class CmdAndroidCheckAndroidQF(Command):
for fname in subfiles: for fname in subfiles:
file_path = os.path.relpath(os.path.join(root, fname), parent_path) file_path = os.path.relpath(os.path.join(root, fname), parent_path)
self.files.append(file_path) self.files.append(file_path)
elif os.path.isfile(self.target_path): elif os.path.isfile(self.target_path):
self.format = "zip" self.format = "zip"
self.archive = zipfile.ZipFile(self.target_path) self.archive = zipfile.ZipFile(self.target_path)
self.files = self.archive.namelist() self.files = self.archive.namelist()
def load_bugreport(self):
# Refactor this file list loading
# First we need to find the bugreport file location
bugreport_zip_path = None
for file_name in self.files:
if file_name.endswith("bugreport.zip"):
bugreport_zip_path = file_name
break
else:
self.log.warning("No bugreport.zip found in the AndroidQF dump")
return None
if self.format == "zip":
# Create handle to the bugreport.zip file inside the AndroidQF dump
handle = self.archive.open(bugreport_zip_path)
bugreport_zip = zipfile.ZipFile(handle)
else:
# Load the bugreport.zip file from the extracted AndroidQF dump on disk.
parent_path = Path(self.target_path).absolute().parent.as_posix()
bug_report_path = os.path.join(parent_path, bugreport_zip_path)
bugreport_zip = zipfile.ZipFile(bug_report_path)
return bugreport_zip
def module_init(self, module): def module_init(self, module):
if isinstance(module, BugReportModule):
bugreport_archive = self.load_bugreport()
if not bugreport_archive:
return
module.from_zip(bugreport_archive, bugreport_archive.namelist())
return
if self.format == "zip": if self.format == "zip":
module.from_zip_file(self.archive, self.files) module.from_zip_file(self.archive, self.files)
else: else:

View File

@@ -65,6 +65,10 @@ class CmdCheckIOCS(Command):
m = iocs_module.from_json( m = iocs_module.from_json(
file_path, log=logging.getLogger(iocs_module.__module__) file_path, log=logging.getLogger(iocs_module.__module__)
) )
if not m:
log.warning("No result from this module, skipping it")
continue
if self.iocs.total_ioc_count > 0: if self.iocs.total_ioc_count > 0:
m.indicators = self.iocs m.indicators = self.iocs
m.indicators.log = m.log m.indicators.log = m.log

View File

@@ -29,7 +29,7 @@ def check_updates() -> None:
if latest_version: if latest_version:
rich_print( rich_print(
f"\t\t[bold]Version {latest_version} is available! " f"\t\t[bold]Version {latest_version} is available! "
"Upgrade mvt with `pip3 install -U mvt`[/bold]" "Upgrade mvt with `pip3 install -U mvt` or with `pipx upgrade mvt`[/bold]"
) )
# Then we check for indicators files updates. # Then we check for indicators files updates.

View File

@@ -69,10 +69,14 @@ class MVTModule:
@classmethod @classmethod
def from_json(cls, json_path: str, log: logging.Logger): def from_json(cls, json_path: str, log: logging.Logger):
with open(json_path, "r", encoding="utf-8") as handle: with open(json_path, "r", encoding="utf-8") as handle:
results = json.load(handle) try:
if log: results = json.load(handle)
log.info('Loaded %d results from "%s"', len(results), json_path) if log:
return cls(results=results, log=log) log.info('Loaded %d results from "%s"', len(results), json_path)
return cls(results=results, log=log)
except json.decoder.JSONDecodeError as err:
log.error('Error to decode the json "%s" file: "%s"', json_path, err)
return None
@classmethod @classmethod
def get_slug(cls) -> str: def get_slug(cls) -> str:

View File

@@ -3,4 +3,4 @@
# Use of this software is governed by the MVT License 1.1 that can be found at # Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/ # https://license.mvt.re/1.1/
MVT_VERSION = "2.6.0" MVT_VERSION = "2.6.1"

View File

@@ -891,6 +891,10 @@
"version": "15.8.2", "version": "15.8.2",
"build": "19H384" "build": "19H384"
}, },
{
"version": "15.8.4",
"build": "19H390"
},
{ {
"build": "20A362", "build": "20A362",
"version": "16.0" "version": "16.0"
@@ -992,6 +996,10 @@
"version": "16.7.8", "version": "16.7.8",
"build": "20H343" "build": "20H343"
}, },
{
"version": "16.7.11",
"build": "20H360"
},
{ {
"version": "17.0", "version": "17.0",
"build": "21A327" "build": "21A327"
@@ -1076,6 +1084,10 @@
"version": "17.6.1", "version": "17.6.1",
"build": "21G101" "build": "21G101"
}, },
{
"version": "17.7.7",
"build": "21H433"
},
{ {
"version": "18", "version": "18",
"build": "22A3354" "build": "22A3354"
@@ -1103,5 +1115,21 @@
{ {
"version": "18.3", "version": "18.3",
"build": "22D63" "build": "22D63"
},
{
"version": "18.3.1",
"build": "22D72"
},
{
"version": "18.4",
"build": "22E240"
},
{
"version": "18.4.1",
"build": "22E252"
},
{
"version": "18.5",
"build": "22F76"
} }
] ]

View File

@@ -29,3 +29,28 @@ class TestDumpsysADBArtifact:
user_key["fingerprint"] == "F0:A1:3D:8C:B3:F4:7B:09:9F:EE:8B:D8:38:2E:BD:C6" user_key["fingerprint"] == "F0:A1:3D:8C:B3:F4:7B:09:9F:EE:8B:D8:38:2E:BD:C6"
) )
assert user_key["user"] == "user@linux" assert user_key["user"] == "user@linux"
def test_parsing_adb_xml(self):
da_adb = DumpsysADBArtifact()
file = get_artifact("android_data/dumpsys_adb_xml.txt")
with open(file, "rb") as f:
data = f.read()
da_adb.parse(data)
assert len(da_adb.results) == 1
adb_data = da_adb.results[0]
assert "user_keys" in adb_data
assert len(adb_data["user_keys"]) == 1
# Check key and fingerprint parsed successfully.
expected_fingerprint = "F0:0B:27:08:E3:68:7B:FA:4C:79:A2:B4:BF:0E:CF:70"
user_key = adb_data["user_keys"][0]
user_key["fingerprint"] == expected_fingerprint
assert user_key["user"] == "user@laptop"
key_store_entry = adb_data["keystore"][0]
assert key_store_entry["user"] == "user@laptop"
assert key_store_entry["fingerprint"] == expected_fingerprint
assert key_store_entry["last_connected"] == "1628501829898"

View File

@@ -0,0 +1,16 @@
-------------------------------------------------------------------------------
DUMP OF SERVICE adb:
ADB MANAGER STATE (dumpsys adb):
{
debugging_manager={
connected_to_adb=true
user_keys=QAAAAAcgbytJst31DsaSP7hn8QcBXKR9NPVPK9MZssFVSNIP user@laptop
keystore=<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<keyStore version="1">
<adbKey key="QAAAAAcgbytJst31DsaSP7hn8QcBXKR9NPVPK9MZssFVSNIP user@laptop" lastConnection="1628501829898" />
</keyStore>
}
}
--------- 0.012s was the duration of dumpsys adb, ending at: 2025-02-04 20:25:58