Compare commits

..

6 Commits
todos ... main

Author SHA1 Message Date
github-actions[bot]
134bfce90f Add new iOS versions and build numbers (#743)
Co-authored-by: DonnchaC <DonnchaC@users.noreply.github.com>
2026-03-26 12:16:59 -04:00
Tek
0141da4293 Fixes bug in IOC import (#749) 2026-03-25 23:23:08 +01:00
dependabot[bot]
5cba61b180 Bump mkdocstrings from 0.30.1 to 1.0.0 (#730)
Bumps [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) from 0.30.1 to 1.0.0.
- [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases)
- [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mkdocstrings/mkdocstrings/compare/0.30.1...1.0.0)

---
updated-dependencies:
- dependency-name: mkdocstrings
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: besendorf <janik@besendorf.org>
2026-03-25 15:24:07 +01:00
dependabot[bot]
29475acb47 Bump click from 8.3.0 to 8.3.1 (#731)
Bumps [click](https://github.com/pallets/click) from 8.3.0 to 8.3.1.
- [Release notes](https://github.com/pallets/click/releases)
- [Changelog](https://github.com/pallets/click/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/click/compare/8.3.0...8.3.1)

---
updated-dependencies:
- dependency-name: click
  dependency-version: 8.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: besendorf <janik@besendorf.org>
2026-03-25 14:37:59 +01:00
dependabot[bot]
1d5c83582c Bump pydantic from 2.12.3 to 2.12.5 (#732)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.12.3 to 2.12.5.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.12.3...v2.12.5)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.12.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: besendorf <janik@besendorf.org>
2026-03-25 14:26:09 +01:00
dependabot[bot]
2dd1428787 Bump cryptography from 46.0.3 to 46.0.5 (#747)
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.3 to 46.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/46.0.3...46.0.5)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 08:49:46 +01:00
20 changed files with 136 additions and 99 deletions

View File

@@ -2,4 +2,4 @@ mkdocs==1.6.1
mkdocs-autorefs==1.4.3 mkdocs-autorefs==1.4.3
mkdocs-material==9.6.20 mkdocs-material==9.6.20
mkdocs-material-extensions==1.3.1 mkdocs-material-extensions==1.3.1
mkdocstrings==0.30.1 mkdocstrings==1.0.0

View File

@@ -17,7 +17,7 @@ classifiers = [
"Programming Language :: Python", "Programming Language :: Python",
] ]
dependencies = [ dependencies = [
"click==8.3.0", "click==8.3.1",
"rich==14.1.0", "rich==14.1.0",
"tld==0.13.1", "tld==0.13.1",
"requests==2.32.5", "requests==2.32.5",
@@ -27,11 +27,11 @@ dependencies = [
"iOSbackup==0.9.925", "iOSbackup==0.9.925",
"adb-shell[usb]==0.4.4", "adb-shell[usb]==0.4.4",
"libusb1==3.3.1", "libusb1==3.3.1",
"cryptography==46.0.3", "cryptography==46.0.5",
"PyYAML>=6.0.2", "PyYAML>=6.0.2",
"pyahocorasick==2.2.0", "pyahocorasick==2.2.0",
"betterproto==1.2.5", "betterproto==1.2.5",
"pydantic==2.12.3", "pydantic==2.12.5",
"pydantic-settings==2.10.1", "pydantic-settings==2.10.1",
"NSKeyedUnArchiver==1.5.2", "NSKeyedUnArchiver==1.5.2",
"python-dateutil==2.9.0.post0", "python-dateutil==2.9.0.post0",
@@ -81,8 +81,8 @@ addopts = "-ra -q --cov=mvt --cov-report html --junitxml=pytest.xml --cov-report
testpaths = ["tests"] testpaths = ["tests"]
[tool.ruff] [tool.ruff]
select = ["C90", "E", "F", "W"] # flake8 default set lint.select = ["C90", "E", "F", "W"] # flake8 default set
ignore = [ lint.ignore = [
"E501", # don't enforce line length violations "E501", # don't enforce line length violations
"C901", # complex-structure "C901", # complex-structure
@@ -95,10 +95,10 @@ ignore = [
# "E203", # whitespace-before-punctuation # "E203", # whitespace-before-punctuation
] ]
[tool.ruff.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"] # unused-import "__init__.py" = ["F401"] # unused-import
[tool.ruff.mccabe] [tool.ruff.lint.mccabe]
max-complexity = 10 max-complexity = 10
[tool.setuptools] [tool.setuptools]

View File

@@ -186,7 +186,7 @@ class DumpsysPackagesArtifact(AndroidArtifact):
package = [] package = []
in_package_list = False in_package_list = False
for line in content.splitlines(): for line in content.split("\n"):
if line.startswith("Packages:"): if line.startswith("Packages:"):
in_package_list = True in_package_list = True
continue continue

View File

@@ -8,7 +8,7 @@ from .artifact import AndroidArtifact
class Processes(AndroidArtifact): class Processes(AndroidArtifact):
def parse(self, entry: str) -> None: def parse(self, entry: str) -> None:
for line in entry.splitlines()[1:]: for line in entry.split("\n")[1:]:
proc = line.split() proc = line.split()
# Skip empty lines # Skip empty lines

View File

@@ -193,7 +193,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
# eg. "Process uptime: 40s" # eg. "Process uptime: 40s"
tombstone[destination_key] = int(value_clean.rstrip("s")) tombstone[destination_key] = int(value_clean.rstrip("s"))
elif destination_key == "command_line": elif destination_key == "command_line":
# Wrap in list for consistency with protobuf format (repeated string). # XXX: Check if command line should be a single string in a list, or a list of strings.
tombstone[destination_key] = [value_clean] tombstone[destination_key] = [value_clean]
else: else:
tombstone[destination_key] = value_clean tombstone[destination_key] = value_clean

View File

@@ -117,6 +117,8 @@ def download_apks(ctx, all_apks, virustotal, output, from_file, serial, verbose)
if from_file: if from_file:
download = DownloadAPKs.from_json(from_file) download = DownloadAPKs.from_json(from_file)
else: else:
# TODO: Do we actually want to be able to run without storing any
# file?
if not output: if not output:
log.critical("You need to specify an output folder with --output!") log.critical("You need to specify an output folder with --output!")
ctx.exit(1) ctx.exit(1)

View File

@@ -105,15 +105,15 @@ class AQFFiles(AndroidQFModule):
) )
self.detected.append(result) self.detected.append(result)
for hash_key in ("sha256", "sha1", "md5"): if result.get("sha256", "") == "":
file_hash = result.get(hash_key, "") continue
if not file_hash:
continue ioc = self.indicators.check_file_hash(result["sha256"])
ioc = self.indicators.check_file_hash(file_hash) if ioc:
if ioc: result["matched_indicator"] = ioc
result["matched_indicator"] = ioc self.detected.append(result)
self.detected.append(result)
break # TODO: adds SHA1 and MD5 when available in MVT
def run(self) -> None: def run(self) -> None:
if timezone := self._get_device_timezone(): if timezone := self._get_device_timezone():
@@ -128,7 +128,7 @@ class AQFFiles(AndroidQFModule):
data = json.loads(rawdata) data = json.loads(rawdata)
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
data = [] data = []
for line in rawdata.splitlines(): for line in rawdata.split("\n"):
if line.strip() == "": if line.strip() == "":
continue continue
data.append(json.loads(line)) data.append(json.loads(line))
@@ -139,7 +139,7 @@ class AQFFiles(AndroidQFModule):
utc_timestamp = datetime.datetime.fromtimestamp( utc_timestamp = datetime.datetime.fromtimestamp(
file_data[ts], tz=datetime.timezone.utc file_data[ts], tz=datetime.timezone.utc
) )
# Convert the UTC timestamp to local time on Android device's local timezone # Convert the UTC timestamp to local tiem on Android device's local timezone
local_timestamp = utc_timestamp.astimezone(device_timezone) local_timestamp = utc_timestamp.astimezone(device_timezone)
# HACK: We only output the UTC timestamp in convert_datetime_to_iso, we # HACK: We only output the UTC timestamp in convert_datetime_to_iso, we

View File

@@ -39,7 +39,7 @@ class AQFSettings(SettingsArtifact, AndroidQFModule):
self.results[namespace] = {} self.results[namespace] = {}
data = self._get_file_content(setting_file) data = self._get_file_content(setting_file)
for line in data.decode("utf-8").splitlines(): for line in data.decode("utf-8").split("\n"):
line = line.strip() line = line.strip()
try: try:
key, value = line.split("=", 1) key, value = line.split("=", 1)

View File

@@ -222,6 +222,7 @@ class Command:
if self.module_name and module.__name__ != self.module_name: if self.module_name and module.__name__ != self.module_name:
continue continue
# FIXME: do we need the logger here
module_logger = logging.getLogger(module.__module__) module_logger = logging.getLogger(module.__module__)
m = module( m = module(

View File

@@ -52,9 +52,7 @@ class Indicators:
if os.path.isfile(path) and path.lower().endswith(".stix2"): if os.path.isfile(path) and path.lower().endswith(".stix2"):
self.parse_stix2(path) self.parse_stix2(path)
elif os.path.isdir(path): elif os.path.isdir(path):
for file in glob.glob( for file in glob.glob(os.path.join(path, "**", "*.stix2"), recursive=True):
os.path.join(path, "**", "*.stix2", recursive=True)
):
self.parse_stix2(file) self.parse_stix2(file)
else: else:
self.log.error( self.log.error(

View File

@@ -180,8 +180,10 @@ class IndicatorsUpdates:
def _get_remote_file_latest_commit( def _get_remote_file_latest_commit(
self, owner: str, repo: str, branch: str, path: str self, owner: str, repo: str, branch: str, path: str
) -> int: ) -> int:
# TODO: The branch is currently not taken into consideration.
# How do we specify which branch to look up to the API?
file_commit_url = ( file_commit_url = (
f"https://api.github.com/repos/{owner}/{repo}/commits?path={path}&sha={branch}" f"https://api.github.com/repos/{owner}/{repo}/commits?path={path}"
) )
try: try:
res = requests.get(file_commit_url, timeout=5) res = requests.get(file_commit_url, timeout=5)

View File

@@ -119,9 +119,10 @@ def convert_mactime_to_datetime(timestamp: Union[int, float], from_2001: bool =
if from_2001: if from_2001:
timestamp = timestamp + 978307200 timestamp = timestamp + 978307200
# TODO: This is rather ugly. Happens sometimes with invalid timestamps.
try: try:
return convert_unix_to_utc_datetime(timestamp) return convert_unix_to_utc_datetime(timestamp)
except (OSError, OverflowError, ValueError): except Exception:
return None return None

View File

@@ -907,6 +907,10 @@
"version": "15.8.6", "version": "15.8.6",
"build": "19H402" "build": "19H402"
}, },
{
"version": "15.8.7",
"build": "19H411"
},
{ {
"build": "20A362", "build": "20A362",
"version": "16.0" "version": "16.0"
@@ -1020,6 +1024,10 @@
"version": "16.7.14", "version": "16.7.14",
"build": "20H370" "build": "20H370"
}, },
{
"version": "16.7.15",
"build": "20H380"
},
{ {
"version": "17.0", "version": "17.0",
"build": "21A327" "build": "21A327"
@@ -1188,6 +1196,10 @@
"version": "18.7.6", "version": "18.7.6",
"build": "22H320" "build": "22H320"
}, },
{
"version": "18.7.7",
"build": "22H333"
},
{ {
"version": "26", "version": "26",
"build": "23A341" "build": "23A341"
@@ -1215,5 +1227,9 @@
{ {
"version": "26.3.1", "version": "26.3.1",
"build": "23D8133" "build": "23D8133"
},
{
"version": "26.4",
"build": "23E246"
} }
] ]

View File

@@ -87,35 +87,6 @@ class ConfigurationProfiles(IOSExtraction):
self.detected.append(result) self.detected.append(result)
continue continue
@staticmethod
def _b64encode_key(d: dict, key: str) -> None:
if key in d:
d[key] = b64encode(d[key])
@staticmethod
def _b64encode_keys(d: dict, keys: list) -> None:
for key in keys:
if key in d:
d[key] = b64encode(d[key])
def _b64encode_plist_bytes(self, plist: dict) -> None:
"""Encode binary plist values to base64 for JSON serialization."""
if "SignerCerts" in plist:
plist["SignerCerts"] = [b64encode(x) for x in plist["SignerCerts"]]
self._b64encode_keys(plist, ["PushTokenDataSentToServerKey", "LastPushTokenHash"])
if "OTAProfileStub" in plist:
stub = plist["OTAProfileStub"]
if "SignerCerts" in stub:
stub["SignerCerts"] = [b64encode(x) for x in stub["SignerCerts"]]
if "PayloadContent" in stub:
self._b64encode_key(stub["PayloadContent"], "EnrollmentIdentityPersistentID")
if "PayloadContent" in plist:
for entry in plist["PayloadContent"]:
self._b64encode_keys(entry, ["PERSISTENT_REF", "IdentityPersistentRef"])
def run(self) -> None: def run(self) -> None:
for conf_file in self._get_backup_files_from_manifest( for conf_file in self._get_backup_files_from_manifest(
domain=CONF_PROFILES_DOMAIN domain=CONF_PROFILES_DOMAIN
@@ -144,7 +115,65 @@ class ConfigurationProfiles(IOSExtraction):
except Exception: except Exception:
conf_plist = {} conf_plist = {}
self._b64encode_plist_bytes(conf_plist) # TODO: Tidy up the following code hell.
if "SignerCerts" in conf_plist:
conf_plist["SignerCerts"] = [
b64encode(x) for x in conf_plist["SignerCerts"]
]
if "OTAProfileStub" in conf_plist:
if "SignerCerts" in conf_plist["OTAProfileStub"]:
conf_plist["OTAProfileStub"]["SignerCerts"] = [
b64encode(x)
for x in conf_plist["OTAProfileStub"]["SignerCerts"]
]
if "PayloadContent" in conf_plist["OTAProfileStub"]:
if (
"EnrollmentIdentityPersistentID"
in conf_plist["OTAProfileStub"]["PayloadContent"]
):
conf_plist["OTAProfileStub"]["PayloadContent"][
"EnrollmentIdentityPersistentID"
] = b64encode(
conf_plist["OTAProfileStub"]["PayloadContent"][
"EnrollmentIdentityPersistentID"
]
)
if "PushTokenDataSentToServerKey" in conf_plist:
conf_plist["PushTokenDataSentToServerKey"] = b64encode(
conf_plist["PushTokenDataSentToServerKey"]
)
if "LastPushTokenHash" in conf_plist:
conf_plist["LastPushTokenHash"] = b64encode(
conf_plist["LastPushTokenHash"]
)
if "PayloadContent" in conf_plist:
for content_entry in range(len(conf_plist["PayloadContent"])):
if "PERSISTENT_REF" in conf_plist["PayloadContent"][content_entry]:
conf_plist["PayloadContent"][content_entry][
"PERSISTENT_REF"
] = b64encode(
conf_plist["PayloadContent"][content_entry][
"PERSISTENT_REF"
]
)
if (
"IdentityPersistentRef"
in conf_plist["PayloadContent"][content_entry]
):
conf_plist["PayloadContent"][content_entry][
"IdentityPersistentRef"
] = b64encode(
conf_plist["PayloadContent"][content_entry][
"IdentityPersistentRef"
]
)
self.results.append( self.results.append(
{ {

View File

@@ -73,7 +73,7 @@ class ShutdownLog(IOSExtraction):
recent_processes = [] recent_processes = []
times_delayed = 0 times_delayed = 0
delay = 0.0 delay = 0.0
for line in content.splitlines(): for line in content.split("\n"):
line = line.strip() line = line.strip()
if line.startswith("remaining client pid:"): if line.startswith("remaining client pid:"):

View File

@@ -11,6 +11,7 @@ from mvt.common.utils import convert_chrometime_to_datetime, convert_datetime_to
from ..base import IOSExtraction from ..base import IOSExtraction
CHROME_FAVICON_BACKUP_IDS = ["55680ab883d0fdcffd94f959b1632e5fbbb18c5b"] CHROME_FAVICON_BACKUP_IDS = ["55680ab883d0fdcffd94f959b1632e5fbbb18c5b"]
# TODO: Confirm Chrome database path.
CHROME_FAVICON_ROOT_PATHS = [ CHROME_FAVICON_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/Library/Application Support/Google/Chrome/Default/Favicons", "private/var/mobile/Containers/Data/Application/*/Library/Application Support/Google/Chrome/Default/Favicons",
] ]

View File

@@ -13,6 +13,7 @@ from ..base import IOSExtraction
CHROME_HISTORY_BACKUP_IDS = [ CHROME_HISTORY_BACKUP_IDS = [
"faf971ce92c3ac508c018dce1bef2a8b8e9838f1", "faf971ce92c3ac508c018dce1bef2a8b8e9838f1",
] ]
# TODO: Confirm Chrome database path.
CHROME_HISTORY_ROOT_PATHS = [ CHROME_HISTORY_ROOT_PATHS = [
"private/var/mobile/Containers/Data/Application/*/Library/Application Support/Google/Chrome/Default/History", # pylint: disable=line-too-long "private/var/mobile/Containers/Data/Application/*/Library/Application Support/Google/Chrome/Default/History", # pylint: disable=line-too-long
] ]

View File

@@ -79,55 +79,32 @@ class WebkitResourceLoadStatistics(IOSExtraction):
cur = conn.cursor() cur = conn.cursor()
try: try:
# FIXME: table contains extra fields with timestamp here
cur.execute( cur.execute(
""" """
SELECT SELECT
domainID, domainID,
registrableDomain, registrableDomain,
lastSeen, lastSeen,
hadUserInteraction, hadUserInteraction
mostRecentUserInteractionTime,
mostRecentWebPushInteractionTime
from ObservedDomains; from ObservedDomains;
""" """
) )
has_extra_timestamps = True
except sqlite3.OperationalError: except sqlite3.OperationalError:
try: return
cur.execute(
"""
SELECT
domainID,
registrableDomain,
lastSeen,
hadUserInteraction
from ObservedDomains;
"""
)
has_extra_timestamps = False
except sqlite3.OperationalError:
return
for row in cur: for row in cur:
result = { self.results.append(
"domain_id": row[0], {
"registrable_domain": row[1], "domain_id": row[0],
"last_seen": row[2], "registrable_domain": row[1],
"had_user_interaction": bool(row[3]), "last_seen": row[2],
"last_seen_isodate": convert_unix_to_iso(row[2]), "had_user_interaction": bool(row[3]),
"domain": domain, "last_seen_isodate": convert_unix_to_iso(row[2]),
"path": path, "domain": domain,
} "path": path,
if has_extra_timestamps: }
result["most_recent_user_interaction_time"] = row[4] )
result["most_recent_user_interaction_time_isodate"] = (
convert_unix_to_iso(row[4])
)
result["most_recent_web_push_interaction_time"] = row[5]
result["most_recent_web_push_interaction_time_isodate"] = (
convert_unix_to_iso(row[5])
)
self.results.append(result)
if len(self.results) > 0: if len(self.results) > 0:
self.log.info( self.log.info(

View File

@@ -76,6 +76,12 @@ class WebkitSessionResourceLog(IOSExtraction):
entry["redirect_destination"] entry["redirect_destination"]
) )
# TODO: Currently not used.
# subframe_origins = self._extract_domains(
# entry["subframe_under_origin"])
# subresource_domains = self._extract_domains(
# entry["subresource_under_origin"])
all_origins = set( all_origins = set(
[entry["origin"]] + source_domains + destination_domains [entry["origin"]] + source_domains + destination_domains
) )

View File

@@ -311,11 +311,14 @@ class NetBase(IOSExtraction):
self.results = sorted(self.results, key=operator.itemgetter("first_isodate")) self.results = sorted(self.results, key=operator.itemgetter("first_isodate"))
def check_indicators(self) -> None: def check_indicators(self) -> None:
# check_manipulated/find_deleted require "live_isodate" and # Check for manipulated process records.
# "live_proc_id" keys which may be absent in older result formats. # TODO: Catching KeyError for live_isodate for retro-compatibility.
if self.results and "live_isodate" in self.results[0]: # This is not very good.
try:
self.check_manipulated() self.check_manipulated()
self.find_deleted() self.find_deleted()
except KeyError:
pass
if not self.indicators: if not self.indicators:
return return