From fd265c4cff901f9d5cc14be6c4a85b9c507d9c2f Mon Sep 17 00:00:00 2001 From: Brigs Date: Fri, 3 Jul 2026 01:21:00 -0400 Subject: [PATCH] Android class 2 batch 2: Google Messages, TikTok, Google Chat, Discord Evidence-derived directions + conversation views, all validated against the Android test set: - Google Messages (bugle_db): direction from the sender participant's sub_id (self participants have sub_id != -2, the documented Bugle convention); adds Conversation ID. Validated 53/41 split over 11 conversations on one image. - TikTok: local account uid is the database filename prefix (_im.db); direction = sender uid == filename uid; adds Conversation ID from msg. Validated 9 Outgoing / 22 Incoming. - Google Chat (dynamite.db): account email is in the db path (user_accounts//); resolved to the gaia id via the users table of the same db; adds Group ID. Validated on two accounts (19/20 and 8/10 splits, owner rows matched by email). - Discord (kv-storage): account id from the @account. path (same derivation as the iLEAPP artifact); emits Sender ID / Account ID / Direction. Validated 18/29 split on the public image. New columns appended; existing column order unchanged. Blank direction when the owner cannot be established. --- scripts/artifacts/discordChats.py | 26 +++++++++++++++++++++-- scripts/artifacts/googleChat.py | 33 +++++++++++++++++++++++++---- scripts/artifacts/googleMessages.py | 21 ++++++++++++++---- scripts/artifacts/tikTok.py | 29 +++++++++++++++++++++---- 4 files changed, 95 insertions(+), 14 deletions(-) diff --git a/scripts/artifacts/discordChats.py b/scripts/artifacts/discordChats.py index 8429c6c8..720afdab 100644 --- a/scripts/artifacts/discordChats.py +++ b/scripts/artifacts/discordChats.py @@ -5,16 +5,27 @@ "description": "Parses Discord chat messages from the kv-storage key-value store", "author": "", "creation_date": "2023-09-18", - "last_update_date": "2023-09-18", + "last_update_date": "2026-07-03", "requirements": "none", "category": "Discord Chats", "notes": "", "paths": ('*/data/com.discord/files/kv-storage/*/a*',), "output_types": "standard", "artifact_icon": "message-square", + "data_views": { + "conversation": { + "conversationDiscriminatorColumn": "Channel ID", + "textColumn": "Content", + "directionColumn": "Direction", + "directionSentValue": "Outgoing", + "timeColumn": "Timestamp", + "senderColumn": "Username" + } + }, } } +import re import sys import json @@ -42,6 +53,9 @@ def make_printable(s): break # Skip all other files source_path = str(file_found) + # local account id is part of the kv-storage path + account_match = re.search(r'@account\.(\d+)', source_path) + account_id = account_match.group(1) if account_match else '' db = open_sqlite_db_readonly(file_found) cursor = db.cursor() cursor.execute(''' @@ -62,6 +76,7 @@ def make_printable(s): channelid = (data['channelId']) dataid = (data['id']) username = (data['message']['author']['username']) + sender_id = str(data['message']['author'].get('id', '')) content = (data['message']['content']) attachments = (data['message']['attachments']) if len(attachments) > 0: @@ -76,7 +91,11 @@ def make_printable(s): avatar = (data['message']['author']['avatar']) editedtimestamp = (data['message']['edited_timestamp']) - data_list.append((datatimestamp,channelid,dataid,username,content,attachementfilename,attachementurl,attachmentproxyurl,mentions,mentionroles,pinned,avatar,editedtimestamp)) + if account_id and sender_id: + direction = 'Outgoing' if sender_id == account_id else 'Incoming' + else: + direction = '' + data_list.append((datatimestamp,channelid,dataid,username,content,attachementfilename,attachementurl,attachmentproxyurl,mentions,mentionroles,pinned,avatar,editedtimestamp,sender_id,account_id,direction)) db.close() @@ -94,5 +113,8 @@ def make_printable(s): 'Pinned', 'Avatar', ('Edited Timestamp', 'datetime'), + 'Sender ID', + 'Account ID', + 'Direction', ) return data_headers, data_list, source_path diff --git a/scripts/artifacts/googleChat.py b/scripts/artifacts/googleChat.py index 1dbdc920..20fc85c6 100755 --- a/scripts/artifacts/googleChat.py +++ b/scripts/artifacts/googleChat.py @@ -5,7 +5,7 @@ "description": "Google Chat messages (dynamite.db)", "author": "Josh Hickman & Alexis Brignoni", "creation_date": "2021-02-05", - "last_update_date": "2021-02-05", + "last_update_date": "2026-07-03", "requirements": "blackboxprotobuf", "category": "Google Chat", "notes": "", @@ -14,6 +14,17 @@ '*/com.google.android.apps.dynamite/databases/user_accounts/*/dynamite.db*'), "output_types": "standard", "artifact_icon": "message-square", + "data_views": { + "conversation": { + "conversationDiscriminatorColumn": "Group ID", + "conversationLabelColumn": "Group Name", + "textColumn": "Message", + "directionColumn": "Direction", + "directionSentValue": "Outgoing", + "timeColumn": "Message Timestamp", + "senderColumn": "Sender" + } + }, }, "get_googleChat_groups": { "name": "Google Chat - Groups", @@ -64,6 +75,7 @@ import datetime import os +import re import sqlite3 import blackboxprotobuf @@ -139,19 +151,32 @@ def get_googleChat(files_found, report_folder, seeker, wrap_text): for source_path in _dbs(files_found): rows = _run(source_path, ''' SELECT topic_messages.create_time, Groups.name, users.name, topic_messages.text_body, - topic_messages.annotation + topic_messages.annotation, topic_messages.group_id, topic_messages.creator_id FROM topic_messages JOIN Groups on Groups.group_id=topic_messages.group_id JOIN users ON users.user_id=topic_messages.creator_id ORDER BY topic_messages.create_time ASC ''') + # the account email is in the db path (user_accounts//dynamite.db); + # resolve it to the gaia id via the users table of the same db + owner_id = '' + email_match = re.search(r'user_accounts/([^/]+)/dynamite\.db', source_path.replace('\\', '/')) + if email_match: + email_sql = email_match.group(1).replace("'", "''") + owner_rows = _run(source_path, f"SELECT user_id FROM users WHERE email = '{email_sql}'") + if owner_rows: + owner_id = str(owner_rows[0][0]) for r in rows: ann = _parse_annotation(r[4]) - data_list.append((_us_to_utc(r[0]), r[1], r[2], r[3]) + ann + (source_path,)) + if owner_id and r[6] is not None: + direction = 'Outgoing' if str(r[6]) == owner_id else 'Incoming' + else: + direction = '' + data_list.append((_us_to_utc(r[0]), r[1], r[2], r[3]) + ann + (source_path, r[5], direction)) data_headers = (('Message Timestamp', 'datetime'), 'Group Name', 'Sender', 'Message', 'Meeting Code', 'Meeting URL', 'Meeting Sender', 'Meeting Sender Profile Pic URL', - 'Filename', 'File Type', 'Width', 'Height', 'Source File') + 'Filename', 'File Type', 'Width', 'Height', 'Source File', 'Group ID', 'Direction') return data_headers, data_list, source_path diff --git a/scripts/artifacts/googleMessages.py b/scripts/artifacts/googleMessages.py index 76a570d3..38962502 100755 --- a/scripts/artifacts/googleMessages.py +++ b/scripts/artifacts/googleMessages.py @@ -5,13 +5,24 @@ "description": "Google Messages", "author": "Josh Hickman (josh@thebinaryhick.blog)", "creation_date": "2021-01-30", - "last_update_date": "2021-01-30", + "last_update_date": "2026-07-03", "requirements": "None", "category": "Google Messages", "notes": "", "paths": ('*/com.google.android.apps.messaging/databases/bugle_db*',), "output_types": "standard", "artifact_icon": "message-square", + "data_views": { + "conversation": { + "conversationDiscriminatorColumn": "Conversation ID", + "conversationLabelColumn": "Other Participant/Conversation Name", + "textColumn": "Message", + "directionColumn": "Direction", + "directionSentValue": "Outgoing", + "timeColumn": "Message Timestamp", + "senderColumn": "Message Sender" + } + }, } } @@ -44,7 +55,9 @@ def get_googleMessages(files_found, report_folder, seeker, wrap_text): WHEN parts.file_size_bytes=-1 THEN "N/A" ELSE parts.file_size_bytes END AS "Attachment Byte Size", - parts.local_cache_path AS "Attachment Location" + parts.local_cache_path AS "Attachment Location", + parts.conversation_id AS "Conversation ID", + CASE WHEN participants.sub_id != -2 THEN 'Outgoing' ELSE 'Incoming' END AS "Direction" FROM parts JOIN messages ON messages._id=parts.message_id @@ -57,7 +70,7 @@ def get_googleMessages(files_found, report_folder, seeker, wrap_text): for row in all_rows: timestamp = datetime.datetime.fromtimestamp(int(row[0]) / 1000, datetime.timezone.utc) if row[0] else '' - data_list.append((timestamp, row[1], row[2], row[3], row[4], row[5], row[6])) + data_list.append((timestamp, row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8])) - data_headers = (('Message Timestamp', 'datetime'), 'Message Type', 'Other Participant/Conversation Name', ('Message Sender', 'phonenumber'), 'Message', 'Attachment Byte Size', 'Attachment Location') + data_headers = (('Message Timestamp', 'datetime'), 'Message Type', 'Other Participant/Conversation Name', ('Message Sender', 'phonenumber'), 'Message', 'Attachment Byte Size', 'Attachment Location', 'Conversation ID', 'Direction') return data_headers, data_list, source_path diff --git a/scripts/artifacts/tikTok.py b/scripts/artifacts/tikTok.py index a26b638a..e7f0765f 100644 --- a/scripts/artifacts/tikTok.py +++ b/scripts/artifacts/tikTok.py @@ -5,13 +5,23 @@ "description": "", "author": "", "creation_date": "2021-03-02", - "last_update_date": "2021-03-02", + "last_update_date": "2026-07-03", "requirements": "none", "category": "TikTok", "notes": "", "paths": ('*_im.db*', '*db_im_xx*'), "output_types": "standard", "artifact_icon": "message-square", + "data_views": { + "conversation": { + "conversationDiscriminatorColumn": "Conversation ID", + "textColumn": "Message", + "directionColumn": "Direction", + "directionSentValue": "Outgoing", + "timeColumn": "Timestamp", + "senderColumn": "Nickname" + } + }, }, "get_tikTok_contacts": { "name": "TikTok - Contacts", @@ -29,6 +39,8 @@ } import datetime +import os +import re from scripts.ilapfuncs import artifact_processor, open_sqlite_db_readonly @@ -66,19 +78,28 @@ def get_tikTok(files_found, report_folder, seeker, wrap_text): case when read_status = 0 then 'Not read' when read_status = 1 then 'Read' else read_status - end local_info + end local_info, + conversation_id from db_im_xx.SIMPLE_USER, msg where UID = sender and json_valid(content) = 1 order by created_time ''') all_rows = cursor.fetchall() db.close() + # local account uid is the filename prefix: _im.db + uid_match = re.search(r'(\d+)_im\.db$', os.path.basename(maindb)) + account_uid = uid_match.group(1) if uid_match else '' for row in all_rows: timestamp = datetime.datetime.fromtimestamp(int(row[0]) / 1000, datetime.timezone.utc) if row[0] else '' - data_list.append((timestamp, row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8])) + if account_uid and row[1] is not None: + direction = 'Outgoing' if str(row[1]) == account_uid else 'Incoming' + else: + direction = '' + data_list.append((timestamp, row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8], + row[9], direction)) data_headers = (('Timestamp', 'datetime'), 'UID', 'Unique ID', 'Nickname', 'Message', 'Link GIF Name', - 'Link GIF URL', 'Read?', 'Local Info') + 'Link GIF URL', 'Read?', 'Local Info', 'Conversation ID', 'Direction') return data_headers, data_list, source_path