From 226b74c5ace535d4e74615935f30e9942bd4737d Mon Sep 17 00:00:00 2001 From: Brigs Date: Fri, 3 Jul 2026 00:59:00 -0400 Subject: [PATCH] Android class 2: derived directions + conversation views (6 artifacts) Validated against the 12-image Android test set: - WhatsApp One-To-One and Group Messages: views only; both already emit Message Direction ('Outgoing') and a stable thread column (other participant / conversation name). ~10k real rows verified. - Google Voice Messages: view only; Direction/Conversation ID verified in test data ('Outgoing'/'Incoming'). - Teleguard: owner serverId from the service table 'user' row (same db, same source as iOS); adds chatId as Chat ID. Validated 19/21 split with blanks on ownerless service rows. - Facebook Messenger (msys): local uid read from the threads_db2-uid file (added to the artifact's paths); direction = sender_id == uid. Validated across 3 images (e.g. 14 Outgoing / 12 Incoming). - IMO: emits buid as Chat Partner (thread was previously only derivable by From/To swap); direction already existed. buid populated 22/22 in test data. New columns appended; existing column order unchanged. --- scripts/artifacts/FacebookMessenger.py | 30 ++++++++++++++++++++++--- scripts/artifacts/WhatsApp.py | 27 ++++++++++++++++++++-- scripts/artifacts/googleVoice.py | 13 ++++++++++- scripts/artifacts/imo.py | 16 +++++++++++-- scripts/artifacts/teleguard.py | 31 ++++++++++++++++++++++---- 5 files changed, 105 insertions(+), 12 deletions(-) diff --git a/scripts/artifacts/FacebookMessenger.py b/scripts/artifacts/FacebookMessenger.py index 7cc5a4db..60605aca 100755 --- a/scripts/artifacts/FacebookMessenger.py +++ b/scripts/artifacts/FacebookMessenger.py @@ -18,13 +18,23 @@ "description": "Facebook/Messenger chat messages (msys_database)", "author": "Kevin Pagano", "creation_date": "2021-03-03", - "last_update_date": "2021-03-03", + "last_update_date": "2026-07-03", "requirements": "none", "category": "Facebook Messenger", "notes": "", "paths": ('*/msys_database*',), "output_types": "standard", "artifact_icon": "message-square", + "data_views": { + "conversation": { + "conversationDiscriminatorColumn": "Thread Key", + "textColumn": "Message", + "directionColumn": "Direction", + "directionSentValue": "Outgoing", + "timeColumn": "Message Timestamp", + "senderColumn": "Sender" + } + }, }, "get_fb_msys_calls": { "name": "Facebook Messenger - Calls (msys_database)", @@ -164,6 +174,16 @@ def get_fb_msys_chats(files_found, report_folder, seeker, wrap_text): if 'msys_database_' not in file_found: continue source = source or file_found + # local account uid from the threads_db2-uid file (fetched via paths) + fb_uid = '' + for uid_file in files_found: + if str(uid_file).endswith('threads_db2-uid'): + try: + with open(str(uid_file), 'r', encoding='utf-8', errors='replace') as dat: + fb_uid = next((line.strip() for line in dat if line.strip()), '') + except OSError: + fb_uid = '' + break rel = _src(file_found, seeker) db = open_sqlite_db_readonly(file_found) cursor = db.cursor() @@ -196,16 +216,20 @@ def get_fb_msys_chats(files_found, report_folder, seeker, wrap_text): ORDER BY messages.timestamp_ms ASC ''') for row in rows: + if fb_uid and row[2] is not None: + direction = 'Outgoing' if str(row[2]) == fb_uid else 'Incoming' + else: + direction = '' data_list.append((_str_to_utc(row[0]), row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8], row[9], row[10], row[11], _str_to_utc(row[12]), row[13], - row[14], rel)) + row[14], rel, direction)) db.close() data_headers = (('Message Timestamp', 'datetime'), 'Sender', 'Sender ID', 'Thread Key', 'Message', 'Snippet', 'Call/Location Information', 'Attachment Name', 'Attachment Type', 'Attachment URL', 'Location Lat/Long', 'Reaction', ('Reaction Timestamp', 'datetime'), 'Is Admin Message', 'Message ID', - 'Source File') + 'Source File', 'Direction') return data_headers, data_list, source diff --git a/scripts/artifacts/WhatsApp.py b/scripts/artifacts/WhatsApp.py index 6a7bb7ce..80d8a30b 100755 --- a/scripts/artifacts/WhatsApp.py +++ b/scripts/artifacts/WhatsApp.py @@ -44,7 +44,7 @@ "description": "WhatsApp 1:1 messages (modern msgstore.db schema)", "author": "", "creation_date": "2021-03-11", - "last_update_date": "2021-03-11", + "last_update_date": "2026-07-03", "requirements": "none", "category": "WhatsApp", "notes": "", @@ -53,13 +53,25 @@ '*/Android/media/com.whatsapp/WhatsApp/Media/*'), "output_types": "standard", "artifact_icon": "message-square", + "data_views": { + "conversation": { + "conversationDiscriminatorColumn": "Other Participant WA User Name", + "textColumn": "Message", + "directionColumn": "Message Direction", + "directionSentValue": "Outgoing", + "timeColumn": "Message Timestamp", + "senderColumn": "Other Participant WA User Name", + "sentMessageStaticLabel": "Local User", + "mediaColumn": "Media" + } + }, }, "get_whatsapp_group_messages": { "name": "WhatsApp - Group Messages", "description": "WhatsApp group messages (modern msgstore.db schema)", "author": "", "creation_date": "2021-03-11", - "last_update_date": "2021-03-11", + "last_update_date": "2026-07-03", "requirements": "none", "category": "WhatsApp", "notes": "", @@ -68,6 +80,17 @@ '*/Android/media/com.whatsapp/WhatsApp/Media/*'), "output_types": "standard", "artifact_icon": "message-square", + "data_views": { + "conversation": { + "conversationDiscriminatorColumn": "Conversation Name", + "textColumn": "Message", + "directionColumn": "Message Direction", + "directionSentValue": "Outgoing", + "timeColumn": "Message Timestamp", + "senderColumn": "Sending Party", + "mediaColumn": "Media" + } + }, }, "get_whatsapp_group_details": { "name": "WhatsApp - Group Details", diff --git a/scripts/artifacts/googleVoice.py b/scripts/artifacts/googleVoice.py index 24011419..307336f1 100644 --- a/scripts/artifacts/googleVoice.py +++ b/scripts/artifacts/googleVoice.py @@ -44,13 +44,24 @@ "description": "Parses Google Voice Messages", "author": "William Campbell (@campwill), Eli Ehresmann (@H-Seek), Reina Girouard (@rgrd59), Paula Rokusek (@paula-rokusek)", "creation_date": "2025-10-22", - "last_update_date": "2025-11-5", + "last_update_date": "2026-07-03", "requirements": "blackboxprotobuf", "category": "Google Voice", "notes": "Tested on version 2025.07.20.788599304 (October 29th, 2025). Tested on Samsung and Motorola devices.", "paths": ('*/data/com.google.android.apps.googlevoice/files/accounts/*/LegacyMsgDbInstance.db*', '*/data/com.google.android.apps.googlevoice/cache/Photo MMS images/*', '*/data/com.samsung.android.providers.contacts/databases/contact*'), "output_types": ["html", "tsv", "lava"], "artifact_icon": "user", + "data_views": { + "conversation": { + "conversationDiscriminatorColumn": "Conversation ID", + "textColumn": "Message", + "directionColumn": "Direction", + "directionSentValue": "Outgoing", + "timeColumn": "Timestamp", + "senderColumn": "Sender", + "mediaColumn": "Image" + } + }, } } diff --git a/scripts/artifacts/imo.py b/scripts/artifacts/imo.py index cd624ca7..8a79f233 100755 --- a/scripts/artifacts/imo.py +++ b/scripts/artifacts/imo.py @@ -18,13 +18,24 @@ "description": "", "author": "", "creation_date": "2021-03-11", - "last_update_date": "2021-03-11", + "last_update_date": "2026-07-03", "requirements": "none", "category": "IMO", "notes": "", "paths": ('*/com.imo.android.imous/databases/imofriends.db*',), "output_types": "standard", "artifact_icon": "message-square", + "data_views": { + "conversation": { + "conversationDiscriminatorColumn": "Chat Partner", + "textColumn": "Last Message", + "directionColumn": "Direction", + "directionSentValue": "Outgoing", + "timeColumn": "Timestamp", + "senderColumn": "Chat Partner", + "sentMessageStaticLabel": "Local User" + } + }, } } @@ -101,7 +112,7 @@ def get_imo_messages(files_found, report_folder, seeker, wrap_text): attachmentPath = attachmentLocalPath timestamp = datetime.datetime.fromtimestamp(int(row[3]), datetime.timezone.utc) - data_list.append((timestamp, from_id, to_id, row[2], row[4], row[5], attachmentPath)) + data_list.append((timestamp, from_id, to_id, row[2], row[4], row[5], attachmentPath, row[0])) db.close() data_headers = ( @@ -112,5 +123,6 @@ def get_imo_messages(files_found, report_folder, seeker, wrap_text): 'Direction', 'Message Read', 'Attachment', + 'Chat Partner', ) return data_headers, data_list, source_path diff --git a/scripts/artifacts/teleguard.py b/scripts/artifacts/teleguard.py index 6648dcd4..8ba62530 100644 --- a/scripts/artifacts/teleguard.py +++ b/scripts/artifacts/teleguard.py @@ -5,7 +5,7 @@ "description": "Teleguard messenger messages", "author": "", "creation_date": "2024-01-09", - "last_update_date": "2024-01-09", + "last_update_date": "2026-07-03", "requirements": "none", "category": "Teleguard", "notes": "", @@ -13,6 +13,17 @@ '*/data/ch.swisscows.messenger.teleguardapp/cache/**'), "output_types": "standard", "artifact_icon": "message-square", + "data_views": { + "conversation": { + "conversationDiscriminatorColumn": "Chat ID", + "textColumn": "Content", + "directionColumn": "Direction", + "directionSentValue": "Outgoing", + "timeColumn": "Timestamp", + "senderColumn": "Sender", + "mediaColumn": "Media" + } + }, }, "get_teleguard_posts": { "name": "Teleguard - Posts", @@ -99,9 +110,16 @@ def get_teleguard(files_found, report_folder, seeker, wrap_text): source_path = _db(files_found) rows = _run(source_path, ''' SELECT datetime(createDate/1000,'unixepoch'), datetime(userTime/1000,'unixepoch'), - type, sender, receiver, content, metadata, status, isEdited + type, sender, receiver, content, metadata, status, isEdited, chatId FROM messages ''') + # local account id lives in the service table ('user' row) of the same db + owner_id = '' + for (svc_data,) in _run(source_path, "SELECT data FROM service WHERE id = 'user'"): + try: + owner_id = (json.loads(svc_data) or {}).get('serverId', '') + except (ValueError, TypeError): + owner_id = '' data_list = [] for row in rows: media_refs = [] @@ -129,11 +147,16 @@ def get_teleguard(files_found, report_folder, seeker, wrap_text): media_cell = media_refs else: media_cell = '' + if owner_id and row[3]: + direction = 'Outgoing' if row[3] == owner_id else 'Incoming' + else: + direction = '' data_list.append((_str_to_utc(row[0]), _str_to_utc(row[1]), row[2], row[3], row[4], row[5], - media_cell, row[6], row[7], row[8])) + media_cell, row[6], row[7], row[8], direction, row[9])) data_headers = (('Timestamp', 'datetime'), ('User Time', 'datetime'), 'Type', 'Sender', 'Receiver', - 'Content', ('Media', 'media'), 'Metadata', 'Status', 'Is Edited?') + 'Content', ('Media', 'media'), 'Metadata', 'Status', 'Is Edited?', 'Direction', + 'Chat ID') return data_headers, data_list, source_path