Commit Graph
172 Commits
Author SHA1 Message Date
Donncha Ó Cearbhaill 32c88822a0 Merge branch 'main' into fix/bugreport-parser-coverage 2026-08-30 14:11:54 +02:00
Donncha Ó Cearbhaill 7d41d0646e Keep the start of mvt-ios and mvt-android cheap for shell completion
Each console script imports its CLI module before Click can answer a
shell completion request, and the completion scripts run the program on
every keystroke. Importing mvt.ios.cli or mvt.android.cli took ~230 ms,
of which building the command tree needed almost nothing: cli_plugins
imported one constant from module_loader, which pulled in MVTModule, the
indicators, the pydantic settings, requests and rich; the command
implementations pulled in the same, and the iOS CLI imported iOSbackup
(pycryptodome) for decrypt-backup.

The two platform CLI modules now only build the command tree: each
command imports what it runs when it is invoked. cli_plugins owns the
custom command prefix instead of importing it from module_loader, and
exec_or_profile() loads the settings when it runs.

Completion of mvt-ios and mvt-android drops from ~260 ms to ~85 ms per
keystroke on a clean install, and every command starts that much
sooner. A test fails as soon as a CLI module imports the module
machinery again.
2026-08-28 00:42:08 +02:00
Donncha Ó Cearbhaill 24c65859ee Merge branch 'main' into fix/bugreport-parser-coverage 2026-08-27 21:06:30 +02:00
Donncha Ó Cearbhaill c9a57f5d10 Do not pin the wording of the mvt help text in the test 2026-08-27 15:34:53 +02:00
Donncha Ó Cearbhaill b06b5c36ac Merge branch 'main' into feature/cli-plugin-enhancements 2026-08-27 15:18:23 +02:00
Donncha Ó Cearbhaill 00d892d354 Run check-iocs on every module which implements check_indicators() (#903)
A module is part of check-iocs for a platform when it declares the
check-iocs pair, as before. It is now also part of check-iocs when it
overrides check_indicators() and supports at least one command of that
platform.

The rule lives in module_supports_command(). CmdCheckIOCS already uses
that function to pick its modules, so --list-modules and --module follow
it too. Built-in modules are unaffected. check-iocs takes them from
IOS_CHECK_IOCS_MODULES and ANDROID_CHECK_IOCS_MODULES.

A custom module which replaces a built-in module is covered by the same
rule. If it subclasses the module it replaces, it inherits its
check_indicators() and takes over the re-check of the results file. The
"Replacing a built-in module" section of the plugin documentation is
updated to say that.
2026-08-27 14:47:17 +02:00
Donncha Ó Cearbhaill 47ac8a5a85 Allow custom modules to replace built-in modules (#902)
A custom module which extends a built-in one ran alongside it, and both
wrote to the same results file when they shared a slug, with the run
order deciding the surviving content.

Custom modules can now name the module class they supersede in a
`replaces` attribute. When both are available to a command, the named
module is dropped from the run and the substitution is logged with the
origin of the replacement, so it is recorded in command.log. Only the
replacements which are applied are reported: a declaration from a
disabled module is ignored, so that replacing a module cannot silently
disable it, and modules which replace each other in a cycle all keep
running and replace nothing, with a warning naming every one of them.

Dependencies are remapped along with the modules themselves: a module
depending on a replaced class is ordered against, and receives the
results of, the module which took its place. Replacing a module which
others depend on therefore does not make that dependency unavailable.

The remapping applies wherever dependencies are read, so a replacement
which cannot run is skipped like any other module with an unavailable
dependency, and takes the modules depending on the module it replaced
with it. Those warnings name the dependency the author declared as well
as the module which replaces it.

A replacement does not have to keep the class name of the module it
replaces, so `--module` now falls back to the name of a replaced module
and runs its replacement. A name which matches no module at all stops
the run with a warning instead of silently analyzing nothing, as does a
selection left with nothing to run once skipped modules are dropped.

Sharing a slug outside a replacement stays possible and is now reported.
Two modules writing to the same results file is a forensic-integrity
problem rather than an error, so both still run and a warning names them,
where each came from, and the file the later one overwrites. Taking over
the slug of a replaced module is not reported, because that module is no
longer part of the run.
2026-08-27 14:47:17 +02:00
Donncha Ó Cearbhaill 0b5b3f2d7c Add the mvt.plugin import surface (#901)
* Add the mvt.plugin import surface

mvt.plugin re-exports the names a plugin needs from MVT under one import
path. It holds the module base classes and Command, the alert and result
types, the database errors a module raises, the timestamp converters, the
plugin settings API, MVT's settings, get_plugin_logger() and MVT_VERSION.

The names it exports are kept working on a best-effort basis. Changes to
them are announced in the release notes. Anything else in mvt can still be
imported, and may change between releases without notice.

get_plugin_logger(__name__) returns a logger under mvt.ext for plugin code
outside a module class. Its records then reach the console and the
command.log file of a run. A file loaded with --load-module or
--load-command is named after the file.

* Document how to write MVT plugins

The custom modules page now leads with plugin packages. Loading module
files with --load-module and MVT_CUSTOM_MODULES moves to a section on
developing a module locally.

A new "Writing a module" section shows a module which subclasses
IOSExtraction. It lists each base class, the command pair it serves and the
helpers it provides. "Depending on a built-in module" says to import a
built-in class from its family package.

"Importing from MVT" says what mvt.plugin exports and what importing from
it means. The custom commands page shows a Command subclass which lists its
own modules. The sysdiagnose and plugin configuration pages import from
mvt.plugin.
2026-08-27 14:47:16 +02:00
Donncha Ó Cearbhaill 85adb02eb9 Share the check-iocs module lists between the CLI and the code (#900)
* Share the check-iocs module lists between the CLI and the code

check-iocs re-checks the results a previous run stored, so its module
list is every module of the platform that could have written one. Each
platform's CLI composed that list inline, concatenating the families by
hand, so the list existed only inside the click callback: anything else
needing to know what check-iocs runs had to build its own copy, and the
two could drift apart without a test noticing.

Give each platform a command_modules.py holding the one list, and have
its CLI assign it. The modules check-iocs runs are unchanged, and a
test pins each list to the families it is composed of.

* Pin that check-iocs re-checks the results of custom modules

check-iocs takes its custom modules from load_custom_modules() like every
check-* command and matches result files to modules by slug, so a plugin
module's stored results are re-checked whenever it declares the
check-iocs pair of its platform; nothing asserted it.
2026-08-27 14:47:16 +02:00
Donncha Ó Cearbhaill 91da901741 Find the console log handler by type when changing verbosity (#899)
* Find the console log handler by type when changing verbosity

set_verbose_logging() adjusted the first handler on the "mvt" logger,
whichever handler that happened to be. Anything else attaching a
handler to that logger - an embedding application, a plugin, a test
harness - had it mistaken for the console and raised or lowered behind
its back, and the same slot can hold the file handler a command
attaches to its output folder, whose level a --verbose flag should
never decide: command.log records the whole run either way.

Walk the handlers instead and adjust only MVT's own console handler,
found by its MVTLogHandler type. Every other handler on the logger is
left alone. Behaviour is otherwise unchanged.

* Add a --verbose option to the mvt, mvt-ios and mvt-android commands

Verbosity was an option of each module-running command, so
"mvt-ios --verbose check-backup" was a usage error, a plugin command had to
define a flag of its own, and there was no way to get debug output from mvt at
all.

The option now sits on the three commands themselves and sets the level of
MVT's console handler for the run, through set_verbose_logging(), before any
command runs. Plugin commands registered on any of the three CLIs get the
option for free and need none of their own.

The per-command --verbose of the check-* commands is kept for backward
compatibility. It only ever raises the level, so the CLI's choice is never
undone by a command's default, and its help text says it is kept for
compatibility. It is to be removed in a later release.
2026-08-27 14:47:16 +02:00
Donncha Ó Cearbhaill dcfd500112 Add plugin update checking and a plugins command (#898)
* Add plugin update checking

Report available updates to the installed MVT plugin packages in the
startup banner, for plugins installed from a package index and for
plugins installed directly from a repository. Repository installs pinned
to a commit or a tag are never reported as outdated.

MVT only prints the command which upgrades a plugin. Installing the
update stays a deliberate choice of the analyst. The check runs at most
once every twelve hours, and in between prints the findings of the
latest check which still apply to what is installed.

Nothing about the check can interrupt a running command: the parts of
the suggested command come from package metadata and are quoted for the
shell, the repository query refuses to prompt for credentials and never
passes metadata as a git option, and a corrupt or stale cache is
discarded rather than trusted.

* Add a plugins command to list installed plugins and check updates

Add a "plugins" command to the platform-neutral mvt command. "plugins
list" shows every installed plugin package with its version, where it was
installed from, how many forensic modules it contributes and which
commands it adds. "plugins check-updates" checks for updates immediately,
without waiting for the automatic check, and prints the command which
upgrades a plugin instead of installing anything.

It lives on mvt only. The packages it lists extend mvt-ios and mvt-android
too, but auditing them is not the job of a command which analyses one
platform, and the two platform CLIs should not carry commands which are
not about an acquisition.

The command is registered as a built-in, before any external command, so
that an installed package cannot replace this audit surface.
2026-08-27 14:47:15 +02:00
Donncha Ó Cearbhaill a78894aaa5 Add a platform-neutral mvt command (#897)
* Add a platform-neutral mvt command

Several MVT commands have nothing to do with the acquisition of one
platform, yet they were reachable only through mvt-ios and mvt-android.
Asking which version is installed or downloading the public indicators
meant picking one of the two platform commands arbitrarily, and each of
those tasks had to be written, documented and maintained twice.

Add a third console script, mvt, hosting the commands which belong to no
platform: version and download-iocs for now, with completion following
in a later commit.

Commands installed in the new mvt.cli_plugins entry-point group are
registered on mvt, and on mvt only, so that a command package chooses
the CLI each of its commands is added to: mvt.ios.cli_plugins for
mvt-ios, mvt.android.cli_plugins for mvt-android and mvt.cli_plugins for
mvt. A command wanted on both platform CLIs is registered in both
platform groups; no group adds a command to every CLI. The
MVT_CUSTOM_COMMANDS variable loads command files and folders into mvt
the way the platform variables already do for mvt-ios and mvt-android.

Run on its own, mvt prints the banner and its help instead of a usage
error. The help text reminds that the forensic analysis of an
acquisition runs through mvt-ios and mvt-android, so that the command
which knows nothing about acquisitions says where they are analysed.

version and download-iocs stay on mvt-ios and mvt-android for now, so
that no documented invocation stops working. They are to be dropped from
the platform CLIs in a later release, once mvt has been available long
enough for the change to be announced.

Unlike mvt-ios and mvt-android, which point at their subpackages, the
console script points at mvt.cli:main and the mvt package re-exports
nothing of it. Importing mvt has to stay cheap and free of side effects:
it is the package plugins import from, and pulling in Click, the CLI and
everything the commands import merely because something imported mvt
would work against that.

While here, give the version command of both platform CLIs the context
settings every other command already has, so that "mvt-ios version -h"
prints its help instead of failing on an unknown option.

* Reduce indent for MVT CLI header

* Move shell completion to the mvt command and generate one script for every MVT command

Setting up shell completion had nothing to do with the acquisition of one
platform, yet it was a command of mvt-ios and mvt-android, each
generating the script of the program it ran under only. Completion now
leaves the platform CLIs for mvt, which emits one script covering mvt,
mvt-ios and mvt-android, installed as a single file loaded when the shell
starts. Click names the completion function of each program after the
program, so the three scripts concatenate without colliding, and fish
takes the file in conf.d rather than one named after a single command.

With one script there is nothing left for the platform commands to
generate, so their completion command goes, and with it the banner
suppression which was keyed on the command name: mvt-ios and mvt-android
now print the banner for every command they have.

The long help line says which commands the completion covers, so a
short_help keeps "mvt --help" from truncating it.

* Point the README and the command docs at plugin packages

Loading a command by path is for local development; a package is how a
command is distributed. The README's summary of what extends MVT now
names plugin packages and nothing else, and the section on loading a
command file says what it is for: keeping a command being written
loadable without reinstalling its package after every change.
2026-08-27 14:47:15 +02:00
Donncha Ó Cearbhaill 24645cb718 Register installed CLI plugins at program start (#896)
register_cli_plugins() ran while mvt.ios and mvt.android were being
imported, so importing any part of MVT executed the entry points of every
installed command package. That made plugin loading depend on import
order: a plugin importing from MVT while MVT was still initializing got an
ImportError and was quietly demoted to a broken command, and the same
plugin worked when MVT happened to be imported first.

Move the call into a main() function in each CLI module and point the
console scripts at it, so registration happens once when the program
starts and importing MVT no longer runs third-party code. For packagers:
mvt.ios:cli and mvt.android:cli stay importable, but a wrapper invoking
cli() directly no longer registers the installed plugin commands and
should call main() instead.
2026-08-27 14:47:14 +02:00
Donncha Ó Cearbhaill 104ffb167f Skip modules with unavailable dependencies instead of aborting the run (#895)
A module declaring a dependency its command does not provide made
_ordered_modules() give up on the whole run, so a single wrong declaration
in a module scoped to several commands turned a forensic analysis into
zero executed modules with one warning to explain it.

Drop only the modules that cannot run: the one with the unavailable
dependency, and anything depending on it. Each gets its own warning naming
the module missing a dependency and the dependency it is missing, and the
remaining modules run in the same stable topological order as before. A
cycle in the dependency graph is still a programming error and still stops
the run.
2026-08-27 14:47:14 +02:00
Donncha Ó CearbhaillandDonncha Ó Cearbhaill 097766a63b Add namespaced plugin configuration support (#894)
* Add namespaced plugin configuration support

Plugin packages need somewhere to keep their own settings, but MVT
rewrites its config.yaml with only the fields it knows about, so any
foreign section is dropped. Add MVTPluginSettings, a pydantic-settings
base class that gives each plugin its own file under the MVT config
folder and its own MVT_PLUGIN_<NAME>_ environment variable namespace.

Settings resolve from constructor arguments, then the environment, then
the plugin file, then the field defaults. Saving skips the values the
environment currently supplies, so credentials passed as environment
variables are not copied to disk, and writes through a private temporary
file so a settings file is never partially written or briefly readable
by other users.

* Add per-plugin data folders

Plugins had no sanctioned place to keep the data they persist, so the
plugin configuration documentation suggested a CACHE_FOLDER setting
defaulting to ~/.cache/example-plugin. That is a Linux convention which
is wrong on macOS, nothing expands or creates it, and it turns a path
into a setting a user can be asked to configure.

Add plugin_data_folder(), which returns the folder a plugin should use
for caches, downloaded artifacts, synchronization state or anything
else it writes to disk, and creates it if it is missing. The plugin
name is validated before anything is created, so a name holding a path
separator raises an error and leaves no folder behind, and calling the
function again returns the same folder with its contents untouched.

The folder sits under plugin-data rather than under the plugins folder
which holds the settings files. On macOS the configuration folder and
the data folder are the same directory, so reusing the plugins name
would leave each plugin's data folder in among the settings files.

Both the plugin-data folder and the folder of each plugin are created
with 0700 permissions. MVT is a forensic tool, and what a plugin keeps
there, such as API responses or sample metadata, is private by default.
The path is resolved on every call, as the configuration folder already
is, so it follows the current environment rather than whatever it was
when MVT was imported.

The documentation now points plugins at the helper, and the example
settings class carries a plain integer setting in place of its cache
folder.

* Derive the data folder of a plugin from its settings class

A plugin with a settings class already names itself in `plugin_name`;
passing the name again to plugin_data_folder() repeats it and can drift.
Add a `data_folder()` class method on MVTPluginSettings which returns
plugin_data_folder() for the class's validated plugin name (works on the
class and on an instance); plugin_data_folder(name) stays as the function
underneath for plugins without a settings class.

---------

Co-authored-by: Donncha Ó Cearbhaill <google@donncha.is>
2026-08-27 14:47:13 +02:00
va@resident 874f75bb4c Do not discard the whole tombstone on a "Caused by:" line (#892)
Keys are matched as bare prefixes, so `Caused by:` inside an abort message
reaches the `Cause` key, fails the key comparison and raises — and the
per-line loop turns that into an error that drops the entire text
tombstone, stack trace included.

A key mismatch means "this line is not that key", not "this file is
broken": decline the line and let the remaining keys have their turn. A
line with no colon is declined the same way instead of raising on the
unpack. The same trap has a second form in the field, HiSilicon/Huawei
tombstones printing `code around pc:` against the `code` key.
2026-08-25 21:35:33 +02:00
va@resident 3c8a581fd0 Do not end the whole androidqf run on an encrypted backup.ab (#891)
from_ab() raises InvalidAndroidBackup instead of exiting when it runs as a
sub-command, which check-androidqf catches to skip the backup modules. The
two password branches still called sys.exit(1) unconditionally, and since
run_backup_cmd() runs inside finish(), that ended the parent run before the
intrusion-logs command and before the timeline, alerts, urls, info and run
manifest were stored — leaving an output directory that looks complete but
has no alerts.json.

Also drop "as backup.ab is malformed" from the skip warning: it covers a
missing or wrong password too.
2026-08-25 20:18:36 +02:00
Janik Besendorf 65df483258 Parse multiline Android properties 2026-08-25 19:19:47 +02:00
Janik Besendorf 7ef26779bf Register added bugreport parsers 2026-08-22 14:21:51 +02:00
Janik Besendorf 3a43f6fcc7 Merge plaintext and protobuf tombstones 2026-08-22 14:21:51 +02:00
Janik Besendorf 45fdf85790 Add bugreport mountinfo parser 2026-08-22 14:21:51 +02:00
Janik Besendorf 627ab32d42 Add bugreport settings parser 2026-08-22 14:21:51 +02:00
Janik Besendorf 8b85972cc3 Add bugreport process table parser 2026-08-22 14:21:51 +02:00
Janik Besendorf 832e46604d Parse all platform compatibility overrides 2026-08-22 14:21:51 +02:00
Janik Besendorf a3c8b56102 Parse typed multi-user package details 2026-08-22 14:21:51 +02:00
Janik Besendorf e9bf197ec6 Parse all database operation details 2026-08-22 14:20:59 +02:00
Janik Besendorf 288b313649 Parse complete battery history events 2026-08-22 14:20:59 +02:00
Janik Besendorf 87a27f6e37 Normalize battery daily update records 2026-08-22 14:20:59 +02:00
Janik Besendorf 1bf2f9b35a Retain AppOps UID and event details 2026-08-22 14:20:59 +02:00
Janik Besendorf b904414bb0 Parse ADB binary XML keys 2026-08-22 14:20:59 +02:00
Janik Besendorf 008844b440 Parse accessibility service states per user 2026-08-22 14:20:59 +02:00
Janik Besendorf ab879bb23b Parse all package resolver categories 2026-08-22 14:20:59 +02:00
Janik Besendorf dc0650bf76 Fix bugreport command section extraction 2026-08-22 14:20:59 +02:00
Donncha Ó Cearbhaill dac4acb180 Load installed module packages via entry points (#883)
* Load installed module packages via entry points

Python packages can already register custom CLI commands which load
automatically, but custom modules still require --load-module or the
MVT_CUSTOM_MODULES environment variable on every invocation.

Add an mvt.modules entry-point group so installed packages can register
forensic modules which load automatically into every module-running
check-* command. An entry point resolves to an iterable of MVTModule
subclasses, or a callable returning one. Broken entry points are
skipped with a warning so a faulty package cannot break MVT.

* Record the source of loaded modules for auditability

Now that installed module packages load automatically, record where every
module came from:

- --list-modules groups the available modules by source, one line per
  source with the modules comma-separated: MVT itself with its version,
  each installed package with its version and VCS commit when recorded
  (PEP 610 direct_url.json), and each --load-module/MVT_CUSTOM_MODULES
  file with its SHA-256 hash.
- Commands log one line per module source with its version or hash and
  the modules loaded from it, so command.log records exactly which
  modules ran and where they came from.
- Make init_logging() idempotent: a loaded module package importing an
  MVT CLI module would previously add a second console handler and
  duplicate every console log line.

* Route loaded module logging under the mvt.ext namespace

Modules loaded from installed packages or file paths live outside the
mvt logger hierarchy, so their log records never reach MVT's console
and file handlers and instead fall through to logging.lastResort:
alerts print as bare unformatted lines and INFO messages are dropped
entirely.

Add get_module_logger() and use it everywhere module loggers are
created. Built-in mvt.* modules keep their existing logger names, and
everything external is parented under a dedicated mvt.ext namespace so
records reach the handlers and external names can never collide with
MVT's internal logger tree. File-path modules are named after their
file (mvt.ext.<stem>) instead of the mangled internal import name.

Document a naming convention for community module packages:
distribute as mvt-plugin-<name> with import package mvt_plugin_<name>,
including the publishing organization in the name. The prefix is
advisory (loading is by entry point, and it is no mark of
authenticity), but conforming packages get a cleaner logger namespace:
the mvt_plugin_ prefix is stripped, so mvt_plugin_amnesty_custom logs
as mvt.ext.amnesty_custom.
2026-08-19 23:15:48 +02:00
Donncha Ó Cearbhaill 30c11f68c7 Add WhatsApp contacts module and fix InteractionC contact resolution (#882)
* Add WhatsappContacts module to extract WhatsApp disappearing messages state

WhatsApp on iOS stores the disappearing messages timer for 1:1 chats on
the contact records in ContactsV2.sqlite, not in ChatStorage.sqlite. Add
a new WhatsappContacts module which extracts contact records from this
database, including phone numbers, WhatsApp and LID identifiers, and the
per-contact disappearing messages duration, and emits a timeline event
when a disappearing messages timer was set.

The database is often missing from incremental backups, so the module
logs a clear warning and returns no results instead of failing. Columns
are selected based on the actual table schema to tolerate changes across
WhatsApp versions, and if the disappearing messages column is absent the
state is reported as unknown rather than off.

The test fixture is a synthetic ContactsV2.sqlite with fictional
contacts, stored under the backup file ID derived from the WhatsApp
shared app group domain.

* Fix InteractionC contact resolution and resolve WhatsApp LIDs to contacts

The two primary InteractionC queries contained a SQL syntax error in
their direction CASE expression (a double column alias), so they always
failed and the module silently fell back to a reduced query without the
recipient join. As a result outgoing messages were serialized with no
counterpart at all ("from None (None)"). Fix the syntax so recipient
names and identifiers are extracted again, and normalize the raw 0/1
direction values from the fallback queries to INCOMING/OUTGOING.

WhatsApp identifies chat peers in interactionC.db by LID and stores the
peer LID in the domain identifier, which InteractionC could not map to a
person. Declare a dependency on the WhatsappContacts module and resolve
sender, recipient and domain identifiers (LID, JID or phone number)
against the WhatsApp contacts database, adding resolved phone number and
name fields to WhatsApp records.

Rewrite the timeline serialization to use the resolved values, fall back
to the chat peer from the domain identifier when no recipient was
recorded, label the local user instead of printing None, and include the
message direction and group name.

* Add timeline events for all WhatsApp contact timestamps

Extract ZABOUTEXPIRATIONTIMESTAMP and emit a timeline event for each
timestamp stored on a WhatsApp contact record: disappearing messages
timer changes, "about" text changes and scheduled expiry, and contact
record updates. ContactsV2.sqlite stores no other date attributes in
any released schema version.

* Add first and last interaction timeline events for WhatsApp chats

Extract one record per ZWACHATSESSION with the first and last stored
message dates, the session's own last-message date, the group creation
date and message counts. Each chat produces chat_first_message and
chat_last_message timeline events, and groups a group_created event.
The session last-message date is preferred over the newest stored
message because it survives message deletion.

* Resolve WhatsApp LID chat identifiers via the LID pair table

Recent WhatsApp versions key 1:1 chat sessions by an opaque LID rather
than the contact's phone number. Extract the ZWAPHONENUMBERLIDPAIR
table from the dedicated LID.sqlite database (or from ChatStorage
itself in versions that store it there) and use it to populate
partner_resolved_phone_number on chat session records and in timeline
events, without requiring the often-missing ContactsV2.sqlite. Each
pair is also extracted as a record and produces a lid_pair_recorded
timeline event marking when the association was learned.

* Reduce duplicate InteractionC timeline events

The interaction record's creation date normally trails its start date
by milliseconds, so serializing both nearly doubled the timeline with
duplicate entries. Only emit the creation date when it diverges from
the start date by more than an hour, with explicit wording, since a
record created long after its event indicates backfill by sync,
restore or tampering.

Per-contact aggregate dates from ZCONTACTS repeat on every interaction
row of the same contact and carried that row's message text. Serialize
them with contact-centric data strings instead, so timeline
de-duplication collapses them into one first/last-seen event per
contact.
2026-08-19 14:07:27 +02:00
besendorf 0ee25edf0a Fix dumpsys package system flag parsing (#874) 2026-08-14 15:58:05 +02:00
besendorf fa24b5465b Scope package fields to the primary user (#872) 2026-08-14 14:33:28 +02:00
besendorf 683b8ba133 Parse package installer from bugreports (#868) 2026-08-14 09:40:34 +02:00
besendorf d92a60c9be Preserve tombstone crash causes (#863) 2026-08-10 20:59:56 +02:00
besendorf 0b48d9fe1d Speed up compressed sysdiagnose analysis (#861)
* Speed up compressed sysdiagnose analysis

* ci: retrigger Ruff check
2026-08-07 09:07:33 +02:00
besendorf 067f053627 Add extensible CLI commands (#853)
* Add extensible CLI commands

* Handle plugin SystemExit failures
2026-08-05 23:30:28 +02:00
besendorf 93b7fb5232 Store message URLs in analysis output (#856) 2026-08-05 23:21:14 +02:00
besendorf 8617e0bf54 Alert on AndroidQF trusted ADB keys (#860) 2026-08-05 17:36:49 +02:00
besendorf f483223e23 Add iOS sysdiagnose checking (#832)
* Add iOS sysdiagnose checking

* Clarify documentation navigation
2026-07-28 18:59:58 +02:00
besendorf 2dfe3cbcb1 Fix module audit findings (#850)
* Fix module audit findings

* Always parse paired tombstones
2026-07-28 18:58:46 +02:00
besendorf 3eff0c550d Handle mis-indented dumpsys receiver actions (#852) 2026-07-28 18:34:13 +02:00
besendorf 797411e1e5 Fix text tombstone crashing thread parsing (#848) 2026-07-27 17:58:34 +02:00
84df51c518 Scan Safari profile databases for history and browser state (#846)
* fix(ios): scan Safari profile databases for history and browser state

Safari profiles (iOS 17 and later) keep their own databases under
Library/Safari/Profiles/<UUID>/, but SafariHistory and SafariBrowserState
only ever looked at the default profile's Library/Safari/History.db and
Library/Safari/BrowserState.db.

On a device where browsing happens inside a profile, MVT silently skipped
that history and still reported no detections, so an indicator only ever
visited within a profile went unnoticed.

Both modules now also match Library/Safari/Profiles/*/ in backups and in
filesystem dumps. No helper changes were needed: the Manifest.db lookup
already translates "*" into a SQL LIKE wildcard, and the filesystem lookup
already globs.

Found while examining an encrypted iOS 26.5.2 backup that contained 14
per-profile History.db files under
AppDomain-com.apple.mobilesafari::Library/Safari/Profiles/<UUID>/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ios): scope Safari redirects to history database

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Janik Besendorf <janik@besendorf.org>
2026-07-27 15:06:44 +02:00
besendorf a6c3a805d8 Parallelize URL indicator checks (#844) 2026-07-27 15:03:02 +02:00
besendorf 123c9081ed Skip resolving Google Maps short URLs (#843) 2026-07-19 17:13:57 +02:00