* 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.
* 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>
* Replace split("\n") with splitlines() for platform compatibility
* Remove dead commented-out code in webkit_session_resource_log
* Remove stale FIXME comment in command.py
* Narrow bare except to specific exception types in convert_mactime_to_datetime
* Fix typo in aqf_files.py comment
* Refactor b64 encoding in configuration_profiles into helper methods
* Pass branch parameter to GitHub commits API in update checker
* Replace bare KeyError catch with explicit key check in net_base
* Remove confirmed Chrome database path TODOs
Backup IDs verified via SHA-1 of AppDomain-com.google.chrome.ios paths.
* Extract additional timestamps from WebKit ObservedDomains table
Query mostRecentUserInteractionTime and mostRecentWebPushInteractionTime
with fallback to the original 4-column query for older iOS versions.
* Clarify command_line list format matches protobuf schema in tombstone parser
* Support SHA1 and MD5 hash matching in AQF files module
* Remove resolved TODO about --output requirement in download-apks
* Clean up code TODOs and type checks
* Fix WebKit timestamp schema handling
* Run bugreport and backup modules during check-androidqf
Adding support to automatically run ADB backup and bugreport modules
automatically when running the check-androidqf command. This is a first
step to deduplicate the code for Android modules.
* Deduplicate modules which are run by the sub-commands.
* Raise the proper NoAndroidQFBackup exception when a back-up isn't found
* Remove check-adb command and update docs
* Remove check-apk code and old dependencies
* Major refactor to add structured alerting and typed indicators
This commit makes a structural change to MVT by changing binary
detected/not detected logic into a structured multi-level system
of alerts. This gives far more power to extend MVT and manage
alerts.
This commit also begins the process of adding proper typing for
key objects used in MVT including Indicators, IndicatorMatches,
and ModuleResults. This will also be keep to programmatically using
the output of MVT.
* Fix up, remove ADB module base
* Rework old detections tracking into stuctured alert levels
* Quote STIX path in log line
* Fix profile events log line
* Close open archive (zip/tar) file handles
* Fix root_binaries and mounts modules to use alertstore
* Update tests to use alertstore instead of detected attribute
* Fix alertstore method calls - use high() instead of warning()
* Fix remaining test errors
- Add log_latest() call in root_binaries to log each alert
- Fix UnboundLocalError in cmd_check_androidqf by initializing bugreport variable
- Remove incorrect backup.close() call since load_backup() returns bytes
- Remove duplicate from_ab method in cmd_check_backup that was using old attributes
* Log alerts on add
* Remove slug from alertstore calls
* update alerts.py
* update alerts.py
* move indicator_match to alert object
* .
* - Remove timeline_detected and route to alertstore
* fix typing for mypy
* Remove unused type imports
* Fix check_receiver_prefix and check_android_property_name
- check_receiver_prefix() used dict syntax (ioc["value"]) on Indicator
dataclass objects from get_iocs(). Changed to ioc.value/ioc.name.
- check_receiver_prefix() returned raw ioc instead of IndicatorMatch.
Now returns IndicatorMatch with descriptive message.
- Fixed return type annotations on both methods to Optional[IndicatorMatch].
- Removed unused Union import.
* Fix residual self.detected usage in packages and dumpsys_receivers
These modules still used self.detected.append() which no longer exists
after the alertstore migration. Converted to alertstore calls:
- packages.py: ROOT_PACKAGES detection → alertstore.high()
- dumpsys_receivers.py: receiver IOC match → alertstore.critical()
* Fix SMS module alertstore.high() call passing slug as message
The first argument was self.get_slug() (module slug) instead of a
human-readable message. The module is already auto-detected via
AlertStore._get_calling_module(). Also removed redundant log_latest().
* Apply suggestions from code review
Fix JSON serialization in `module.save_to_json` and fix argument order in iOS alertstore calls.
Co-authored-by: tes <tesitura@users.noreply.github.com>
* Remove unsupported ADB modules
* Fail removed check-adb command
* Fix alert serialization and logging
* Close sqlite connections in iOS modules
* Fix DEBUG messages not reaching handlers, save_to_json for dictionary results and TypeError on mixed event_time types in safary_history
* add matched_indicator via alertstore instead of directly modifying json objects
* Alert on battery daily uninstall and downgrade
* Lower alert severity to medium for suspicious items
* Switch version to 2026.4.28 CalVer
---------
Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
Co-authored-by: tes <tesitura@users.noreply.github.com>
Co-authored-by: Janik Besendorf <janik.besendorf@reporter-ohne-grenzen.de>