diff --git a/InternalPythonModules/android/fbmessenger.py b/InternalPythonModules/android/fbmessenger.py index 3a678f16d0..2347144f09 100644 --- a/InternalPythonModules/android/fbmessenger.py +++ b/InternalPythonModules/android/fbmessenger.py @@ -45,6 +45,7 @@ from org.sleuthkit.datamodel import Account from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CallMediaType import json import traceback @@ -181,15 +182,242 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): contactsDb.close() - - ## Adds a recipient to given list - def addRecipientToList(self, user_key, recipientList): + ## Extracts recipeint id from 'user_key' column and adds recipient to given list, + ## if the recipeint id is not the same as sender id + def addRecipientToList(self, user_key, senderId, recipientList): if user_key is not None: - recipientId = user_key.replace('FACEBOOK:', '') - recipientList.append(recipientId) + recipientId = user_key.replace('FACEBOOK:', '') + if recipientId != senderId: + recipientList.append(recipientId) + + + ## Extracts sender id from the json in 'sender' column. + def getSenderIdFromJson(self, senderJsonStr): + senderId = None; + if senderJsonStr is not None: + sender_dict = json.loads(senderJsonStr) + senderId = sender_dict['user_key'] + senderId = senderId.replace('FACEBOOK:', '') - ## Analyze messages - def analyzeMessages(self, dataSource, fileManager, context): + return senderId + + ## determines communication direction by comparing senderId with selfAccountId + def deduceDirectionFromSenderId(self, senderId): + direction = CommunicationDirection.UNKNOWN + if senderId is not None: + if senderId == self.selfAccountId: + direction = CommunicationDirection.OUTGOING + else: + direction = CommunicationDirection.INCOMING + return direction + + ## Analyzes messages + def analyzeMessages(self, threadsDb, threadsDBHelper): + try: + + ## Messages are found in the messages table. + ## This query filters messages by msg_type to only get actual user created conversation messages (msg_type 0). + ## The participant ids can be found in the thread_participants table. + ## Participant names are found in thread_users table. + ## Joining these tables produces multiple rows per message, one row for each recipient. + ## The result set is processed to collect the multiple recipients for a given message. + sqlString = """ + SELECT msg_id, text, sender, timestamp_ms, msg_type, messages.thread_key as thread_key, + snippet, thread_participants.user_key as user_key, thread_users.name as name + FROM messages + JOIN thread_participants ON messages.thread_key = thread_participants.thread_key + JOIN thread_users ON thread_participants.user_key = thread_users.user_key + WHERE msg_type = 0 + ORDER BY msg_id + """ + + messagesResultSet = threadsDb.runQuery(sqlString) + if messagesResultSet is not None: + oldMsgId = None + + direction = CommunicationDirection.UNKNOWN + fromId = None + recipientIdsList = None + timeStamp = -1 + msgText = "" + threadId = "" + + while messagesResultSet.next(): + msgId = messagesResultSet.getString("msg_id") + + # new msg begins when msgId changes + if msgId != oldMsgId: + # Create message artifact with collected attributes + if oldMsgId is not None: + messageArtifact = threadsDBHelper.addMessage( + self._MESSAGE_TYPE, + direction, + fromId, + recipientIdsList, + timeStamp, + MessageReadStatus.UNKNOWN, + "", # subject + msgText, + threadId) + + oldMsgId = msgId + + # New message - collect all attributes + recipientIdsList = [] + + ## get sender id by parsing JSON in sender column + fromId = self.getSenderIdFromJson(messagesResultSet.getString("sender")) + direction = self.deduceDirectionFromSenderId(fromId) + + # Get recipient and add to list + self.addRecipientToList(messagesResultSet.getString("user_key"), fromId, + recipientIdsList) + + timeStamp = messagesResultSet.getLong("timestamp_ms") / 1000 + + # Get msg text + # Sometimes there may not be an explict msg text, + # but an app generated snippet instead + msgText = messagesResultSet.getString("text") + if not msgText: + msgText = messagesResultSet.getString("snippet") + + # TBD: get attachment + + threadId = messagesResultSet.getString("thread_key") + + else: # same msgId as last, just collect recipient from current row + self.addRecipientToList(messagesResultSet.getString("user_key"), fromId, + recipientIdsList) + + + # at the end of the loop, add last message + messageArtifact = threadsDBHelper.addMessage( + self._MESSAGE_TYPE, + direction, + fromId, + recipientIdsList, + timeStamp, + MessageReadStatus.UNKNOWN, + "", # subject + msgText, + threadId) + + except SQLException as ex: + self._logger.log(Level.WARNING, "Error processing query result for FB Messenger messages.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + self._logger.log(Level.SEVERE, "Failed to add FB Messenger message artifacts.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + self._logger.log(Level.WARNING, "Failed to post artifacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + ## Analyzes call logs + def analyzeCallLogs(self, threadsDb, threadsDBHelper): + try: + + ## Call logs are found in the messages table. + ## msg_type indicates type of call: + ## 9: one to one calls + ## 203: group call + ## 1-to-1 calls only have a call_ended record. + ## group calls have a call_started_record as well as call_ended recorded, with *different* message ids. + ## all the data we need can be found in the call_ended record. + + sqlString = """ + SELECT msg_id, text, sender, timestamp_ms, msg_type, admin_text_thread_rtc_event, + generic_admin_message_extensible_data, + messages.thread_key as thread_key, + thread_participants.user_key as user_key, + thread_users.name as name + FROM messages + JOIN thread_participants ON messages.thread_key = thread_participants.thread_key + JOIN thread_users ON thread_participants.user_key = thread_users.user_key + WHERE msg_type = 9 OR (msg_type = 203 AND admin_text_thread_rtc_event = 'group_call_ended') + ORDER BY msg_id + """ + + messagesResultSet = threadsDb.runQuery(sqlString) + if messagesResultSet is not None: + oldMsgId = None + + direction = CommunicationDirection.UNKNOWN + callerId = None + calleeIdsList = None + startTimeStamp = -1 + endTimeStamp = -1 + duration = 0 + mediaType = CallMediaType.AUDIO + + while messagesResultSet.next(): + msgId = messagesResultSet.getString("msg_id") + + # new call begins when msgId changes + if msgId != oldMsgId: + # Create call log artifact with collected attributes + if oldMsgId is not None: + messageArtifact = threadsDBHelper.addCalllog( + direction, + callerId, + calleeIdsList, + startTimeStamp, + endTimeStamp, + mediaType ) + + oldMsgId = msgId + + # New message - collect all attributes + calleeIdsList = [] + + ## get caller id by parsing JSON in sender column + callerId = self.getSenderIdFromJson(messagesResultSet.getString("sender")) + direction = self.deduceDirectionFromSenderId(callerId) + + # Get recipient and add to list + self.addRecipientToList(messagesResultSet.getString("user_key"), callerId, + calleeIdsList) + + # the timestamp from call ended msg is used as end timestamp + endTimeStamp = messagesResultSet.getLong("timestamp_ms") / 1000 + + # parse the generic_admin_message_extensible_data JSON to extract the duration and video fields + adminDataJsonStr = messagesResultSet.getString("generic_admin_message_extensible_data") + if adminDataJsonStr is not None: + adminData_dict = json.loads(adminDataJsonStr) + duration = adminData_dict['call_duration'] # call duration in seconds + isVideo = adminData_dict['video'] + if isVideo: + mediaType = CallMediaType.VIDEO + + startTimeStamp = endTimeStamp - duration + + else: # same msgId as last, just collect callee from current row + self.addRecipientToList(messagesResultSet.getString("user_key"), callerId, + calleeIdsList) + + # at the end of the loop, add last message + messageArtifact = threadsDBHelper.addCalllog( + direction, + callerId, + calleeIdsList, + startTimeStamp, + endTimeStamp, + mediaType ) + + except SQLException as ex: + self._logger.log(Level.WARNING, "Error processing query result for FB Messenger call logs.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + self._logger.log(Level.SEVERE, "Failed to add FB Messenger call log artifacts.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + self._logger.log(Level.WARNING, "Failed to post FB Messenger call log artifacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + + ## Analyze messages and call log threads + def analyzeMessagesAndCallLogs(self, dataSource, fileManager, context): threadsDbs = AppSQLiteDB.findAppDatabases(dataSource, "threads_db2", True, self._FB_MESSENGER_PACKAGE_NAME) for threadsDb in threadsDbs: try: @@ -202,113 +430,12 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): self._MODULE_NAME, threadsDb.getDBFile(), Account.Type.FACEBOOK) - ## Messages are found in the messages table. - ## This query filters messages by msg_type to only get actual user created conversation messages (msg_type 0). - ## The participant ids can be found in the thread_participants table. - ## Participant names are found in thread_users table. - ## Joining these tables produces multiple rows per message, one row for each recipient. - ## The result set is processed to collect the multiple recipients for a given message. - sqlString = """ - SELECT msg_id, text, sender, timestamp_ms, msg_type, messages.thread_key as thread_key, - snippet, thread_participants.user_key as user_key, thread_users.name as name - FROM messages - JOIN thread_participants ON messages.thread_key = thread_participants.thread_key - JOIN thread_users ON thread_participants.user_key = thread_users.user_key - WHERE msg_type = 0 - ORDER BY msg_id - """ - - messagesResultSet = threadsDb.runQuery(sqlString) - if messagesResultSet is not None: - oldMsgId = None - - direction = CommunicationDirection.UNKNOWN - fromId = None - recipientIdsList = None - timeStamp = -1 - msgText = "" - threadId = "" - - while messagesResultSet.next(): - msgId = messagesResultSet.getString("msg_id") - - # new msg begins when msgId changes - if msgId != oldMsgId: - # Create message artifact with collected attributes - if oldMsgId is not None: - messageArtifact = threadsDBHelper.addMessage( - self._MESSAGE_TYPE, - direction, - fromId, - recipientIdsList, - timeStamp, - MessageReadStatus.UNKNOWN, - "", # subject - msgText, - threadId) - - oldMsgId = msgId - - # New message - collect all attributes - recipientIdsList = [] - - ## get sender id by parsing JSON in sender column - senderJsonStr = messagesResultSet.getString("sender") - if senderJsonStr is not None: - sender_dict = json.loads(senderJsonStr) - senderId = sender_dict['user_key'] - senderId = senderId.replace('FACEBOOK:', '') - senderName = sender_dict['name'] - fromId = senderId - if senderId == self.selfAccountId: - direction = CommunicationDirection.OUTGOING - else: - direction = CommunicationDirection.INCOMING - - - # Get recipient and add to list - self.addRecipientToList(messagesResultSet.getString("user_key"), - recipientIdsList) - - timeStamp = messagesResultSet.getLong("timestamp_ms") / 1000 - - # Get msg text - # Sometimes there may not be an explict msg text, - # but an app generated snippet instead - msgText = messagesResultSet.getString("text") - if not msgText: - msgText = messagesResultSet.getString("snippet") - - # TBD: get attachment - - threadId = messagesResultSet.getString("thread_key") - - else: # same msgId as last, just collect recipient from current row - self.addRecipientToList(messagesResultSet.getString("user_key"), - recipientIdsList) - - - # at the end of the loop, add last message - messageArtifact = threadsDBHelper.addMessage( - self._MESSAGE_TYPE, - direction, - fromId, - recipientIdsList, - timeStamp, - MessageReadStatus.UNKNOWN, - "", # subject - msgText, - threadId) + self.analyzeMessages(threadsDb, threadsDBHelper) + self.analyzeCallLogs(threadsDb, threadsDBHelper) - except SQLException as ex: - self._logger.log(Level.WARNING, "Error processing query result for FB Messenger messages.", ex) - self._logger.log(Level.WARNING, traceback.format_exc()) except TskCoreException as ex: - self._logger.log(Level.SEVERE, "Failed to add FB Messenger message artifacts.", ex) + self._logger.log(Level.SEVERE, "Failed to to create CommunicationArtifactsHelper for FB Messenger.", ex) self._logger.log(Level.SEVERE, traceback.format_exc()) - except BlackboardException as ex: - self._logger.log(Level.WARNING, "Failed to post artifacts.", ex) - self._logger.log(Level.WARNING, traceback.format_exc()) finally: threadsDb.close() @@ -321,6 +448,6 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): return self.analyzeContacts(dataSource, fileManager, context) - self.analyzeMessages(dataSource, fileManager, context) + self.analyzeMessagesAndCallLogs(dataSource, fileManager, context)